All Downloads are FREE. Search and download functionalities are using the official Maven repository.

space.vectrix.flare.fastutil.Short2ObjectSyncMapImpl Maven / Gradle / Ivy

There is a newer version: 2.0.1
Show newest version
/*
 * This file is part of flare, licensed under the MIT License (MIT).
 *
 * Copyright (c) vectrix.space 
 * Copyright (c) contributors
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 */
package space.vectrix.flare.fastutil;

import static java.util.Objects.requireNonNull;

import it.unimi.dsi.fastutil.shorts.AbstractShort2ObjectMap;
import it.unimi.dsi.fastutil.shorts.Short2ObjectFunction;
import it.unimi.dsi.fastutil.shorts.Short2ObjectMap;
import it.unimi.dsi.fastutil.shorts.Short2ObjectMaps;
import it.unimi.dsi.fastutil.objects.AbstractObjectSet;
import it.unimi.dsi.fastutil.objects.ObjectIterator;
import it.unimi.dsi.fastutil.objects.ObjectSet;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;

import java.util.AbstractMap;
import java.util.Iterator;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicReferenceFieldUpdater;
import java.util.function.BiConsumer;
import java.util.function.BiFunction;
import java.util.function.Consumer;
import java.util.function.IntFunction;
import java.util.function.IntFunction;

/* package */ final class Short2ObjectSyncMapImpl extends AbstractShort2ObjectMap implements Short2ObjectSyncMap {
  private static final long serialVersionUID = 1;

  /**
   * A single implicit lock when dealing with {@code dirty} mutations.
   */
  private transient final Object lock = new Object();

  /**
   * The read only map that does not require a lock and does not allow mutations.
   */
  private transient volatile Short2ObjectMap> read;

  /**
   * Represents whether the {@code dirty} map has changes the {@code read} map
   * does not have yet.
   */
  private transient volatile boolean amended;

  /**
   * The read/write map that requires a lock and allows mutations.
   */
  private transient Short2ObjectMap> dirty;

  /**
   * Represents the amount of times an attempt has been made to access the
   * {@code dirty} map while {@code amended} is {@code true}.
   */
  private transient int misses;

  private transient EntrySetView entrySet;

  private final IntFunction>> function;
  private final float promotionFactor;

  /* package */ Short2ObjectSyncMapImpl(final @NonNull IntFunction>> function, final int initialCapacity) {
    this(function, initialCapacity, 1.0F);
  }

  /* package */ Short2ObjectSyncMapImpl(final @NonNull IntFunction>> function, final int initialCapacity, final float promotionFactor) {
    if(promotionFactor <= 0.0F || promotionFactor > 1.0F) throw new IllegalArgumentException("Promotion factor must be more than 0 and less than or equal to 1");
    this.function = function;
    this.promotionFactor = promotionFactor;
    this.read = function.apply(initialCapacity);
  }

  // Query Operations

  @Override
  public int size() {
    this.promoteIfNeeded();
    int size = 0;
    for(final ExpungingValue value : this.read.values()) {
      if(value.exists()) size++;
    }
    return size;
  }

  @Override
  public boolean isEmpty() {
    this.promoteIfNeeded();
    for(final ExpungingValue value : this.read.values()) {
      if(value.exists()) return false;
    }
    return true;
  }

  @Override
  public boolean containsValue(final @Nullable Object value) {
    for(final Short2ObjectMap.Entry entry : this.short2ObjectEntrySet()) {
      if(Objects.equals(entry.getValue(), value)) return true;
    }
    return false;
  }

  @Override
  public boolean containsKey(final short key) {
    ExpungingValue entry;
    if((entry = this.read.get(key)) == null && this.amended) {
      synchronized(this.lock) {
        if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
          entry = this.dirty.get(key);
          // The slow path should be avoided, even if the value does
          // not match or is present. So we mark a miss, to eventually
          // promote and take a faster path.
          this.missLocked();
        }
      }
    }
    return entry != null && entry.exists();
  }

  @Override
  public @Nullable V get(final short key) {
    ExpungingValue entry;
    if((entry = this.read.get(key)) == null && this.amended) {
      synchronized(this.lock) {
        if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
          entry = this.dirty.get(key);
          // The slow path should be avoided, even if the value does
          // not match or is present. So we mark a miss, to eventually
          // promote and take a faster path.
          this.missLocked();
        }
      }
    }
    return entry != null ? entry.get() : null;
  }

  @Override
  @SuppressWarnings("ConstantConditions")
  public @Nullable V computeIfAbsent(final short key, final @NonNull IntFunction mappingFunction) {
    requireNonNull(mappingFunction, "mappingFunction");
    ExpungingValue entry; V current;
    if((entry = this.read.get(key)) != null) {
      if((current = entry.get()) == null) current = entry.set(mappingFunction.apply(key));
      return current;
    }
    synchronized(this.lock) {
      if((entry = this.read.get(key)) != null) {
        // The entry was previously expunged, which implies this entry
        // is not within the dirty map.
        if(entry.tryUnexpunge()) {
          this.dirty.put(key, entry);
        }
        if((current = entry.get()) == null) current = entry.set(mappingFunction.apply(key));
      } else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
        if((current = entry.get()) == null) current = entry.set(mappingFunction.apply(key));
        this.missLocked();
      } else {
        if(!this.amended) {
          // Adds the first new key to the dirty map and marks it as
          // amended.
          this.dirtyLocked();
          this.amended = true;
        }
        this.dirty.put(key, new ExpungingValueImpl<>(current = mappingFunction.apply(key)));
      }
    }
    return current;
  }

  @Override
  @SuppressWarnings("ConstantConditions")
  public @Nullable V computeIfAbsent(final short key, final @NonNull Short2ObjectFunction mappingFunction) {
    requireNonNull(mappingFunction, "mappingFunction");
    ExpungingValue entry; V current;
    if((entry = this.read.get(key)) != null) {
      if((current = entry.get()) == null) current = entry.set(mappingFunction.get(key));
      return current;
    }
    synchronized(this.lock) {
      if((entry = this.read.get(key)) != null) {
        // The entry was previously expunged, which implies this entry
        // is not within the dirty map.
        if(entry.tryUnexpunge()) {
          this.dirty.put(key, entry);
        }
        if((current = entry.get()) == null) current = entry.set(mappingFunction.get(key));
      } else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
        if((current = entry.get()) == null) current = entry.set(mappingFunction.get(key));
        this.missLocked();
      } else {
        if(!this.amended) {
          // Adds the first new key to the dirty map and marks it as
          // amended.
          this.dirtyLocked();
          this.amended = true;
        }
        this.dirty.put(key, new ExpungingValueImpl<>(current = mappingFunction.get(key)));
      }
    }
    return current;
  }

  @Override
  public @Nullable V computeIfPresent(final short key, final @NonNull BiFunction remappingFunction) {
    requireNonNull(remappingFunction, "remappingFunction");
    ExpungingValue entry; V current;
    if((entry = this.read.get(key)) != null) {
      if((current = entry.get()) != null) entry.trySet(current = remappingFunction.apply(key, current));
      return current;
    }
    synchronized(this.lock) {
      if((entry = this.read.get(key)) != null) {
        if((current = entry.get()) != null) entry.trySet(current = remappingFunction.apply(key, current));
      } else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
        if((current = entry.get()) != null) entry.trySet(current = remappingFunction.apply(key, current));
        this.missLocked();
      } else {
        current = null;
      }
    }
    return current;
  }

  @Override
  @SuppressWarnings("ConstantConditions")
  public @Nullable V compute(final short key, final @NonNull BiFunction remappingFunction) {
    requireNonNull(remappingFunction, "remappingFunction");
    ExpungingValue entry; final V current;
    if((entry = this.read.get(key)) != null) {
      current = entry.set(remappingFunction.apply(key, entry.get()));
      return current;
    }
    synchronized(this.lock) {
      if((entry = this.read.get(key)) != null) {
        // The entry was previously expunged, which implies this entry
        // is not within the dirty map.
        if(entry.tryUnexpunge()) {
          this.dirty.put(key, entry);
        }
        current = entry.set(remappingFunction.apply(key, entry.get()));
      } else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
        current = entry.set(remappingFunction.apply(key, entry.get()));
        this.missLocked();
      } else {
        if(!this.amended) {
          // Adds the first new key to the dirty map and marks it as
          // amended.
          this.dirtyLocked();
          this.amended = true;
        }
        this.dirty.put(key, new ExpungingValueImpl<>(current = remappingFunction.apply(key, null)));
      }
    }
    return current;
  }

  @Override
  @SuppressWarnings("ConstantConditions")
  public @Nullable V putIfAbsent(final short key, final @NonNull V value) {
    requireNonNull(value, "value");
    ExpungingValue entry; Map.Entry result;
    if((entry = this.read.get(key)) != null) {
      if((result = entry.putIfAbsent(value)).getKey() == Boolean.TRUE) {
        return result.getValue();
      }
    }
    synchronized(this.lock) {
      if((entry = this.read.get(key)) != null) {
        // The entry was previously expunged, which implies this entry
        // is not within the dirty map.
        if(entry.tryUnexpunge()) {
          this.dirty.put(key, entry);
        }
        result = entry.putIfAbsent(value);
      } else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
        result = entry.putIfAbsent(value);
        this.missLocked();
      } else {
        if(!this.amended) {
          // Adds the first new key to the dirty map and marks it as
          // amended.
          this.dirtyLocked();
          this.amended = true;
        }
        this.dirty.put(key, new ExpungingValueImpl<>(value));
        result = new AbstractMap.SimpleImmutableEntry<>(Boolean.TRUE, null);
      }
    }
    return result.getValue();
  }

  @Override
  public @Nullable V put(final short key, final @NonNull V value) {
    return this.putValue(key, value, false);
  }

  @SuppressWarnings("ConstantConditions")
  private V putValue(final short key, final @NonNull V value, final boolean onlyIfPresent) {
    requireNonNull(value, "value");
    ExpungingValue entry = this.read.get(key);
    V previous = entry != null ? entry.get() : null;
    if(entry != null && entry.trySet(value)) return previous;
    synchronized(this.lock) {
      if((entry = this.read.get(key)) != null) {
        previous = entry.get();
        // If entry can be absent and previously expunged, add the
        // entry back to the dirty map.
        if(onlyIfPresent) {
          entry.trySet(value);
        } else if(entry.tryUnexpungeAndSet(value)) {
          this.dirty.put(key, entry);
        } else {
          entry.set(value);
        }
      } else if(this.dirty != null && (entry = this.dirty.get(key)) != null) {
        previous = entry.get();
        entry.set(value);
        this.missLocked();
      } else if(!onlyIfPresent) {
        if(!this.amended) {
          // Adds the first new key to the dirty map and marks it as
          // amended.
          this.dirtyLocked();
          this.amended = true;
        }
        this.dirty.put(key, new ExpungingValueImpl<>(value));
      }
    }
    return previous;
  }

  @Override
  public @Nullable V remove(final short key) {
    ExpungingValue entry;
    if((entry = this.read.get(key)) == null && this.amended) {
      synchronized(this.lock) {
        if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
          entry = this.dirty.remove(key);
          // The slow path should be avoided, even if the value does
          // not match or is present. So we mark a miss, to eventually
          // promote and take a faster path.
          this.missLocked();
        }
      }
    }
    return entry != null ? entry.clear() : null;
  }

  @Override
  @SuppressWarnings("AssignmentUsedAsCondition")
  public boolean remove(final short key, final @NonNull Object value) {
    requireNonNull(value, "value");
    ExpungingValue entry;
    if((entry = this.read.get(key)) == null && this.amended) {
      synchronized(this.lock) {
        if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
          final boolean present;
          if(present = (((entry = this.dirty.get(key))) != null && entry.replace(value, null))) {
            this.dirty.remove(key);
          }
          // The slow path should be avoided, even if the value does
          // not match or is present. So we mark a miss, to eventually
          // promote and take a faster path.
          this.missLocked();
          return present;
        }
      }
    }
    return entry != null && entry.replace(value, null);
  }

  @Override
  public @Nullable V replace(final short key, final @NonNull V value) {
    return this.putValue(key, value, true);
  }

  @Override
  public boolean replace(final short key, final @NonNull V currentValue, final @NonNull V newValue) {
    requireNonNull(currentValue, "currentValue");
    requireNonNull(newValue, "newValue");
    ExpungingValue entry;
    if((entry = this.read.get(key)) == null && this.amended) {
      synchronized(this.lock) {
        if((entry = this.read.get(key)) == null && this.amended && this.dirty != null) {
          final boolean present = ((entry = this.dirty.get(key)) != null && entry.replace(currentValue, newValue));
          // The slow path should be avoided, even if the value does
          // not match or is present. So we mark a miss, to eventually
          // promote and take a faster path.
          this.missLocked();
          return present;
        }
      }
    }
    return entry != null && entry.replace(currentValue, newValue);
  }

  // Bulk Operations

  @Override
  public void forEach(final @NonNull BiConsumer action) {
    requireNonNull(action, "action");
    this.promoteIfNeeded();
    V value;
    for(final Short2ObjectMap.Entry> that : this.read.short2ObjectEntrySet()) {
      if((value = that.getValue().get()) != null) {
        action.accept(that.getShortKey(), value);
      }
    }
  }

  @Override
  public void putAll(final @NonNull Map map) {
    requireNonNull(map, "map");
    for(final Map.Entry entry : map.entrySet()) {
      this.putValue(entry.getKey(), entry.getValue(), false);
    }
  }

  @Override
  public void replaceAll(final @NonNull BiFunction function) {
    requireNonNull(function, "function");
    this.promoteIfNeeded();
    ExpungingValue entry; V value;
    for(final Short2ObjectMap.Entry> that : this.read.short2ObjectEntrySet()) {
      if((value = (entry = that.getValue()).get()) != null) {
        entry.trySet(function.apply(that.getShortKey(), value));
      }
    }
  }

  @Override
  public void clear() {
    synchronized(this.lock) {
      this.read = this.function.apply(this.read.size());
      this.amended = false;
      this.dirty = null;
      this.misses = 0;
    }
  }

  // Views

  @Override
  public @NonNull ObjectSet> short2ObjectEntrySet() {
    if(this.entrySet != null) return this.entrySet;
    return this.entrySet = new EntrySetView();
  }

  private void promoteIfNeeded() {
    if(this.amended) {
      synchronized(this.lock) {
        if(this.amended) {
          this.promoteLocked();
        }
      }
    }
  }

  private void promoteLocked() {
    if(this.dirty != null) {
      this.read = this.dirty;
    }
    this.amended = false;
    this.dirty = null;
    this.misses = 0;
  }

  private void missLocked() {
    if(this.misses++ >= (this.dirty != null ? (int) (this.dirty.size() * this.promotionFactor) : 0)) {
      this.promoteLocked();
    }
  }

  private void dirtyLocked() {
    if(this.dirty != null) return;
    this.dirty = this.function.apply(this.read.size());
    Short2ObjectMaps.fastForEach(this.read, (entry) -> {
      if(!entry.getValue().tryMarkExpunged()) {
        this.dirty.put(entry.getShortKey(), entry.getValue());
      }
    });
  }

  @SuppressWarnings({"unchecked", "rawtypes"})
  private final static class ExpungingValueImpl implements Short2ObjectSyncMap.ExpungingValue {
    private static final AtomicReferenceFieldUpdater VALUE_UPDATER = AtomicReferenceFieldUpdater
      .newUpdater(ExpungingValueImpl.class, Object.class, "value");
    private static final Object EXPUNGED = new Object();
    private volatile Object value;

    private ExpungingValueImpl(final @NonNull V value) {
      this.value = value;
    }

    @Override
    public @Nullable V get() {
      final Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
      return value == ExpungingValueImpl.EXPUNGED ? null : (V) value;
    }

    @Override
    public Map.@NonNull Entry putIfAbsent(final @NonNull V value) {
      for(; ; ) {
        final Object previous = ExpungingValueImpl.VALUE_UPDATER.get(this);
        if(previous == ExpungingValueImpl.EXPUNGED) {
          return new AbstractMap.SimpleImmutableEntry<>(Boolean.FALSE, null);
        }
        if(previous != null) {
          return new AbstractMap.SimpleImmutableEntry<>(Boolean.TRUE, (V) previous);
        }
        if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, null, value)) {
          return new AbstractMap.SimpleImmutableEntry<>(Boolean.TRUE, null);
        }
      }
    }

    @Override
    public boolean exists() {
      final Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
      return value != null && value != ExpungingValueImpl.EXPUNGED;
    }

    @Override
    public @NonNull V set(final @NonNull V value) {
      ExpungingValueImpl.VALUE_UPDATER.set(this, value);
      return value;
    }

    @Override
    public boolean replace(final @NonNull Object compare, final @Nullable V newValue) {
      for(; ; ) {
        final Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
        if(value == ExpungingValueImpl.EXPUNGED || !Objects.equals(value, compare)) return false;
        if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, value, newValue)) return true;
      }
    }

    @Override
    public @Nullable V clear() {
      for(; ; ) {
        final Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
        if(value == null || value == ExpungingValueImpl.EXPUNGED) return null;
        if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, value, null)) return (V) value;
      }
    }

    @Override
    public boolean trySet(final @NonNull V value) {
      for(; ; ) {
        final Object present = ExpungingValueImpl.VALUE_UPDATER.get(this);
        if(present == ExpungingValueImpl.EXPUNGED) return false;
        if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, present, value)) return true;
      }
    }

    @Override
    public boolean tryMarkExpunged() {
      Object value = ExpungingValueImpl.VALUE_UPDATER.get(this);
      while(value == null) {
        if(ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, null, ExpungingValueImpl.EXPUNGED)) return true;
        value = ExpungingValueImpl.VALUE_UPDATER.get(this);
      }
      return false;
    }

    @Override
    public boolean tryUnexpungeAndSet(final @Nullable V value) {
      return ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, ExpungingValueImpl.EXPUNGED, value);
    }

    @Override
    public boolean tryUnexpunge() {
      return ExpungingValueImpl.VALUE_UPDATER.compareAndSet(this, ExpungingValueImpl.EXPUNGED, null);
    }

    @Override
    public String toString() {
      return "Short2ObjectSyncMapImpl.ExpungingValue{value=" + this.get() + "}";
    }
  }

  private final class MapEntry implements Short2ObjectMap.Entry {
    private final short key;
    private V value;

    private MapEntry(final short key, final @NonNull V value) {
      this.key = key;
      this.value = value;
    }

    @Override
    public short getShortKey() {
      return this.key;
    }

    @Override
    public @NonNull V getValue() {
      return this.value;
    }

    @Override
    public @Nullable V setValue(final @NonNull V value) {
      requireNonNull(value, "value");
      return Short2ObjectSyncMapImpl.this.put(this.key, this.value = value);
    }

    @Override
    public @NonNull String toString() {
      return "Short2ObjectSyncMapImpl.MapEntry{key=" + this.getShortKey() + ", value=" + this.getValue() + "}";
    }

    @Override
    public boolean equals(final @Nullable Object other) {
      if(this == other) return true;
      if(!(other instanceof Short2ObjectMap.Entry)) return false;
      final Short2ObjectMap.Entry that = (Short2ObjectMap.Entry) other;
      return Objects.equals(this.getShortKey(), that.getShortKey())
        && Objects.equals(this.getValue(), that.getValue());
    }

    @Override
    public int hashCode() {
      return Objects.hash(this.getShortKey(), this.getValue());
    }
  }

  private final class EntrySetView extends AbstractObjectSet> {
    @Override
    public int size() {
      return Short2ObjectSyncMapImpl.this.size();
    }

    @Override
    public boolean contains(final @Nullable Object entry) {
      if(!(entry instanceof Short2ObjectMap.Entry)) return false;
      final Short2ObjectMap.Entry mapEntry = (Short2ObjectMap.Entry) entry;
      final V value = Short2ObjectSyncMapImpl.this.get(mapEntry.getShortKey());
      return value != null && Objects.equals(value, mapEntry.getValue());
    }

    @Override
    public boolean remove(final @Nullable Object entry) {
      if(!(entry instanceof Short2ObjectMap.Entry)) return false;
      final Short2ObjectMap.Entry mapEntry = (Short2ObjectMap.Entry) entry;
      return Short2ObjectSyncMapImpl.this.remove(mapEntry.getShortKey()) != null;
    }

    @Override
    public void clear() {
      Short2ObjectSyncMapImpl.this.clear();
    }

    @Override
    public @NonNull ObjectIterator> iterator() {
      Short2ObjectSyncMapImpl.this.promoteIfNeeded();
      return new EntryIterator(Short2ObjectSyncMapImpl.this.read.short2ObjectEntrySet().iterator());
    }
  }

  private final class EntryIterator implements ObjectIterator> {
    private final Iterator>> backingIterator;
    private Short2ObjectMap.Entry next;
    private Short2ObjectMap.Entry current;

    private EntryIterator(final @NonNull Iterator>> backingIterator) {
      this.backingIterator = backingIterator;
      this.next = this.nextValue();
    }

    @Override
    public boolean hasNext() {
      return this.next != null;
    }

    @Override
    public Short2ObjectMap.@NonNull Entry next() {
      if((this.current = this.next) == null) throw new NoSuchElementException();
      this.next = this.nextValue();
      return this.current;
    }

    private Short2ObjectMap.@Nullable Entry nextValue() {
      Short2ObjectMap.Entry> entry;
      V value;
      while(this.backingIterator.hasNext()) {
        if((value = (entry = this.backingIterator.next()).getValue().get()) != null) {
          return new MapEntry(entry.getShortKey(), value);
        }
      }
      return null;
    }

    @Override
    public void remove() {
      if(this.current == null) throw new IllegalStateException();
      Short2ObjectSyncMapImpl.this.remove(this.current.getShortKey());
      this.current = null;
    }

    @Override
    public void forEachRemaining(final @NonNull Consumer> action) {
      requireNonNull(action, "action");
      if(this.next != null) action.accept(this.next);
      this.backingIterator.forEachRemaining(entry -> {
        final V value = entry.getValue().get();
        if(value != null) action.accept(new MapEntry(entry.getShortKey(), value));
      });
    }
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy