blob: 11b633ebf97f4eea3c893d160f897784f0f62b95 [file] [log] [blame]
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.apache.commons.collections4.multimap;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.util.AbstractCollection;
import java.util.AbstractMap;
import java.util.AbstractSet;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.Map;
import java.util.Objects;
import java.util.Map.Entry;
import java.util.Set;
import org.apache.commons.collections4.CollectionUtils;
import org.apache.commons.collections4.IteratorUtils;
import org.apache.commons.collections4.MapIterator;
import org.apache.commons.collections4.MultiSet;
import org.apache.commons.collections4.MultiValuedMap;
import org.apache.commons.collections4.Transformer;
import org.apache.commons.collections4.iterators.AbstractIteratorDecorator;
import org.apache.commons.collections4.iterators.EmptyMapIterator;
import org.apache.commons.collections4.iterators.IteratorChain;
import org.apache.commons.collections4.iterators.LazyIteratorChain;
import org.apache.commons.collections4.iterators.TransformIterator;
import org.apache.commons.collections4.keyvalue.AbstractMapEntry;
import org.apache.commons.collections4.keyvalue.UnmodifiableMapEntry;
import org.apache.commons.collections4.multiset.AbstractMultiSet;
import org.apache.commons.collections4.multiset.UnmodifiableMultiSet;
/**
* Abstract implementation of the {@link MultiValuedMap} interface to simplify
* the creation of subclass implementations.
* <p>
* Subclasses specify a Map implementation to use as the internal storage.
* </p>
*
* @param <K> the type of the keys in this map
* @param <V> the type of the values in this map
* @since 4.1
*/
public abstract class AbstractMultiValuedMap<K, V> implements MultiValuedMap<K, V> {
/** The values view */
private transient Collection<V> valuesView;
/** The EntryValues view */
private transient EntryValues entryValuesView;
/** The KeyMultiSet view */
private transient MultiSet<K> keysMultiSetView;
/** The AsMap view */
private transient AsMap asMapView;
/** The map used to store the data */
private transient Map<K, Collection<V>> map;
/**
* Constructor needed for subclass serialisation.
*/
protected AbstractMultiValuedMap() {
super();
}
/**
* Constructor that wraps (not copies).
*
* @param map the map to wrap, must not be null
* @throws NullPointerException if the map is null
*/
@SuppressWarnings("unchecked")
protected AbstractMultiValuedMap(final Map<K, ? extends Collection<V>> map) {
this.map = (Map<K, Collection<V>>) Objects.requireNonNull(map, "map");
}
// -----------------------------------------------------------------------
/**
* Gets the map being wrapped.
*
* @return the wrapped map
*/
protected Map<K, ? extends Collection<V>> getMap() {
return map;
}
/**
* Sets the map being wrapped.
* <p>
* <b>NOTE:</b> this method should only be used during deserialization
*
* @param map the map to wrap
*/
@SuppressWarnings("unchecked")
protected void setMap(final Map<K, ? extends Collection<V>> map) {
this.map = (Map<K, Collection<V>>) map;
}
protected abstract Collection<V> createCollection();
// -----------------------------------------------------------------------
@Override
public boolean containsKey(final Object key) {
return getMap().containsKey(key);
}
@Override
public boolean containsValue(final Object value) {
return values().contains(value);
}
@Override
public boolean containsMapping(final Object key, final Object value) {
final Collection<V> coll = getMap().get(key);
return coll != null && coll.contains(value);
}
@Override
public Collection<Entry<K, V>> entries() {
return entryValuesView != null ? entryValuesView : (entryValuesView = new EntryValues());
}
/**
* Gets the collection of values associated with the specified key. This
* would return an empty collection in case the mapping is not present
*
* @param key the key to retrieve
* @return the {@code Collection} of values, will return an empty {@code Collection} for no mapping
*/
@Override
public Collection<V> get(final K key) {
return wrappedCollection(key);
}
Collection<V> wrappedCollection(final K key) {
return new WrappedCollection(key);
}
/**
* Removes all values associated with the specified key.
* <p>
* A subsequent {@code get(Object)} would return an empty collection.
*
* @param key the key to remove values from
* @return the {@code Collection} of values removed, will return an
* empty, unmodifiable collection for no mapping found
*/
@Override
public Collection<V> remove(final Object key) {
return CollectionUtils.emptyIfNull(getMap().remove(key));
}
/**
* Removes a specific key/value mapping from the multi-valued map.
* <p>
* The value is removed from the collection mapped to the specified key.
* Other values attached to that key are unaffected.
* <p>
* If the last value for a key is removed, an empty collection would be
* returned from a subsequent {@link #get(Object)}.
*
* @param key the key to remove from
* @param value the value to remove
* @return true if the mapping was removed, false otherwise
*/
@Override
public boolean removeMapping(final Object key, final Object value) {
final Collection<V> coll = getMap().get(key);
if (coll == null) {
return false;
}
final boolean changed = coll.remove(value);
if (coll.isEmpty()) {
getMap().remove(key);
}
return changed;
}
@Override
public boolean isEmpty() {
return getMap().isEmpty();
}
@Override
public Set<K> keySet() {
return getMap().keySet();
}
/**
* {@inheritDoc}
* <p>
* This implementation does <b>not</b> cache the total size
* of the multi-valued map, but rather calculates it by iterating
* over the entries of the underlying map.
*/
@Override
public int size() {
// the total size should be cached to improve performance
// but this requires that all modifications of the multimap
// (including the wrapped collections and entry/value
// collections) are tracked.
int size = 0;
for (final Collection<V> col : getMap().values()) {
size += col.size();
}
return size;
}
/**
* Gets a collection containing all the values in the map.
* <p>
* Returns a collection containing all the values from all keys.
*
* @return a collection view of the values contained in this map
*/
@Override
public Collection<V> values() {
final Collection<V> vs = valuesView;
return vs != null ? vs : (valuesView = new Values());
}
@Override
public void clear() {
getMap().clear();
}
/**
* Adds the value to the collection associated with the specified key.
* <p>
* Unlike a normal {@code Map} the previous value is not replaced.
* Instead the new value is added to the collection stored against the key.
*
* @param key the key to store against
* @param value the value to add to the collection at the key
* @return the value added if the map changed and null if the map did not change
*/
@Override
public boolean put(final K key, final V value) {
Collection<V> coll = getMap().get(key);
if (coll == null) {
coll = createCollection();
if (coll.add(value)) {
map.put(key, coll);
return true;
}
return false;
}
return coll.add(value);
}
/**
* Copies all of the mappings from the specified map to this map. The effect
* of this call is equivalent to that of calling {@link #put(Object,Object)
* put(k, v)} on this map once for each mapping from key {@code k} to value
* {@code v} in the specified map. The behavior of this operation is
* undefined if the specified map is modified while the operation is in
* progress.
*
* @param map mappings to be stored in this map, may not be null
* @return true if the map changed as a result of this operation
* @throws NullPointerException if map is null
*/
@Override
public boolean putAll(final Map<? extends K, ? extends V> map) {
Objects.requireNonNull(map, "map");
boolean changed = false;
for (final Map.Entry<? extends K, ? extends V> entry : map.entrySet()) {
changed |= put(entry.getKey(), entry.getValue());
}
return changed;
}
/**
* Copies all of the mappings from the specified MultiValuedMap to this map.
* The effect of this call is equivalent to that of calling
* {@link #put(Object,Object) put(k, v)} on this map once for each mapping
* from key {@code k} to value {@code v} in the specified map. The
* behavior of this operation is undefined if the specified map is modified
* while the operation is in progress.
*
* @param map mappings to be stored in this map, may not be null
* @return true if the map changed as a result of this operation
* @throws NullPointerException if map is null
*/
@Override
public boolean putAll(final MultiValuedMap<? extends K, ? extends V> map) {
Objects.requireNonNull(map, "map");
boolean changed = false;
for (final Map.Entry<? extends K, ? extends V> entry : map.entries()) {
changed |= put(entry.getKey(), entry.getValue());
}
return changed;
}
/**
* Returns a {@link MultiSet} view of the key mapping contained in this map.
* <p>
* Returns a MultiSet of keys with its values count as the count of the MultiSet.
* This multiset is backed by the map, so any changes in the map is reflected here.
* Any method which modifies this multiset like {@code add}, {@code remove},
* {@link Iterator#remove()} etc throws {@code UnsupportedOperationException}.
*
* @return a bag view of the key mapping contained in this map
*/
@Override
public MultiSet<K> keys() {
if (keysMultiSetView == null) {
keysMultiSetView = UnmodifiableMultiSet.unmodifiableMultiSet(new KeysMultiSet());
}
return keysMultiSetView;
}
@Override
public Map<K, Collection<V>> asMap() {
return asMapView != null ? asMapView : (asMapView = new AsMap(map));
}
/**
* Adds Iterable values to the collection associated with the specified key.
*
* @param key the key to store against
* @param values the values to add to the collection at the key, may not be null
* @return true if this map changed
* @throws NullPointerException if values is null
*/
@Override
public boolean putAll(final K key, final Iterable<? extends V> values) {
Objects.requireNonNull(values, "values");
if (values instanceof Collection<?>) {
final Collection<? extends V> valueCollection = (Collection<? extends V>) values;
return !valueCollection.isEmpty() && get(key).addAll(valueCollection);
}
final Iterator<? extends V> it = values.iterator();
return it.hasNext() && CollectionUtils.addAll(get(key), it);
}
@Override
public MapIterator<K, V> mapIterator() {
if (size() == 0) {
return EmptyMapIterator.emptyMapIterator();
}
return new MultiValuedMapIterator();
}
@Override
public boolean equals(final Object obj) {
if (this == obj) {
return true;
}
if (obj instanceof MultiValuedMap) {
return asMap().equals(((MultiValuedMap<?, ?>) obj).asMap());
}
return false;
}
@Override
public int hashCode() {
return getMap().hashCode();
}
@Override
public String toString() {
return getMap().toString();
}
// -----------------------------------------------------------------------
/**
* Wrapped collection to handle add and remove on the collection returned
* by get(object).
* <p>
* Currently, the wrapped collection is not cached and has to be retrieved
* from the underlying map. This is safe, but not very efficient and
* should be improved in subsequent releases. For this purpose, the
* scope of this collection is set to package private to simplify later
* refactoring.
*/
class WrappedCollection implements Collection<V> {
protected final K key;
WrappedCollection(final K key) {
this.key = key;
}
protected Collection<V> getMapping() {
return getMap().get(key);
}
@Override
public boolean add(final V value) {
Collection<V> coll = getMapping();
if (coll == null) {
coll = createCollection();
AbstractMultiValuedMap.this.map.put(key, coll);
}
return coll.add(value);
}
@Override
public boolean addAll(final Collection<? extends V> other) {
Collection<V> coll = getMapping();
if (coll == null) {
coll = createCollection();
AbstractMultiValuedMap.this.map.put(key, coll);
}
return coll.addAll(other);
}
@Override
public void clear() {
final Collection<V> coll = getMapping();
if (coll != null) {
coll.clear();
AbstractMultiValuedMap.this.remove(key);
}
}
@Override
public Iterator<V> iterator() {
final Collection<V> coll = getMapping();
if (coll == null) {
return IteratorUtils.EMPTY_ITERATOR;
}
return new ValuesIterator(key);
}
@Override
public int size() {
final Collection<V> coll = getMapping();
return coll == null ? 0 : coll.size();
}
@Override
public boolean contains(final Object obj) {
final Collection<V> coll = getMapping();
return coll != null && coll.contains(obj);
}
@Override
public boolean containsAll(final Collection<?> other) {
final Collection<V> coll = getMapping();
return coll != null && coll.containsAll(other);
}
@Override
public boolean isEmpty() {
final Collection<V> coll = getMapping();
return coll == null || coll.isEmpty();
}
@Override
public boolean remove(final Object item) {
final Collection<V> coll = getMapping();
if (coll == null) {
return false;
}
final boolean result = coll.remove(item);
if (coll.isEmpty()) {
AbstractMultiValuedMap.this.remove(key);
}
return result;
}
@Override
public boolean removeAll(final Collection<?> c) {
final Collection<V> coll = getMapping();
if (coll == null) {
return false;
}
final boolean result = coll.removeAll(c);
if (coll.isEmpty()) {
AbstractMultiValuedMap.this.remove(key);
}
return result;
}
@Override
public boolean retainAll(final Collection<?> c) {
final Collection<V> coll = getMapping();
if (coll == null) {
return false;
}
final boolean result = coll.retainAll(c);
if (coll.isEmpty()) {
AbstractMultiValuedMap.this.remove(key);
}
return result;
}
@Override
public Object[] toArray() {
final Collection<V> coll = getMapping();
if (coll == null) {
return CollectionUtils.EMPTY_COLLECTION.toArray();
}
return coll.toArray();
}
@Override
@SuppressWarnings("unchecked")
public <T> T[] toArray(final T[] a) {
final Collection<V> coll = getMapping();
if (coll == null) {
return (T[]) CollectionUtils.EMPTY_COLLECTION.toArray(a);
}
return coll.toArray(a);
}
@Override
public String toString() {
final Collection<V> coll = getMapping();
if (coll == null) {
return CollectionUtils.EMPTY_COLLECTION.toString();
}
return coll.toString();
}
}
/**
* Inner class that provides a MultiSet<K> keys view.
*/
private class KeysMultiSet extends AbstractMultiSet<K> {
@Override
public boolean contains(final Object o) {
return getMap().containsKey(o);
}
@Override
public boolean isEmpty() {
return getMap().isEmpty();
}
@Override
public int size() {
return AbstractMultiValuedMap.this.size();
}
@Override
protected int uniqueElements() {
return getMap().size();
}
@Override
public int getCount(final Object object) {
int count = 0;
final Collection<V> col = AbstractMultiValuedMap.this.getMap().get(object);
if (col != null) {
count = col.size();
}
return count;
}
@Override
protected Iterator<MultiSet.Entry<K>> createEntrySetIterator() {
final MapEntryTransformer transformer = new MapEntryTransformer();
return IteratorUtils.transformedIterator(map.entrySet().iterator(), transformer);
}
private final class MapEntryTransformer
implements Transformer<Map.Entry<K, Collection<V>>, MultiSet.Entry<K>> {
@Override
public MultiSet.Entry<K> transform(final Map.Entry<K, Collection<V>> mapEntry) {
return new AbstractMultiSet.AbstractEntry<K>() {
@Override
public K getElement() {
return mapEntry.getKey();
}
@Override
public int getCount() {
return mapEntry.getValue().size();
}
};
}
}
}
/**
* Inner class that provides the Entry<K, V> view
*/
private class EntryValues extends AbstractCollection<Entry<K, V>> {
@Override
public Iterator<Entry<K, V>> iterator() {
return new LazyIteratorChain<Entry<K, V>>() {
final Collection<K> keysCol = new ArrayList<>(getMap().keySet());
final Iterator<K> keyIterator = keysCol.iterator();
@Override
protected Iterator<? extends Entry<K, V>> nextIterator(final int count) {
if (!keyIterator.hasNext()) {
return null;
}
final K key = keyIterator.next();
final Transformer<V, Entry<K, V>> entryTransformer = input -> new MultiValuedMapEntry(key, input);
return new TransformIterator<>(new ValuesIterator(key), entryTransformer);
}
};
}
@Override
public int size() {
return AbstractMultiValuedMap.this.size();
}
}
/**
* Inner class for MultiValuedMap Entries.
*/
private class MultiValuedMapEntry extends AbstractMapEntry<K, V> {
MultiValuedMapEntry(final K key, final V value) {
super(key, value);
}
@Override
public V setValue(final V value) {
throw new UnsupportedOperationException();
}
}
/**
* Inner class for MapIterator.
*/
private class MultiValuedMapIterator implements MapIterator<K, V> {
private final Iterator<Entry<K, V>> it;
private Entry<K, V> current = null;
MultiValuedMapIterator() {
this.it = AbstractMultiValuedMap.this.entries().iterator();
}
@Override
public boolean hasNext() {
return it.hasNext();
}
@Override
public K next() {
current = it.next();
return current.getKey();
}
@Override
public K getKey() {
if (current == null) {
throw new IllegalStateException();
}
return current.getKey();
}
@Override
public V getValue() {
if (current == null) {
throw new IllegalStateException();
}
return current.getValue();
}
@Override
public void remove() {
it.remove();
}
@Override
public V setValue(final V value) {
if (current == null) {
throw new IllegalStateException();
}
return current.setValue(value);
}
}
/**
* Inner class that provides the values view.
*/
private class Values extends AbstractCollection<V> {
@Override
public Iterator<V> iterator() {
final IteratorChain<V> chain = new IteratorChain<>();
for (final K k : keySet()) {
chain.addIterator(new ValuesIterator(k));
}
return chain;
}
@Override
public int size() {
return AbstractMultiValuedMap.this.size();
}
@Override
public void clear() {
AbstractMultiValuedMap.this.clear();
}
}
/**
* Inner class that provides the values iterator.
*/
private class ValuesIterator implements Iterator<V> {
private final Object key;
private final Collection<V> values;
private final Iterator<V> iterator;
ValuesIterator(final Object key) {
this.key = key;
this.values = getMap().get(key);
this.iterator = values.iterator();
}
@Override
public void remove() {
iterator.remove();
if (values.isEmpty()) {
AbstractMultiValuedMap.this.remove(key);
}
}
@Override
public boolean hasNext() {
return iterator.hasNext();
}
@Override
public V next() {
return iterator.next();
}
}
/**
* Inner class that provides the AsMap view.
*/
private class AsMap extends AbstractMap<K, Collection<V>> {
final transient Map<K, Collection<V>> decoratedMap;
AsMap(final Map<K, Collection<V>> map) {
this.decoratedMap = map;
}
@Override
public Set<Map.Entry<K, Collection<V>>> entrySet() {
return new AsMapEntrySet();
}
@Override
public boolean containsKey(final Object key) {
return decoratedMap.containsKey(key);
}
@Override
public Collection<V> get(final Object key) {
final Collection<V> collection = decoratedMap.get(key);
if (collection == null) {
return null;
}
@SuppressWarnings("unchecked")
final K k = (K) key;
return wrappedCollection(k);
}
@Override
public Set<K> keySet() {
return AbstractMultiValuedMap.this.keySet();
}
@Override
public int size() {
return decoratedMap.size();
}
@Override
public Collection<V> remove(final Object key) {
final Collection<V> collection = decoratedMap.remove(key);
if (collection == null) {
return null;
}
final Collection<V> output = createCollection();
output.addAll(collection);
collection.clear();
return output;
}
@Override
public boolean equals(final Object object) {
return this == object || decoratedMap.equals(object);
}
@Override
public int hashCode() {
return decoratedMap.hashCode();
}
@Override
public String toString() {
return decoratedMap.toString();
}
@Override
public void clear() {
AbstractMultiValuedMap.this.clear();
}
class AsMapEntrySet extends AbstractSet<Map.Entry<K, Collection<V>>> {
@Override
public Iterator<Map.Entry<K, Collection<V>>> iterator() {
return new AsMapEntrySetIterator(decoratedMap.entrySet().iterator());
}
@Override
public int size() {
return AsMap.this.size();
}
@Override
public void clear() {
AsMap.this.clear();
}
@Override
public boolean contains(final Object o) {
return decoratedMap.entrySet().contains(o);
}
@Override
public boolean remove(final Object o) {
if (!contains(o)) {
return false;
}
final Map.Entry<?, ?> entry = (Map.Entry<?, ?>) o;
AbstractMultiValuedMap.this.remove(entry.getKey());
return true;
}
}
/**
* EntrySet iterator for the asMap view.
*/
class AsMapEntrySetIterator extends AbstractIteratorDecorator<Map.Entry<K, Collection<V>>> {
AsMapEntrySetIterator(final Iterator<Map.Entry<K, Collection<V>>> iterator) {
super(iterator);
}
@Override
public Map.Entry<K, Collection<V>> next() {
final Map.Entry<K, Collection<V>> entry = super.next();
final K key = entry.getKey();
return new UnmodifiableMapEntry<>(key, wrappedCollection(key));
}
}
}
//-----------------------------------------------------------------------
/**
* Write the map out using a custom routine.
* @param out the output stream
* @throws IOException any of the usual I/O related exceptions
*/
protected void doWriteObject(final ObjectOutputStream out) throws IOException {
out.writeInt(map.size());
for (final Map.Entry<K, Collection<V>> entry : map.entrySet()) {
out.writeObject(entry.getKey());
out.writeInt(entry.getValue().size());
for (final V value : entry.getValue()) {
out.writeObject(value);
}
}
}
/**
* Read the map in using a custom routine.
* @param in the input stream
* @throws IOException any of the usual I/O related exceptions
* @throws ClassNotFoundException if the stream contains an object which class can not be loaded
* @throws ClassCastException if the stream does not contain the correct objects
*/
protected void doReadObject(final ObjectInputStream in)
throws IOException, ClassNotFoundException {
final int entrySize = in.readInt();
for (int i = 0; i < entrySize; i++) {
@SuppressWarnings("unchecked") // This will fail at runtime if the stream is incorrect
final K key = (K) in.readObject();
final Collection<V> values = get(key);
final int valueSize = in.readInt();
for (int j = 0; j < valueSize; j++) {
@SuppressWarnings("unchecked") // see above
final V value = (V) in.readObject();
values.add(value);
}
}
}
}