/*
* JBoss, Home of Professional Open Source
* Copyright 2010, Red Hat Middleware LLC, and individual contributors
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* Licensed 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.jboss.solder.util.collections;
import java.util.AbstractCollection;
import java.util.Collection;
import java.util.ConcurrentModificationException;
import java.util.Iterator;
import static org.jboss.solder.util.collections.Preconditions.checkNotNull;
/**
* Collection decorator that stays in sync with the multimap values for a
* key. There are two kinds of wrapped collections: full and subcollections.
* Both have a delegate pointing to the underlying collection class.
* <p/>
* <p/>
* Full collections, identified by a null ancestor field, contain all
* multimap values for a given key. Its delegate is a value in
* {@link AbstractMultimap#map} whenever the delegate is non-empty. The
* {@code refreshIfEmpty}, {@code removeIfEmpty}, and {@code addToMap}
* methods ensure that the {@code WrappedCollection} and map remain
* consistent.
* <p/>
* <p/>
* A subcollection, such as a sublist, contains some of the values for a
* given key. Its ancestor field points to the full wrapped collection with
* all values for the key. The subcollection {@code refreshIfEmpty}, {@code
* removeIfEmpty}, and {@code addToMap} methods call the corresponding
* methods of the full wrapped collection.
*/
class WrappedCollection<K, V> extends AbstractCollection<V> {
/**
*
*/
private final AbstractMultimap<K, V> abstractMultimap;
final K key;
Collection<V> delegate;
final WrappedCollection<K, V> ancestor;
final Collection<V> ancestorDelegate;
WrappedCollection(AbstractMultimap<K, V> abstractMultimap, K key, Collection<V> delegate, WrappedCollection<K, V> ancestor) {
this.abstractMultimap = abstractMultimap;
this.key = key;
this.delegate = delegate;
this.ancestor = ancestor;
this.ancestorDelegate = (ancestor == null) ? null : ancestor.getDelegate();
}
public AbstractMultimap<K, V> getParent() {
return this.abstractMultimap;
}
/**
* If the delegate collection is empty, but the multimap has values for
* the key, replace the delegate with the new collection for the key.
* <p/>
* <p/>
* For a subcollection, refresh its ancestor and validate that the
* ancestor delegate hasn't changed.
*/
void refreshIfEmpty() {
if (ancestor != null) {
ancestor.refreshIfEmpty();
if (ancestor.getDelegate() != ancestorDelegate) {
throw new ConcurrentModificationException();
}
} else if (delegate.isEmpty()) {
Collection<V> newDelegate = abstractMultimap.map.get(key);
if (newDelegate != null) {
delegate = newDelegate;
}
}
}
/**
* If collection is empty, remove it from {@code map}. For subcollections,
* check whether the ancestor collection is empty.
*/
void removeIfEmpty() {
if (ancestor != null) {
ancestor.removeIfEmpty();
} else if (delegate.isEmpty()) {
abstractMultimap.map.remove(key);
}
}
K getKey() {
return key;
}
/**
* Add the delegate to the map. Other {@code WrappedCollection} methods
* should call this method after adding elements to a previously empty
* collection.
* <p/>
* <p/>
* Subcollection add the ancestor's delegate instead.
*/
void addToMap() {
if (ancestor != null) {
ancestor.addToMap();
} else {
abstractMultimap.map.put(key, delegate);
}
}
@Override
public int size() {
refreshIfEmpty();
return delegate.size();
}
@Override
public boolean equals(Object object) {
if (object == this) {
return true;
}
refreshIfEmpty();
return delegate.equals(object);
}
@Override
public int hashCode() {
refreshIfEmpty();
return delegate.hashCode();
}
@Override
public String toString() {
refreshIfEmpty();
return delegate.toString();
}
Collection<V> getDelegate() {
return delegate;
}
@Override
public Iterator<V> iterator() {
refreshIfEmpty();
return new WrappedIterator(this);
}
@Override
public boolean add(V value) {
refreshIfEmpty();
boolean wasEmpty = delegate.isEmpty();
boolean changed = delegate.add(value);
if (changed) {
abstractMultimap.totalSize++;
if (wasEmpty) {
addToMap();
}
}
return changed;
}
WrappedCollection<K, V> getAncestor() {
return ancestor;
}
// The following methods are provided for better performance.
@Override
public boolean addAll(Collection<? extends V> collection) {
if (collection.isEmpty()) {
return false;
}
int oldSize = size(); // calls refreshIfEmpty
boolean changed = delegate.addAll(collection);
if (changed) {
int newSize = delegate.size();
abstractMultimap.totalSize += (newSize - oldSize);
if (oldSize == 0) {
addToMap();
}
}
return changed;
}
@Override
public boolean contains(Object o) {
refreshIfEmpty();
return delegate.contains(o);
}
@Override
public boolean containsAll(Collection<?> c) {
refreshIfEmpty();
return delegate.containsAll(c);
}
@Override
public void clear() {
int oldSize = size(); // calls refreshIfEmpty
if (oldSize == 0) {
return;
}
delegate.clear();
abstractMultimap.totalSize -= oldSize;
removeIfEmpty(); // maybe shouldn't be removed if this is a sublist
}
@Override
public boolean remove(Object o) {
refreshIfEmpty();
boolean changed = delegate.remove(o);
if (changed) {
abstractMultimap.totalSize--;
removeIfEmpty();
}
return changed;
}
@Override
public boolean removeAll(Collection<?> c) {
if (c.isEmpty()) {
return false;
}
int oldSize = size(); // calls refreshIfEmpty
boolean changed = delegate.removeAll(c);
if (changed) {
int newSize = delegate.size();
abstractMultimap.totalSize += (newSize - oldSize);
removeIfEmpty();
}
return changed;
}
@Override
public boolean retainAll(Collection<?> c) {
checkNotNull(c);
int oldSize = size(); // calls refreshIfEmpty
boolean changed = delegate.retainAll(c);
if (changed) {
int newSize = delegate.size();
abstractMultimap.totalSize += (newSize - oldSize);
removeIfEmpty();
}
return changed;
}
}