/** * Copyright (C) 2009 Future Invent Informationsmanagement GmbH. All rights * reserved. * * This library is free software; you can redistribute it and/or modify it under * the terms of the GNU Lesser General Public License as published by the Free * Software Foundation; either version 3 of the License, or (at your option) any * later version. * * This library is distributed in the hope that it will be useful, but WITHOUT * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more * details. * * You should have received a copy of the GNU Lesser General Public License * along with this library. If not, see . */ package org.fuin.utils4j; import java.util.Collection; import java.util.Collections; import java.util.HashMap; import java.util.Iterator; import java.util.Map; import java.util.Set; /** * A wrapper for maps that keeps track of all changes made to the map since * construction. Only adding, replacing or deleting elements is tracked (not * changes inside the objects). It's also possible to revert all changes. */ public class ChangeTrackingMap implements Map, Taggable { private final Map map; private final Map added; private final Map changed; private final Map removed; private boolean tagged; /** * Constructor with covered map. The map is tagged at construction time - * This means {@link #isTagged()} will return true without * calling {@link #tag()} first. If this behavior is not wanted you can call * {@link #untag()} after constructing the map. * * @param map * Wrapped map - Be aware that this map will be changed by this * class. There is no internal copy of the map - The reference * itself is used. */ public ChangeTrackingMap(final Map map) { super(); Utils4J.checkNotNull("map", map); this.map = map; this.added = new HashMap(); this.changed = new HashMap(); this.removed = new HashMap(); this.tagged = true; } /** * Returns if the list has changed. If the map is not in tag mode (this means * {@link #isTagged()} returns true) this method will always * return false. * * @return If elements have been added or deleted true else * false. */ public final boolean isChanged() { return (added.size() > 0) || (changed.size() > 0) || (removed.size() > 0); } /** * Returns removed elements. If the map is not in tag mode (this means * {@link #isTagged()} returns true) this method will always * return an empty map. * * @return Elements that have been deleted since construction of this * instance - Unmodifiable map! */ public final Map getRemoved() { return Collections.unmodifiableMap(removed); } /** * Returns changed elements. If the map is not in tag mode (this means * {@link #isTagged()} returns true) this method will always * return an empty map. * * @return Elements that have been changed since construction of this * instance - Unmodifiable map! */ public final Map getChanged() { return Collections.unmodifiableMap(changed); } /** * Roll back all changes made since construction. This is the * same function ad {@link #revertToTag()}. If the map is not in tag mode ( * this means {@link #isTagged()} returns true) this method * will do nothing. */ public final void revert() { if (tagged) { // Remove the added entries final Iterator addedIt = added.keySet().iterator(); while (addedIt.hasNext()) { final Object key = addedIt.next(); map.remove(key); addedIt.remove(); } // Replace the changed entries final Iterator changedIt = changed.keySet().iterator(); while (changedIt.hasNext()) { final Object key = changedIt.next(); final Object value = changed.get(key); map.put(key, value); changedIt.remove(); } // Add the removed entries final Iterator removedIt = removed.keySet().iterator(); while (removedIt.hasNext()) { final Object key = removedIt.next(); final Object value = removed.get(key); map.put(key, value); removedIt.remove(); } } } /** * Returns added elements. If the map is not in tag mode (this means * {@link #isTagged()} returns true) this method will always * return an empty map. * * @return Elements that have been added since construction of this * instance - Unmodifiable map! */ public final Map getAdded() { return Collections.unmodifiableMap(added); } private void changeIntern(final Object key, final Object oldValue, final Object newValue) { if (tagged) { final Object addedValue = added.get(key); if (addedValue == null) { final Object changedValue = changed.get(key); if (changedValue == null) { final Object removedValue = removed.get(key); if (removedValue == null) { if (oldValue == null) { added.put(key, newValue); } else { changed.put(key, oldValue); } } else { removed.remove(key); if (!removedValue.equals(newValue)) { changed.put(key, removedValue); } } } else { if (changedValue.equals(newValue)) { changed.remove(key); } } } else { if (!addedValue.equals(newValue) && (newValue != null)) { added.put(key, newValue); } } } } private void removeIntern(final Object key, final Object value) { if (tagged) { if (added.get(key) == null) { final Object changedValue = changed.get(key); if (changedValue == null) { if ((removed.get(key) == null) && (value != null)) { removed.put(key, value); } } else { changed.remove(key); removed.put(key, changedValue); } } else { added.remove(key); } } } /** * {@inheritDoc} */ public final void clear() { final Iterator it = map.keySet().iterator(); while (it.hasNext()) { final Object key = it.next(); final Object value = map.get(key); removeIntern(key, value); } if (tagged) { added.clear(); } map.clear(); } /** * {@inheritDoc} */ public final boolean containsKey(final Object key) { return map.containsKey(key); } /** * {@inheritDoc} */ public final boolean containsValue(final Object value) { return map.containsValue(value); } /** * {@inheritDoc} */ public final Set entrySet() { return map.entrySet(); } /** * {@inheritDoc} */ public final Object get(final Object key) { return map.get(key); } /** * {@inheritDoc} */ public final boolean isEmpty() { return map.isEmpty(); } /** * {@inheritDoc} */ public final Set keySet() { return map.keySet(); } /** * {@inheritDoc} */ public final Object put(final Object key, final Object newValue) { final Object oldValue = map.put(key, newValue); changeIntern(key, oldValue, newValue); return oldValue; } /** * {@inheritDoc} */ public final void putAll(final Map newMap) { final Iterator it = newMap.keySet().iterator(); while (it.hasNext()) { final Object key = it.next(); final Object newValue = newMap.get(key); final Object oldValue = map.put(key, newValue); changeIntern(key, oldValue, newValue); } } /** * {@inheritDoc} */ public final Object remove(final Object key) { final Object oldValue = map.remove(key); removeIntern(key, oldValue); return oldValue; } /** * {@inheritDoc} */ public final int size() { return map.size(); } /** * {@inheritDoc} */ public final Collection values() { return map.values(); } /** * {@inheritDoc} */ public final String toString() { return map.toString(); } /** * {@inheritDoc} */ public final boolean hasChangedSinceTagging() { return isChanged(); } /** * {@inheritDoc} */ public final boolean isTagged() { return tagged; } /** * {@inheritDoc} */ public final void revertToTag() { revert(); } /** * {@inheritDoc} */ public final void tag() { if (!tagged) { tagged = true; } } /** * {@inheritDoc} */ public final void untag() { if (tagged) { tagged = false; added.clear(); changed.clear(); removed.clear(); } } }