/* * Copyright 2013 Michael Gatto <michael@gatto.ch> * * 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 li.strolch.utils.objectfilter; import java.text.MessageFormat; import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; /** * This class implements a filter where modifications to an object are collected, and only the most recent action and * version of the object is returned. * <p> * In its current implementation, any instance of an object may "live" in the cache registered only under one single * key. * </p> * <p> * The rules of the cache are defined as follows. The top row represents the state of the cache for an object, given in * version O1. Here, "N/A" symbolizes that the object is not yet present in the cache or, if it is a result of an * operation, that it is removed from the cache. Each other row symbolizes the next action that is performed on an * object, with the cell containing the "final" action for this object with the version of the object to be retained. * Err! symbolize incorrect sequences of events that cause an exception. * </p> * <table border="1"> * <tr> * <td>Action \ State in Cache</td> * <td>N/A</td> * <td>Add(01)</td> * <td>Update(O1)</td> * <td>Remove(O1)</td> * </tr> * <tr> * <td>Add (O2)</td> * <td>Add(O2)</td> * <td>Err!</td> * <td>Err!</td> * <td>Update(O2)</td> * </tr> * <tr> * <td>Update (O2)</td> * <td>Update(O2)</td> * <td>Add(O2)</td> * <td>Update(O2)</td> * <td>Err!</td> * </tr> * <tr> * <td>Remove (O2)</td> * <td>Remove(O2)</td> * <td>N/A</td> * <td>Remove(O2)</td> * <td>Err!</td> * </tr> * </table> * * @author Michael Gatto <michael@gatto.ch> (initial version) * @author Robert von Burg <eitch@eitchnet.ch> (minor modifications, refactorings) */ public class ObjectFilter { private final static Logger logger = LoggerFactory.getLogger(ObjectFilter.class); private static long id = ObjectCache.UNSET; private final Map<Object, ObjectCache> cache; private final Set<String> keySet; /** * Default constructor initializing the filter */ public ObjectFilter() { this.cache = new HashMap<>(); this.keySet = new HashSet<>(); } /** * Register, under the given key, the addition of the given object. * <p> * This is the single point where the updating logic is applied for the cache in case of addition. The logic is: * <table border="1" > * <tr> * <td>Action\State in Cache</td> * <td>N/A</td> * <td>Add(01)</td> * <td>Update(O1)</td> * <td>Remove(O1)</td> * </tr> * <tr> * <td>Add (O2)</td> * <td>Add(O2)</td> * <td>Err!</td> * <td>Err!</td> * <td>Update(O2)</td> * </tr> * </table> * * @param key * the key to register the object with * @param objectToAdd * The object for which addition shall be registered. */ public void add(String key, Object objectToAdd) { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug(MessageFormat.format("add object {0} with key {1}", objectToAdd, key)); //$NON-NLS-1$ // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. ObjectCache cached = this.cache.get(objectToAdd); if (cached == null) { // The object has not yet been added to the cache. // Hence, we add it now, with the ADD operation. ObjectCache cacheObj = new ObjectCache(dispenseID(), key, objectToAdd, Operation.ADD); this.cache.put(objectToAdd, cacheObj); } else { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; //$NON-NLS-1$ throw new IllegalArgumentException(MessageFormat.format(msg, Long.toString(id), Operation.ADD.toString(), existingKey, key, objectToAdd.toString())); } // The object is in cache: update the version as required, keeping in mind that most // of the cases here will be mistakes... Operation op = cached.getOperation(); switch (op) { case ADD: throw new IllegalStateException("Stale State exception: Invalid + after +"); //$NON-NLS-1$ case MODIFY: throw new IllegalStateException("Stale State exception: Invalid + after +="); //$NON-NLS-1$ case REMOVE: // replace key if necessary replaceKey(cached.getObject(), objectToAdd); // update operation's object cached.setObject(objectToAdd); cached.setOperation(Operation.MODIFY); break; default: throw new IllegalStateException("Stale State exception: Unhandled state " + op); //$NON-NLS-1$ } // switch }// else of object not in cache // register the key this.keySet.add(key); } private void replaceKey(Object oldObject, Object newObject) { if (oldObject != newObject) { if (ObjectFilter.logger.isDebugEnabled()) { String msg = "Replacing key for object as they are not the same reference: old: {0} / new: {1}"; //$NON-NLS-1$ msg = MessageFormat.format(msg, oldObject, newObject); ObjectFilter.logger.warn(msg); } ObjectCache objectCache = this.cache.remove(oldObject); this.cache.put(newObject, objectCache); } } /** * Register, under the given key, the update of the given object. * </p> * <table border="1"> * <tr> * <td>Action \ State in Cache</td> * <td>N/A</td> * <td>Add(01)</td> * <td>Update(O1)</td> * <td>Remove(O1)</td> * </tr> * <tr> * <td>Update (O2)</td> * <td>Update(O2)</td> * <td>Add(O2)</td> * <td>Update(O2)</td> * <td>Err!</td> * </tr> * </table> * * @param key * the key to register the object with * @param objectToUpdate * The object for which update shall be registered. */ public void update(String key, Object objectToUpdate) { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug(MessageFormat.format("update object {0} with key {1}", objectToUpdate, key)); //$NON-NLS-1$ // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. ObjectCache cached = this.cache.get(objectToUpdate); if (cached == null) { // The object got an ID during this run, but was not added to this cache. // Hence, we add it now, with the current operation. ObjectCache cacheObj = new ObjectCache(dispenseID(), key, objectToUpdate, Operation.MODIFY); this.cache.put(objectToUpdate, cacheObj); } else { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; //$NON-NLS-1$ throw new IllegalArgumentException(MessageFormat.format(msg, Long.toString(id), Operation.MODIFY.toString(), existingKey, key, objectToUpdate.toString())); } // The object is in cache: update the version as required. Operation op = cached.getOperation(); switch (op) { case ADD: case MODIFY: // replace key if necessary replaceKey(cached.getObject(), objectToUpdate); // update operation's object cached.setObject(objectToUpdate); break; case REMOVE: throw new IllegalStateException("Stale State exception: Invalid += after -"); //$NON-NLS-1$ default: throw new IllegalStateException("Stale State exception: Unhandled state " + op); //$NON-NLS-1$ } // switch }// else of object not in cache // register the key this.keySet.add(key); } /** * Register, under the given key, the removal of the given object. * </p> * <table border="1"> * <tr> * <td>Action \ State in Cache</td> * <td>N/A</td> * <td>Add(01)</td> * <td>Update(O1)</td> * <td>Remove(O1)</td> * </tr> * <tr> * <td>Remove (O2)</td> * <td>Remove(O2)</td> * <td>N/A</td> * <td>Remove(O2)</td> * <td>Err!</td> * </tr> * </table> * * @param key * the key to register the object with * @param objectToRemove * The object for which removal shall be registered. */ public void remove(String key, Object objectToRemove) { if (ObjectFilter.logger.isDebugEnabled()) ObjectFilter.logger.debug(MessageFormat.format("remove object {0} with key {1}", objectToRemove, key)); //$NON-NLS-1$ // BEWARE: you fix a bug here, be sure to update BOTH tables on the logic. ObjectCache cached = this.cache.get(objectToRemove); if (cached == null) { // The object got an ID during this run, but was not added to this cache. // Hence, we add it now, with the current operation. ObjectCache cacheObj = new ObjectCache(dispenseID(), key, objectToRemove, Operation.REMOVE); this.cache.put(objectToRemove, cacheObj); } else { String existingKey = cached.getKey(); if (!existingKey.equals(key)) { String msg = "Invalid key provided for object with transaction ID {0} and operation {1}: existing key is {2}, new key is {3}. Object may be present in the same filter instance only once, registered using one key only. Object:{4}"; //$NON-NLS-1$ throw new IllegalArgumentException(MessageFormat.format(msg, Long.toString(id), Operation.REMOVE.toString(), existingKey, key, objectToRemove.toString())); } // The object is in cache: update the version as required. Operation op = cached.getOperation(); switch (op) { case ADD: // this is a case where we're removing the object from the cache, since we are // removing it now and it was added previously. this.cache.remove(objectToRemove); break; case MODIFY: // replace key if necessary replaceKey(cached.getObject(), objectToRemove); // update operation's object cached.setObject(objectToRemove); cached.setOperation(Operation.REMOVE); break; case REMOVE: throw new IllegalStateException("Stale State exception: Invalid - after -"); //$NON-NLS-1$ default: throw new IllegalStateException("Stale State exception: Unhandled state " + op); //$NON-NLS-1$ } // switch } // register the key this.keySet.add(key); } /** * Register, under the given key, the addition of the given list of objects. * * @param key * the key to register the objects with * @param addedObjects * The objects for which addition shall be registered. */ public void addAll(String key, Collection<Object> addedObjects) { for (Object addObj : addedObjects) { add(key, addObj); } } /** * Register, under the given key, the update of the given list of objects. * * @param key * the key to register the objects with * @param updatedObjects * The objects for which update shall be registered. */ public void updateAll(String key, Collection<Object> updatedObjects) { for (Object update : updatedObjects) { update(key, update); } } /** * Register, under the given key, the removal of the given list of objects. * * @param key * the key to register the objects with * @param removedObjects * The objects for which removal shall be registered. */ public void removeAll(String key, Collection<Object> removedObjects) { for (Object removed : removedObjects) { remove(key, removed); } } /** * Register the addition of the given object. Since no key is provided, the class name is used as a key. * * @param object * The object that shall be registered for addition */ public void add(Object object) { add(object.getClass().getName(), object); } /** * Register the update of the given object. Since no key is provided, the class name is used as a key. * * @param object * The object that shall be registered for updating */ public void update(Object object) { update(object.getClass().getName(), object); } /** * Register the removal of the given object. Since no key is provided, the class name is used as a key. * * @param object * The object that shall be registered for removal */ public void remove(Object object) { remove(object.getClass().getName(), object); } /** * Register the addition of all objects in the list. Since no key is provided, the class name of each object is used * as a key. * * @param objects * The objects that shall be registered for addition */ public void addAll(List<Object> objects) { for (Object addedObj : objects) { add(addedObj.getClass().getName(), addedObj); } } /** * Register the updating of all objects in the list. Since no key is provided, the class name of each object is used * as a key. * * @param updateObjects * The objects that shall be registered for updating */ public void updateAll(List<Object> updateObjects) { for (Object update : updateObjects) { update(update.getClass().getName(), update); } } /** * Register the removal of all objects in the list. Since no key is provided, the class name of each object is used * as a key. * * @param removedObjects * The objects that shall be registered for removal */ public void removeAll(List<Object> removedObjects) { for (Object removed : removedObjects) { remove(removed.getClass().getName(), removed); } } /** * Get all objects that were registered under the given key and that have as a resulting final action an addition. * * @param key * The registration key of the objects to match * * @return The list of all objects registered under the given key and that need to be added. */ public List<Object> getAdded(String key) { List<Object> addedObjects = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { addedObjects.add(objectCache.getObject()); } } return addedObjects; } /** * Get all objects that were registered under the given key and that have as a resulting final action an addition. * * @param clazz * The class type of the object to be retrieved, that acts as an additional filter criterion. * @param key * The registration key of the objects to match * * @return The list of all objects registered under the given key and that need to be added. */ public <V extends Object> List<V> getAdded(Class<V> clazz, String key) { List<V> addedObjects = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.ADD && (objectCache.getKey().equals(key))) { if (objectCache.getObject().getClass() == clazz) { @SuppressWarnings("unchecked") V object = (V) objectCache.getObject(); addedObjects.add(object); } } } return addedObjects; } /** * Get all objects that were registered under the given key and that have as a resulting final action an update. * * @param key * registration key of the objects to match * * @return The list of all objects registered under the given key and that need to be updated. */ public List<Object> getUpdated(String key) { List<Object> updatedObjects = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { updatedObjects.add(objectCache.getObject()); } } return updatedObjects; } /** * Get all objects that were registered under the given key and that have as a resulting final action an update. * * @param clazz * The class type of the object to be retrieved, that acts as an additional filter criterion. * @param key * registration key of the objects to match * * @return The list of all objects registered under the given key and that need to be updated. */ public <V extends Object> List<V> getUpdated(Class<V> clazz, String key) { List<V> updatedObjects = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.MODIFY && (objectCache.getKey().equals(key))) { if (objectCache.getObject().getClass() == clazz) { @SuppressWarnings("unchecked") V object = (V) objectCache.getObject(); updatedObjects.add(object); } } } return updatedObjects; } /** * Get all objects that were registered under the given key that have as a resulting final action their removal. * * @param key * The registration key of the objects to match * * @return The list of object registered under the given key that have, as a final action, removal. */ public List<Object> getRemoved(String key) { List<Object> removedObjects = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { removedObjects.add(objectCache.getObject()); } } return removedObjects; } /** * Get all objects that were registered under the given key that have as a resulting final action their removal. * * @param clazz * The class type of the object to be retrieved, that acts as an additional filter criterion. * @param key * The registration key of the objects to match * * @return The list of object registered under the given key that have, as a final action, removal. */ public <V extends Object> List<V> getRemoved(Class<V> clazz, String key) { List<V> removedObjects = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getOperation() == Operation.REMOVE && (objectCache.getKey().equals(key))) { if (objectCache.getObject().getClass() == clazz) { @SuppressWarnings("unchecked") V object = (V) objectCache.getObject(); removedObjects.add(object); } } } return removedObjects; } /** * Get all objects that were registered under the given key * * @param clazz * The class type of the object to be retrieved, that acts as an additional filter criterion. * @param key * The registration key of the objects to match * * @return The list of object registered under the given key that have, as a final action, removal. */ public <V extends Object> List<V> getAll(Class<V> clazz, String key) { List<V> objects = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { if (objectCache.getObject().getClass() == clazz) { @SuppressWarnings("unchecked") V object = (V) objectCache.getObject(); objects.add(object); } } } return objects; } /** * Get all objects that of the given class * * @param clazz * The class type of the object to be retrieved, that acts as an additional filter criterion. * * @return The list of all objects that of the given class */ public <V extends Object> List<V> getAll(Class<V> clazz) { List<V> objects = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getObject().getClass() == clazz) { @SuppressWarnings("unchecked") V object = (V) objectCache.getObject(); objects.add(object); } } return objects; } /** * Get all the objects that were processed in this transaction, that were registered under the given key. No action * is associated to the object. * * @param key * The registration key for which the objects shall be retrieved * * @return The list of objects matching the given key. */ public List<Object> getAll(String key) { List<Object> allObjects = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { allObjects.add(objectCache.getObject()); } } return allObjects; } /** * Get all the objects that were processed in this transaction, that were registered under the given key. No action * is associated to the object. * * @param key * The registration key for which the objects shall be retrieved * * @return The list of objects matching the given key. */ public List<ObjectCache> getCache(String key) { List<ObjectCache> allCache = new LinkedList<>(); Collection<ObjectCache> allObjs = this.cache.values(); for (ObjectCache objectCache : allObjs) { if (objectCache.getKey().equals(key)) { allCache.add(objectCache); } } return allCache; } /** * Return the set of keys that are currently present in the object filter. * * @return The set containing the keys of that have been added to the filter. */ public Set<String> keySet() { return this.keySet; } /** * Clears the cache */ public void clearCache() { this.cache.clear(); this.keySet.clear(); } /** * @return the set of keys used to register objects */ public int sizeKeySet() { return this.keySet.size(); } /** * @return the number of objects registered in this filter */ public int sizeCache() { return this.cache.size(); } /** * @return get a unique transaction ID */ public synchronized long dispenseID() { ObjectFilter.id++; if (ObjectFilter.id == Long.MAX_VALUE) { ObjectFilter.logger.error("Rolling IDs of objectFilter back to 1. Hope this is fine."); //$NON-NLS-1$ ObjectFilter.id = 1; } return ObjectFilter.id; } }