/*******************************************************************************
* Copyright (c) 2006, 2015 IBM Corporation 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
*
* Contributors:
* IBM Corporation - initial API and implementation
* Matthew Hall - bugs 241585, 247394, 226289, 194734, 190881, 266754,
* 268688
* Ovidio Mallo - bug 303847
* Stefan Xenos <sxenos@gmail.com> - Bug 335792
*******************************************************************************/
package org.eclipse.core.databinding.observable.map;
import java.util.AbstractSet;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import org.eclipse.core.databinding.observable.Diffs;
import org.eclipse.core.databinding.observable.DisposeEvent;
import org.eclipse.core.databinding.observable.IDisposeListener;
import org.eclipse.core.databinding.observable.IStaleListener;
import org.eclipse.core.databinding.observable.ObservableTracker;
import org.eclipse.core.databinding.observable.StaleEvent;
import org.eclipse.core.databinding.observable.set.IObservableSet;
import org.eclipse.core.databinding.observable.set.ISetChangeListener;
import org.eclipse.core.databinding.observable.set.SetChangeEvent;
import org.eclipse.core.internal.databinding.identity.IdentitySet;
/**
* Maps objects to one of their attributes. Tracks changes to the underlying
* observable set of objects (keys), as well as changes to attribute values.
*
* @param <K>
* type of the keys to the map
* @param <V>
* type of the values in the map
*/
public abstract class ComputedObservableMap<K, V> extends AbstractObservableMap<K, V> {
private IObservableSet<K> keySet;
private Set<K> knownKeys;
private Object valueType;
private ISetChangeListener<K> setChangeListener = new ISetChangeListener<K>() {
@Override
public void handleSetChange(SetChangeEvent<? extends K> event) {
Set<K> addedKeys = new HashSet<K>(event.diff.getAdditions());
Set<K> removedKeys = new HashSet<K>(event.diff.getRemovals());
Map<K, V> oldValues = new HashMap<>();
Map<K, V> newValues = new HashMap<>();
for (K removedKey : removedKeys) {
V oldValue = null;
if (removedKey != null) {
oldValue = doGet(removedKey);
unhookListener(removedKey);
knownKeys.remove(removedKey);
}
oldValues.put(removedKey, oldValue);
}
for (K addedKey : addedKeys) {
V newValue = null;
if (addedKey != null) {
newValue = doGet(addedKey);
hookListener(addedKey);
knownKeys.add(addedKey);
}
newValues.put(addedKey, newValue);
}
Set<K> changedKeys = Collections.emptySet();
fireMapChange(Diffs.createMapDiff(addedKeys, removedKeys,
changedKeys, oldValues, newValues));
}
};
private IStaleListener staleListener = new IStaleListener() {
@Override
public void handleStale(StaleEvent staleEvent) {
fireStale();
}
};
private Set<Map.Entry<K, V>> entrySet = new EntrySet();
private class EntrySet extends AbstractSet<Map.Entry<K, V>> {
@Override
public Iterator<Map.Entry<K, V>> iterator() {
final Iterator<K> keyIterator = keySet.iterator();
return new Iterator<Map.Entry<K, V>>() {
@Override
public boolean hasNext() {
return keyIterator.hasNext();
}
@Override
public Map.Entry<K, V> next() {
final K key = keyIterator.next();
return new Map.Entry<K, V>() {
@Override
public K getKey() {
getterCalled();
return key;
}
@Override
public V getValue() {
return get(getKey());
}
@Override
public V setValue(V value) {
return put(getKey(), value);
}
};
}
@Override
public void remove() {
keyIterator.remove();
}
};
}
@Override
public int size() {
return keySet.size();
}
}
/**
* @param keySet
*/
public ComputedObservableMap(IObservableSet<K> keySet) {
this(keySet, null);
}
/**
* @param keySet
* @param valueType
* @since 1.2
*/
public ComputedObservableMap(IObservableSet<K> keySet, Object valueType) {
super(keySet.getRealm());
this.keySet = keySet;
this.valueType = valueType;
keySet.addDisposeListener(new IDisposeListener() {
@Override
public void handleDispose(DisposeEvent disposeEvent) {
ComputedObservableMap.this.dispose();
}
});
}
/**
* @deprecated Subclasses are no longer required to call this method.
*/
@Deprecated
protected void init() {
}
@Override
protected void firstListenerAdded() {
getRealm().exec(new Runnable() {
@Override
public void run() {
hookListeners();
}
});
}
@Override
protected void lastListenerRemoved() {
unhookListeners();
}
private void hookListeners() {
if (keySet != null) {
knownKeys = new IdentitySet<>();
keySet.addSetChangeListener(setChangeListener);
keySet.addStaleListener(staleListener);
for (K key : this.keySet) {
hookListener(key);
knownKeys.add(key);
}
}
}
private void unhookListeners() {
if (keySet != null) {
keySet.removeSetChangeListener(setChangeListener);
keySet.removeStaleListener(staleListener);
}
if (knownKeys != null) {
Set<K> immutableKnownKeys = Collections.unmodifiableSet(knownKeys);
for (K key : immutableKnownKeys) {
unhookListener(key);
}
knownKeys.clear();
knownKeys = null;
}
}
protected final void fireSingleChange(K key, V oldValue, V newValue) {
fireMapChange(Diffs.createMapDiffSingleChange(key, oldValue, newValue));
}
/**
* @since 1.2
*/
@Override
public Object getKeyType() {
return keySet.getElementType();
}
/**
* @since 1.2
*/
@Override
public Object getValueType() {
return valueType;
}
/**
* @since 1.3
*/
@Override
public V remove(Object key) {
checkRealm();
V oldValue = get(key);
keySet().remove(key);
return oldValue;
}
/**
* @since 1.3
*/
@Override
public boolean containsKey(Object key) {
getterCalled();
return keySet().contains(key);
}
@Override
public Set<Map.Entry<K, V>> entrySet() {
return entrySet;
}
@Override
public Set<K> keySet() {
return keySet;
}
@SuppressWarnings("unchecked")
@Override
final public V get(Object key) {
getterCalled();
if (!keySet.contains(key))
return null;
return doGet((K) key);
}
private void getterCalled() {
ObservableTracker.getterCalled(this);
}
@Override
final public V put(K key, V value) {
checkRealm();
if (!keySet.contains(key))
return null;
return doPut(key, value);
}
/**
* @param removedKey
*/
protected abstract void unhookListener(K removedKey);
/**
* @param addedKey
*/
protected abstract void hookListener(K addedKey);
/**
* @param key
* @return the value for the given key
*/
protected abstract V doGet(K key);
/**
* @param key
* @param value
* @return the old value for the given key
*/
protected abstract V doPut(K key, V value);
@Override
public boolean isStale() {
return super.isStale() || keySet.isStale();
}
@Override
public synchronized void dispose() {
unhookListeners();
entrySet = null;
keySet = null;
setChangeListener = null;
super.dispose();
}
}