org.opendaylight.yangtools.util.MutableOffsetMap Maven / Gradle / Ivy
/*
* Copyright (c) 2015 Cisco Systems, Inc. and others. All rights reserved.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License v1.0 which accompanies this distribution,
* and is available at http://www.eclipse.org/legal/epl-v10.html
*/
package org.opendaylight.yangtools.util;
import static com.google.common.base.Preconditions.checkState;
import static com.google.common.base.Verify.verifyNotNull;
import static java.util.Objects.requireNonNull;
import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableMap;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
import org.eclipse.jdt.annotation.NonNull;
import org.eclipse.jdt.annotation.Nullable;
/**
* A mutable version of {@link ImmutableOffsetMap}. It inherits the set of mappings from the immutable version and
* allows updating/removing existing mappings. New mappings are stored in a dedicated {@link LinkedHashMap} to preserve
* insertion order. It also tracks the need to duplicate the backing array, so the sequence of
*
* ImmutableOffsetMap<K, V> source;
* ImmutableOffsetMap<K, V> result = source.createMutableClone().immutableCopy();
*
* results in source and result sharing the backing objects.
*
* This map does not support null keys nor values.
*
* @param the type of keys maintained by this map
* @param the type of mapped values
*/
public abstract class MutableOffsetMap extends AbstractMap implements Cloneable, ModifiableMapPhase {
static final class Ordered extends MutableOffsetMap {
Ordered() {
}
Ordered(final Map source) {
super(OffsetMapCache.orderedOffsets(source.keySet()), source);
}
Ordered(final ImmutableMap offsets, final V[] objects) {
super(offsets, objects);
}
@Override
Object removedObject() {
return REMOVED;
}
@Override
UnmodifiableMapPhase modifiedMap(final List keys, final V[] values) {
return new ImmutableOffsetMap.Ordered<>(OffsetMapCache.orderedOffsets(keys), values);
}
@Override
UnmodifiableMapPhase unmodifiedMap(final ImmutableMap offsetMap, final V[] values) {
return new ImmutableOffsetMap.Ordered<>(offsetMap, values);
}
@Override
SharedSingletonMap singletonMap() {
return SharedSingletonMap.orderedCopyOf(this);
}
@Override
HashMap createNewKeys() {
return new LinkedHashMap<>();
}
}
static final class Unordered extends MutableOffsetMap {
Unordered() {
}
Unordered(final Map source) {
super(OffsetMapCache.unorderedOffsets(source.keySet()), source);
}
Unordered(final ImmutableMap offsets, final V[] objects) {
super(offsets, objects);
}
@Override
Object removedObject() {
return null;
}
@Override
UnmodifiableMapPhase modifiedMap(final List keys, final V[] values) {
final ImmutableMap offsets = OffsetMapCache.unorderedOffsets(keys);
return new ImmutableOffsetMap.Unordered<>(offsets, OffsetMapCache.adjustedArray(offsets, keys, values));
}
@Override
UnmodifiableMapPhase unmodifiedMap(final ImmutableMap offsetMap, final V[] values) {
return new ImmutableOffsetMap.Unordered<>(offsetMap, values);
}
@Override
SharedSingletonMap singletonMap() {
return SharedSingletonMap.unorderedCopyOf(this);
}
@Override
HashMap createNewKeys() {
return new HashMap<>();
}
}
private static final Object[] EMPTY_ARRAY = new Object[0];
private static final Object REMOVED = new Object();
private final ImmutableMap offsets;
private HashMap newKeys;
private Object[] objects;
private int removed = 0;
// Fail-fast iterator guard, see java.util.ArrayList for reference.
@SuppressFBWarnings("VO_VOLATILE_INCREMENT")
private transient volatile int modCount;
private boolean needClone = true;
MutableOffsetMap(final ImmutableMap offsets, final Object[] objects) {
this.offsets = requireNonNull(offsets);
this.objects = requireNonNull(objects);
}
MutableOffsetMap() {
this(ImmutableMap.of(), EMPTY_ARRAY);
}
MutableOffsetMap(final ImmutableMap offsets, final Map source) {
this(offsets, new Object[offsets.size()]);
for (Entry e : source.entrySet()) {
objects[verifyNotNull(offsets.get(e.getKey()))] = requireNonNull(e.getValue());
}
needClone = false;
}
/**
* Create a {@link MutableOffsetMap} of the specified map, retaining its iteration order.
*
* @param map input map
* @return MutableOffsetMap with the same iteration order
* @throws NullPointerException if {@code map} is null
*/
public static @NonNull MutableOffsetMap orderedCopyOf(final Map map) {
if (map instanceof Ordered ordered) {
return ordered.clone();
} else if (map instanceof ImmutableOffsetMap om) {
return new Ordered<>(om.offsets(), om.objects());
} else {
return new Ordered<>(map);
}
}
/**
* Create a {@link MutableOffsetMap} of the specified map, potentially with a different iteration order.
*
* @param map input map
* @return MutableOffsetMap with undefined iteration order
* @throws NullPointerException if {@code map} is null
*/
public static @NonNull MutableOffsetMap unorderedCopyOf(final Map map) {
if (map instanceof Unordered unordered) {
return unordered.clone();
} else if (map instanceof ImmutableOffsetMap om) {
return new Unordered<>(om.offsets(), om.objects());
} else {
return new Unordered<>(map);
}
}
/**
* Create an empty {@link MutableOffsetMap} which has an iteration order matching the insertion order.
*
* @return MutableOffsetMap which preserves insertion order
*/
public static @NonNull MutableOffsetMap ordered() {
return new MutableOffsetMap.Ordered<>();
}
/**
* Create an empty {@link MutableOffsetMap} which has unspecified iteration order.
*
* @return An MutableOffsetMap
*/
public static @NonNull MutableOffsetMap unordered() {
return new MutableOffsetMap.Unordered<>();
}
abstract Object removedObject();
abstract UnmodifiableMapPhase modifiedMap(List keys, V[] values);
abstract UnmodifiableMapPhase unmodifiedMap(ImmutableMap offsetMap, V[] values);
abstract SharedSingletonMap singletonMap();
@Override
public final int size() {
return offsets.size() - removed + (newKeys == null ? 0 : newKeys.size());
}
@Override
public final boolean isEmpty() {
return size() == 0;
}
@Override
public final boolean containsKey(final Object key) {
final Integer offset = offsets.get(key);
if (offset != null) {
final Object obj = objects[offset];
if (!REMOVED.equals(obj)) {
return obj != null;
}
}
return newKeys != null && newKeys.containsKey(key);
}
@Override
public final V get(final Object key) {
final Integer offset = offsets.get(key);
if (offset != null) {
final Object obj = objects[offset];
/*
* This is a bit tricky: Ordered will put REMOVED to removed objects to retain strict insertion order.
* Unordered will add null, indicating that the slot may be reused in future. Hence if we see a REMOVED
* marker, we need to fall back to checking with new keys.
*/
if (!REMOVED.equals(obj)) {
@SuppressWarnings("unchecked")
final V ret = (V)obj;
return ret;
}
}
return newKeys == null ? null : newKeys.get(key);
}
private void cloneArray() {
if (needClone) {
needClone = false;
if (objects.length != 0) {
objects = objects.clone();
}
}
}
@Override
public final V put(final K key, final V value) {
requireNonNull(value);
final Integer offset = offsets.get(requireNonNull(key));
if (offset != null) {
final Object obj = objects[offset];
/*
* Put which can potentially replace something in objects. Replacing an object does not cause iterators
* to be invalidated and does follow insertion order (since it is not a fresh insert). If the object has
* been removed, we fall back to newKeys.
*/
if (!REMOVED.equals(obj)) {
@SuppressWarnings("unchecked")
final V ret = (V)obj;
cloneArray();
objects[offset] = value;
if (ret == null) {
modCount++;
removed--;
}
return ret;
}
}
if (newKeys == null) {
newKeys = createNewKeys();
}
final V ret = newKeys.put(key, value);
if (ret == null) {
modCount++;
}
return ret;
}
@Override
public final V remove(final Object key) {
final Integer offset = offsets.get(key);
if (offset != null) {
final Object obj = objects[offset];
/*
* A previous remove() may have indicated that the objects slot cannot be reused. In that case we need
* to fall back to checking with newKeys.
*/
if (!REMOVED.equals(obj)) {
cloneArray();
@SuppressWarnings("unchecked")
final V ret = (V)obj;
objects[offset] = removedObject();
if (ret != null) {
modCount++;
removed++;
}
return ret;
}
}
if (newKeys == null) {
return null;
}
final V ret = newKeys.remove(key);
if (ret != null) {
modCount++;
}
return ret;
}
@Override
public final void clear() {
if (size() != 0) {
if (newKeys != null) {
newKeys.clear();
}
cloneArray();
Arrays.fill(objects, removedObject());
removed = objects.length;
modCount++;
}
}
@Override
public final @NonNull Set> entrySet() {
return new EntrySet();
}
@Override
public @NonNull Map toUnmodifiableMap() {
if (removed == 0 && noNewKeys()) {
// Make sure next modification clones the array, as we leak it to the map we return.
needClone = true;
// We have ended up with no removed objects, hence this cast is safe
@SuppressWarnings("unchecked")
final V[] values = (V[])objects;
/*
* TODO: we could track the ImmutableOffsetMap from which this one was instantiated and if we do not
* perform any modifications, just return the original instance. The trade-off is increased complexity
* and an additional field in this class.
*/
return unmodifiedMap(offsets, values);
}
final int s = size();
if (s == 0) {
return ImmutableMap.of();
}
if (s == 1) {
return singletonMap();
}
// Construct the set of keys
final List keyset = new ArrayList<>(s);
if (removed != 0) {
if (removed != offsets.size()) {
for (Entry e : offsets.entrySet()) {
final Object o = objects[e.getValue()];
if (o != null && !REMOVED.equals(o)) {
keyset.add(e.getKey());
}
}
}
} else {
keyset.addAll(offsets.keySet());
}
if (newKeys != null) {
keyset.addAll(newKeys.keySet());
}
// Construct the values
@SuppressWarnings("unchecked")
final V[] values = (V[])new Object[keyset.size()];
int offset = 0;
if (removed != 0) {
if (removed != offsets.size()) {
for (Entry e : offsets.entrySet()) {
final Object o = objects[e.getValue()];
if (o != null && !REMOVED.equals(o)) {
@SuppressWarnings("unchecked")
final V v = (V) o;
values[offset++] = v;
}
}
}
} else {
System.arraycopy(objects, 0, values, 0, offsets.size());
offset = offsets.size();
}
if (newKeys != null) {
for (V v : newKeys.values()) {
values[offset++] = v;
}
}
return modifiedMap(keyset, values);
}
@SuppressWarnings("unchecked")
@Override
public @NonNull MutableOffsetMap clone() {
final MutableOffsetMap ret;
try {
ret = (MutableOffsetMap) super.clone();
} catch (CloneNotSupportedException e) {
throw new IllegalStateException("Clone is expected to work", e);
}
ret.newKeys = newKeys == null ? null : (HashMap) newKeys.clone();
ret.needClone = true;
return ret;
}
@Override
public final int hashCode() {
int result = 0;
for (Entry e : offsets.entrySet()) {
final Object v = objects[e.getValue()];
if (v != null) {
result += e.getKey().hashCode() ^ v.hashCode();
}
}
return newKeys != null ? result + newKeys.hashCode() : result;
}
@Override
public final boolean equals(final Object obj) {
if (obj == this) {
return true;
}
if (!(obj instanceof Map)) {
return false;
}
if (obj instanceof ImmutableOffsetMap, ?> om) {
if (noNewKeys() && offsets.equals(om.offsets())) {
return Arrays.deepEquals(objects, om.objects());
}
} else if (obj instanceof MutableOffsetMap, ?> om && offsets.equals(om.offsets)) {
return Arrays.deepEquals(objects, om.objects) && equalNewKeys(om);
}
// Fall back to brute map compare
return mapEquals((Map, ?>)obj);
}
private boolean equalNewKeys(final MutableOffsetMap, ?> other) {
return noNewKeys() ? other.noNewKeys() : newKeys.equals(other.newKeys());
}
private boolean mapEquals(final Map, ?> other) {
// Size and key sets have to match
if (size() != other.size() || !keySet().equals(other.keySet())) {
return false;
}
try {
if (newKeys != null) {
// Ensure all newKeys are present. Note newKeys is guaranteed to not contain a null value.
for (Entry e : newKeys.entrySet()) {
if (!e.getValue().equals(other.get(e.getKey()))) {
return false;
}
}
}
// Ensure all objects are present
for (Entry e : offsets.entrySet()) {
final Object val = objects[e.getValue()];
if (val != null && !REMOVED.equals(val) && !val.equals(other.get(e.getKey()))) {
return false;
}
}
} catch (ClassCastException e) {
// Can be thrown by other.get() and indicate we have incompatible key types
return false;
}
return true;
}
@Override
public final @NonNull Set keySet() {
return new KeySet();
}
@VisibleForTesting
final boolean needClone() {
return needClone;
}
@VisibleForTesting
final Object array() {
return objects;
}
@VisibleForTesting
final Object newKeys() {
return newKeys != null ? newKeys : ImmutableMap.of();
}
abstract HashMap createNewKeys();
private boolean noNewKeys() {
return newKeys == null || newKeys.isEmpty();
}
private final class EntrySet extends AbstractSet> {
@Override
public @NonNull Iterator> iterator() {
return new AbstractSetIterator<>() {
@Override
public Entry next() {
final K key = nextKey();
return new SimpleEntry<>(key, get(key));
}
};
}
@Override
public int size() {
return MutableOffsetMap.this.size();
}
@Override
@SuppressWarnings("checkstyle:parameterName")
public boolean contains(final Object o) {
if (!(o instanceof Entry)) {
return false;
}
@SuppressWarnings("unchecked")
final Entry e = (Entry) o;
if (e.getValue() == null) {
return false;
}
return e.getValue().equals(MutableOffsetMap.this.get(e.getKey()));
}
@Override
@SuppressWarnings("checkstyle:parameterName")
public boolean add(final Entry e) {
final V v = requireNonNull(e.getValue());
final V p = MutableOffsetMap.this.put(e.getKey(), v);
return !v.equals(p);
}
@Override
@SuppressWarnings("checkstyle:parameterName")
public boolean remove(final Object o) {
if (!(o instanceof Entry)) {
return false;
}
@SuppressWarnings("unchecked")
final Entry e = (Entry) o;
if (e.getValue() == null) {
return false;
}
final V v = MutableOffsetMap.this.get(e.getKey());
if (e.getValue().equals(v)) {
MutableOffsetMap.this.remove(e.getKey());
return true;
}
return false;
}
@Override
public void clear() {
MutableOffsetMap.this.clear();
}
}
private final class KeySet extends AbstractSet {
@Override
public @NonNull Iterator iterator() {
return new AbstractSetIterator<>() {
@Override
public K next() {
return nextKey();
}
};
}
@Override
public int size() {
return MutableOffsetMap.this.size();
}
}
private abstract class AbstractSetIterator implements Iterator {
private final Iterator> oldIterator = offsets.entrySet().iterator();
private final Iterator newIterator = newKeys == null ? Collections.emptyIterator()
: newKeys.keySet().iterator();
private int expectedModCount = modCount;
private @Nullable K currentKey = null;
private @Nullable K nextKey;
AbstractSetIterator() {
updateNextKey();
}
private void updateNextKey() {
while (oldIterator.hasNext()) {
final Entry e = oldIterator.next();
final Object obj = objects[e.getValue()];
if (obj != null && !REMOVED.equals(obj)) {
nextKey = e.getKey();
return;
}
}
nextKey = newIterator.hasNext() ? newIterator.next() : null;
}
private void checkModCount() {
if (modCount != expectedModCount) {
throw new ConcurrentModificationException();
}
}
@Override
public final boolean hasNext() {
checkModCount();
return nextKey != null;
}
@Override
public final void remove() {
checkModCount();
checkState(currentKey != null);
final Integer offset = offsets.get(currentKey);
if (offset != null) {
cloneArray();
objects[offset] = removedObject();
removed++;
} else {
newIterator.remove();
}
expectedModCount = ++modCount;
currentKey = null;
}
protected final K nextKey() {
if (nextKey == null) {
throw new NoSuchElementException();
}
checkModCount();
currentKey = nextKey;
updateNextKey();
return currentKey;
}
}
}