| /***************************************************************** |
| * 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 |
| * |
| * https://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.cayenne.util; |
| |
| import java.io.IOException; |
| import java.io.ObjectInputStream; |
| import java.io.ObjectOutputStream; |
| import java.io.Serializable; |
| import java.lang.ref.Reference; |
| import java.lang.ref.ReferenceQueue; |
| import java.util.AbstractMap; |
| import java.util.AbstractSet; |
| import java.util.ArrayList; |
| import java.util.Collection; |
| import java.util.HashMap; |
| import java.util.HashSet; |
| import java.util.Iterator; |
| import java.util.Map; |
| import java.util.NoSuchElementException; |
| import java.util.Set; |
| |
| /** |
| * Map that transparently stores values as references and resolves them as needed. |
| * Though this implementation tries to follow general {@link Map} contract (including equals() and hashCode()) |
| * it is not intended for general usage. |
| * <p> |
| * There is no HardReferenceMap as simple HashMap can be used for that. |
| * <p> |
| * This map doesn't guarantee that value will be there even right after put(), as GC can remove it at any time. |
| * <p> |
| * This implementation supports proper serialization. |
| * <p> |
| * |
| * @param <K> key type |
| * @param <V> value type |
| * @param <R> reference type that will be used to store values |
| * |
| * @see WeakValueMap implementation that uses WeakReference to store values |
| * @see SoftValueMap implementation that uses SoftReference to store values |
| * |
| * @since 4.1 |
| */ |
| abstract class ReferenceMap<K, V, R extends Reference<V>> extends AbstractMap<K, V> implements Serializable { |
| |
| /* |
| * Implementation notes: |
| * - internally data stored in HashMap thus this class and all implementations are not thread safe; |
| * - to track references that were cleared ReferenceQueue is used; |
| * - this map is abstract, all that required for the concrete implementation is |
| * to define newReference(Object) method; |
| * - all accessors/modifiers should call checkReferenceQueue() to clear all stale data |
| */ |
| |
| private static final long serialVersionUID = -3365744592038165092L; |
| |
| /** |
| * This is a main data storage used for most operations |
| */ |
| protected transient HashMap<K, R> map; |
| |
| protected transient ReferenceQueue<V> referenceQueue; |
| |
| /** |
| * This is a lazily created set of entries that is essentially a view to actual data |
| */ |
| protected transient Set<Entry<K, V>> entrySet; |
| |
| public ReferenceMap() { |
| map = new HashMap<>(); |
| referenceQueue = new ReferenceQueue<>(); |
| } |
| |
| public ReferenceMap(int initialCapacity) { |
| map = new HashMap<>(initialCapacity); |
| referenceQueue = new ReferenceQueue<>(); |
| } |
| |
| public ReferenceMap(Map<? extends K, ? extends V> m) { |
| this(m.size()); |
| putAll(m); |
| } |
| |
| @Override |
| public int size() { |
| checkReferenceQueue(); |
| return map.size(); |
| } |
| |
| @Override |
| public boolean isEmpty() { |
| checkReferenceQueue(); |
| return map.isEmpty(); |
| } |
| |
| @Override |
| public boolean containsKey(Object key) { |
| checkReferenceQueue(); |
| return map.containsKey(key); |
| } |
| |
| @Override |
| public boolean containsValue(Object value) { |
| checkReferenceQueue(); |
| for(R ref : map.values()) { |
| if(ref == null) { |
| // should not happen, we can't have nulls in internal map |
| throw new IllegalStateException(); |
| } |
| V v = ref.get(); |
| if(v != null) { |
| if(v.equals(value)) { |
| return true; |
| } |
| } |
| } |
| return false; |
| } |
| |
| @Override |
| public V get(Object key) { |
| checkReferenceQueue(); |
| R ref = map.get(key); |
| if(ref == null) { |
| return null; |
| } |
| return ref.get(); |
| } |
| |
| @Override |
| public V put(K key, V value) { |
| if(value == null) { |
| throw new NullPointerException("ReferenceMap can't contain null values"); |
| } |
| checkReferenceQueue(); |
| R refValue = newReference(value); |
| R oldValue = map.put(key, refValue); |
| if(oldValue == null) { |
| return null; |
| } |
| return oldValue.get(); |
| } |
| |
| @Override |
| public V remove(Object key) { |
| checkReferenceQueue(); |
| R oldValue = map.remove(key); |
| if(oldValue == null) { |
| return null; |
| } |
| return oldValue.get(); |
| } |
| |
| @Override |
| public void putAll(Map<? extends K, ? extends V> m) { |
| checkReferenceQueue(); |
| for(Map.Entry<? extends K, ? extends V> entry : m.entrySet()) { |
| if(entry.getValue() == null) { |
| throw new NullPointerException("ReferenceMap can't contain null values"); |
| } |
| R value = newReference(entry.getValue()); |
| map.put(entry.getKey(), value); |
| } |
| } |
| |
| @Override |
| public void clear() { |
| map.clear(); |
| resetReferenceQueue(); |
| } |
| |
| @Override |
| public Set<K> keySet() { |
| checkReferenceQueue(); |
| // should this check for cleared references? it can be invalid later anyway... |
| return map.keySet(); |
| } |
| |
| @Override |
| public Collection<V> values() { |
| checkReferenceQueue(); |
| // this can be optimized by creating view instead of new heavyweight collection |
| Collection<R> referenceValues = map.values(); |
| Collection<V> values = new ArrayList<>(referenceValues.size()); |
| for(R v : referenceValues) { |
| if(v != null) { |
| V value = v.get(); |
| // check for null in case GC cleared some values after last queue check |
| if(value != null) { |
| values.add(value); |
| } |
| } |
| } |
| return values; |
| } |
| |
| @Override |
| public Set<Entry<K, V>> entrySet() { |
| checkReferenceQueue(); |
| // lazily create entry set view |
| Set<Entry<K, V>> es = entrySet; |
| if(es == null) { |
| entrySet = es = new ReferenceEntrySet(); |
| } |
| return es; |
| } |
| |
| /** |
| * Cleanup all references collected by GC so far |
| */ |
| protected void checkReferenceQueue() { |
| Collection<Reference<? extends V>> valuesToRemove = null; |
| Reference<? extends V> reference; |
| while((reference = referenceQueue.poll()) != null) { |
| if(valuesToRemove == null) { |
| valuesToRemove = new HashSet<>(); |
| } |
| valuesToRemove.add(reference); |
| } |
| |
| if(valuesToRemove == null) { |
| return; |
| } |
| |
| Collection<K> keysToRemove = new ArrayList<>(valuesToRemove.size()); |
| for(Map.Entry<K, R> entry : map.entrySet()) { |
| if(valuesToRemove.contains(entry.getValue())) { |
| keysToRemove.add(entry.getKey()); |
| } |
| } |
| |
| for(K keyToRemove : keysToRemove) { |
| map.remove(keyToRemove); |
| } |
| } |
| |
| private void resetReferenceQueue() { |
| //noinspection StatementWithEmptyBody |
| while(referenceQueue.poll() != null) { |
| // just purge this queue |
| } |
| } |
| |
| /** |
| * This method should be implemented by concrete implementations of this abstract class. |
| * |
| * @param value to be wrapped into reference |
| * @return new reference to the value |
| */ |
| abstract R newReference(V value); |
| |
| private void writeObject(ObjectOutputStream out) throws IOException { |
| checkReferenceQueue(); |
| Map<K, V> replacementMap = new HashMap<>(map.size()); |
| for(Entry<K, R> entry : map.entrySet()) { |
| if(entry.getValue() != null) { |
| V value = entry.getValue().get(); |
| if(value != null) { |
| replacementMap.put(entry.getKey(), value); |
| } |
| } |
| } |
| out.writeObject(replacementMap); |
| } |
| |
| private void readObject(ObjectInputStream in) throws IOException, ClassNotFoundException { |
| @SuppressWarnings("unchecked") |
| Map<K, V> replacement = (Map<K, V>) in.readObject(); |
| map = new HashMap<>(replacement.size()); |
| referenceQueue = new ReferenceQueue<>(); |
| putAll(replacement); |
| } |
| |
| /** |
| * View over {@link #map} entry set |
| */ |
| class ReferenceEntrySet extends AbstractSet<Entry<K, V>> { |
| |
| @Override |
| public Iterator<Entry<K, V>> iterator() { |
| return new ReferenceEntryIterator(); |
| } |
| |
| @Override |
| public int size() { |
| return map.size(); |
| } |
| } |
| |
| /** |
| * Iterator used by entrySet. Wrapper around {@link #map} iterator. |
| * It fetch ahead to be sure we have valid value, or otherwise we can return cleared reference. |
| */ |
| class ReferenceEntryIterator implements Iterator<Entry<K, V>> { |
| |
| Iterator<Entry<K, R>> internalIterator; |
| |
| Entry<K, V> next; |
| |
| ReferenceEntryIterator() { |
| internalIterator = map.entrySet().iterator(); |
| tryAdvance(); |
| } |
| |
| @Override |
| public boolean hasNext() { |
| return next != null; |
| } |
| |
| @Override |
| public Entry<K, V> next() { |
| if(!hasNext()) { |
| throw new NoSuchElementException(); |
| } |
| Entry<K, V> result = next; |
| tryAdvance(); |
| return result; |
| } |
| |
| /** |
| * Moves ahead internalIterator and tries to find first and store nonnull reference |
| */ |
| private void tryAdvance() { |
| next = null; |
| |
| while(internalIterator.hasNext()) { |
| Entry<K, R> nextRefEntry = internalIterator.next(); |
| if(nextRefEntry.getValue() == null) { |
| // should not happen, we can't have nulls in internal map |
| throw new IllegalStateException(); |
| } |
| V value = nextRefEntry.getValue().get(); |
| if(value != null) { |
| next = new ReferenceEntry(nextRefEntry, value); |
| break; |
| } |
| } |
| } |
| } |
| |
| /** |
| * View over {@link Map.Entry} that transparently resolves Reference |
| */ |
| class ReferenceEntry extends SimpleEntry<K, V> { |
| |
| private static final long serialVersionUID = -1795136249842496011L; |
| |
| Entry<K, R> refEntry; |
| |
| public ReferenceEntry(Entry<K, R> refEntry, V value) { |
| super(refEntry.getKey(), value); |
| this.refEntry = refEntry; |
| } |
| |
| @Override |
| public V setValue(V value) { |
| R newRef = newReference(value); |
| R oldRef = refEntry.setValue(newRef); |
| super.setValue(value); |
| if(oldRef != null) { |
| return oldRef.get(); |
| } |
| return null; |
| } |
| } |
| } |