| /******************************************************************************* |
| * Copyright (c) 2006, 2008 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 - bug 233306, 226289, 190881 |
| *******************************************************************************/ |
| package org.eclipse.core.databinding.observable.map; |
| |
| 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.Realm; |
| import org.eclipse.core.databinding.observable.masterdetail.IObservableFactory; |
| import org.eclipse.core.databinding.observable.set.WritableSet; |
| import org.eclipse.core.runtime.Assert; |
| |
| /** |
| * A read-only observable map formed by the composition of two observable maps. |
| * If map1 maps keys a:A to values b1:B, and map2 maps keys b2:B to values c:C, |
| * the composite map maps keys a:A to values c:C. For example, map1 could map |
| * Order objects to their corresponding Customer objects, and map2 could map |
| * Customer objects to their "last name" property of type String. The composite |
| * map of map1 and map2 would then map Order objects to their customers' last |
| * names. |
| * |
| * <p> |
| * This class is thread safe. All state accessing methods must be invoked from |
| * the {@link Realm#isCurrent() current realm}. Methods for adding and removing |
| * listeners may be invoked from any thread. |
| * </p> |
| * |
| * @since 1.1 |
| * |
| */ |
| public class CompositeMap extends ObservableMap { |
| // adds that need to go through the second map and thus will be picked up by |
| // secondMapListener. |
| private Set pendingAdds = new HashSet(); |
| |
| // Removes that need to go through the second map and thus will be picked up |
| // by secondMapListener. Maps from value being removed to key being removed. |
| private Map pendingRemoves = new HashMap(); |
| |
| // Changes that need to go through the second map and thus will be picked up |
| // by secondMapListener. Maps from old value to new value and new value to old |
| // value. |
| private Map pendingChanges = new HashMap(); |
| |
| private IMapChangeListener firstMapListener = new IMapChangeListener() { |
| |
| public void handleMapChange(MapChangeEvent event) { |
| MapDiff diff = event.diff; |
| Set rangeSetAdditions = new HashSet(); |
| Set rangeSetRemovals = new HashSet(); |
| final Set adds = new HashSet(); |
| final Set changes = new HashSet(); |
| final Set removes = new HashSet(); |
| final Map oldValues = new HashMap(); |
| |
| for (Iterator it = diff.getAddedKeys().iterator(); it.hasNext();) { |
| Object addedKey = it.next(); |
| Object newValue = diff.getNewValue(addedKey); |
| if (!rangeSet.contains(newValue)) { |
| pendingAdds.add(newValue); |
| rangeSetAdditions.add(newValue); |
| } else { |
| adds.add(addedKey); |
| wrappedMap.put(addedKey, secondMap.get(newValue)); |
| } |
| } |
| for (Iterator it = diff.getChangedKeys().iterator(); it.hasNext();) { |
| Object changedKey = it.next(); |
| Object oldValue = diff.getOldValue(changedKey); |
| Object newValue = diff.getNewValue(changedKey); |
| boolean removed = firstMap.getKeys(oldValue).isEmpty(); |
| boolean added = !rangeSet.contains(newValue); |
| if (removed) { |
| pendingRemoves.put(oldValue, changedKey); |
| rangeSetRemovals.add(oldValue); |
| } |
| if (added) { |
| pendingAdds.add(newValue); |
| rangeSetAdditions.add(newValue); |
| } |
| if (added || removed) { |
| pendingChanges.put(oldValue, newValue); |
| pendingChanges.put(newValue, oldValue); |
| } else { |
| changes.add(changedKey); |
| oldValues.put(changedKey, oldValue); |
| wrappedMap.put(changedKey, secondMap.get(newValue)); |
| } |
| } |
| for (Iterator it = diff.getRemovedKeys().iterator(); it.hasNext();) { |
| Object removedKey = it.next(); |
| Object oldValue = diff.getOldValue(removedKey); |
| if (firstMap.getKeys(oldValue).isEmpty()) { |
| pendingRemoves.put(oldValue, removedKey); |
| rangeSetRemovals.add(oldValue); |
| } else { |
| removes.add(removedKey); |
| oldValues.put(removedKey, secondMap.get(oldValue)); |
| wrappedMap.remove(removedKey); |
| } |
| } |
| |
| if (adds.size() > 0 || removes.size() > 0 || changes.size() > 0) { |
| fireMapChange(new MapDiff() { |
| |
| public Set getAddedKeys() { |
| return adds; |
| } |
| |
| public Set getChangedKeys() { |
| return changes; |
| } |
| |
| public Object getNewValue(Object key) { |
| return wrappedMap.get(key); |
| } |
| |
| public Object getOldValue(Object key) { |
| return oldValues.get(key); |
| } |
| |
| public Set getRemovedKeys() { |
| return removes; |
| } |
| }); |
| } |
| |
| if (rangeSetAdditions.size() > 0 || rangeSetRemovals.size() > 0) { |
| rangeSet.addAndRemove(rangeSetAdditions, rangeSetRemovals); |
| } |
| } |
| }; |
| |
| private IMapChangeListener secondMapListener = new IMapChangeListener() { |
| |
| public void handleMapChange(MapChangeEvent event) { |
| MapDiff diff = event.diff; |
| final Set adds = new HashSet(); |
| final Set changes = new HashSet(); |
| final Set removes = new HashSet(); |
| final Map oldValues = new HashMap(); |
| final Map newValues = new HashMap(); |
| Set addedKeys = new HashSet(diff.getAddedKeys()); |
| Set removedKeys = new HashSet(diff.getRemovedKeys()); |
| |
| for (Iterator it = addedKeys.iterator(); it.hasNext();) { |
| Object addedKey = it.next(); |
| Set elements = firstMap.getKeys(addedKey); |
| Object newValue = diff.getNewValue(addedKey); |
| if (pendingChanges.containsKey(addedKey)) { |
| Object oldKey = pendingChanges.remove(addedKey); |
| Object oldValue; |
| if (removedKeys.remove(oldKey)) { |
| oldValue = diff.getOldValue(oldKey); |
| } else { |
| oldValue = secondMap.get(oldKey); |
| } |
| pendingChanges.remove(oldKey); |
| pendingAdds.remove(addedKey); |
| pendingRemoves.remove(oldKey); |
| for (Iterator it2 = elements.iterator(); it2.hasNext();) { |
| Object element = it2.next(); |
| changes.add(element); |
| oldValues.put(element, oldValue); |
| newValues.put(element, newValue); |
| wrappedMap.put(element, newValue); |
| } |
| } else if (pendingAdds.remove(addedKey)) { |
| for (Iterator it2 = elements.iterator(); it2.hasNext();) { |
| Object element = it2.next(); |
| adds.add(element); |
| newValues.put(element, newValue); |
| wrappedMap.put(element, newValue); |
| } |
| } else { |
| Assert.isTrue(false, "unexpected case"); //$NON-NLS-1$ |
| } |
| } |
| for (Iterator it = diff.getChangedKeys().iterator(); it.hasNext();) { |
| Object changedKey = it.next(); |
| Set elements = firstMap.getKeys(changedKey); |
| for (Iterator it2 = elements.iterator(); it2.hasNext();) { |
| Object element = it2.next(); |
| changes.add(element); |
| oldValues.put(element, diff.getOldValue(changedKey)); |
| Object newValue = diff.getNewValue(changedKey); |
| newValues.put(element, newValue); |
| wrappedMap.put(element, newValue); |
| } |
| } |
| for (Iterator it = removedKeys.iterator(); it.hasNext();) { |
| Object removedKey = it.next(); |
| Object element = pendingRemoves.remove(removedKey); |
| if (element != null) { |
| if (pendingChanges.containsKey(removedKey)) { |
| Object newKey = pendingChanges.remove(removedKey); |
| pendingChanges.remove(newKey); |
| pendingAdds.remove(newKey); |
| pendingRemoves.remove(removedKey); |
| changes.add(element); |
| oldValues.put(element, diff.getOldValue(removedKey)); |
| Object newValue = secondMap.get(newKey); |
| newValues.put(element, newValue); |
| wrappedMap.put(element, newValue); |
| } else { |
| removes.add(element); |
| Object oldValue = diff.getOldValue(removedKey); |
| oldValues.put(element, oldValue); |
| wrappedMap.remove(element); |
| } |
| } else { |
| Assert.isTrue(false, "unexpected case"); //$NON-NLS-1$ |
| } |
| } |
| |
| if (adds.size() > 0 || removes.size() > 0 || changes.size() > 0) { |
| fireMapChange(new MapDiff() { |
| |
| public Set getAddedKeys() { |
| return adds; |
| } |
| |
| public Set getChangedKeys() { |
| return changes; |
| } |
| |
| public Object getNewValue(Object key) { |
| return newValues.get(key); |
| } |
| |
| public Object getOldValue(Object key) { |
| return oldValues.get(key); |
| } |
| |
| public Set getRemovedKeys() { |
| return removes; |
| } |
| }); |
| } |
| } |
| }; |
| |
| private BidiObservableMap firstMap; |
| private IObservableMap secondMap; |
| |
| private static class WritableSetPlus extends WritableSet { |
| void addAndRemove(Set additions, Set removals) { |
| wrappedSet.removeAll(removals); |
| wrappedSet.addAll(additions); |
| fireSetChange(Diffs.createSetDiff(additions, removals)); |
| } |
| } |
| |
| private WritableSetPlus rangeSet = new WritableSetPlus(); |
| |
| /** |
| * Creates a new composite map. Because the key set of the second map is |
| * determined by the value set of the given observable map |
| * <code>firstMap</code>, it cannot be passed in as an argument. Instead, |
| * the second map will be created by calling |
| * <code>secondMapFactory.createObservable(valueSet())</code>. |
| * |
| * @param firstMap |
| * the first map |
| * @param secondMapFactory |
| * a factory that creates the second map when given an observable |
| * set representing the value set of <code>firstMap</code>. |
| */ |
| public CompositeMap(IObservableMap firstMap, |
| IObservableFactory secondMapFactory) { |
| super(firstMap.getRealm(), new HashMap()); |
| this.firstMap = new BidiObservableMap(firstMap); |
| this.firstMap.addMapChangeListener(firstMapListener); |
| rangeSet.addAll(this.firstMap.values()); |
| this.secondMap = (IObservableMap) secondMapFactory |
| .createObservable(rangeSet); |
| secondMap.addMapChangeListener(secondMapListener); |
| for (Iterator it = this.firstMap.entrySet().iterator(); it.hasNext();) { |
| Map.Entry entry = (Entry) it.next(); |
| wrappedMap.put(entry.getKey(), secondMap.get(entry.getValue())); |
| } |
| } |
| |
| /** |
| * @since 1.2 |
| */ |
| public Object getKeyType() { |
| return firstMap.getKeyType(); |
| } |
| |
| /** |
| * @since 1.2 |
| */ |
| public Object getValueType() { |
| return secondMap.getValueType(); |
| } |
| |
| public synchronized void dispose() { |
| super.dispose(); |
| if (firstMap != null) { |
| firstMap.removeMapChangeListener(firstMapListener); |
| firstMap = null; |
| } |
| if (secondMap != null) { |
| secondMap.dispose(); |
| secondMap = null; |
| } |
| } |
| |
| } |