strolch/utils/src/main/java/li/strolch/utils/objectfilter/ObjectFilter.java

841 lines
25 KiB
Java

/*
* 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.Collections;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Stream;
import li.strolch.utils.collections.MapOfMaps;
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" summary="Action state matrix">
*
* <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 &lt;michael@gatto.ch&gt; (initial version)
* @author Robert von Burg &lt;eitch@eitchnet.ch&gt; (minor modifications, refactorings)
*/
public class ObjectFilter {
private final static Logger logger = LoggerFactory.getLogger(ObjectFilter.class);
private static long id = ObjectCache.UNSET;
private final MapOfMaps<String, Object, ObjectCache> cache;
private final Set<String> keySet;
/**
* Default constructor initializing the filter
*/
public ObjectFilter() {
this.cache = new MapOfMaps<>(1);
this.keySet = new HashSet<>(1);
}
private void replaceKey(ObjectCache cached, Object newObjectKey, Object newObject) {
if (cached.getObjectKey() != newObjectKey) {
ObjectCache objectCache = this.cache.removeElement(cached.getKey(), cached.getObjectKey());
this.cache.addElement(objectCache.getKey(), newObjectKey, objectCache);
}
}
/**
* 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" summary="Allowed operations">
* <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 objectKey
* the key for the object
* @param objectToAdd
* The object for which addition shall be registered.
*/
public void add(String key, Object objectKey, Object objectToAdd) {
// BEWARE: you fix a bug here, be sure to update BOTH tables on the logic.
ObjectCache cached = this.cache.getElement(key, objectKey);
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, objectKey, objectToAdd, Operation.ADD);
this.cache.addElement(key, objectKey, 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}";
throw new IllegalArgumentException(
MessageFormat.format(msg, Long.toString(id), Operation.ADD.toString(), existingKey, key,
objectKey.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 + for " + objectKey);
case MODIFY ->
throw new IllegalStateException("Stale State exception: Invalid + after += for " + objectKey);
case REMOVE -> {
// replace key if necessary
replaceKey(cached, objectKey, objectToAdd);
// update operation's object
cached.setObject(objectToAdd);
cached.setOperation(Operation.MODIFY);
}
default -> throw new IllegalStateException("Stale State exception: Unhandled state " + op);
} // switch
} // else of object not in cache
// register the key
this.keySet.add(key);
}
/**
* Register, under the given key, the update of the given object.
*
* <table border="1" summary="Allowed operations">
* <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 objectKey
* the key for the object
* @param objectToUpdate
* The object for which update shall be registered.
*/
public void update(String key, Object objectKey, Object objectToUpdate) {
// BEWARE: you fix a bug here, be sure to update BOTH tables on the logic.
ObjectCache cached = this.cache.getElement(key, objectKey);
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, objectKey, objectToUpdate, Operation.MODIFY);
this.cache.addElement(key, objectKey, cacheObj);
} else {
// The object is in cache: update the version as required.
Operation op = cached.getOperation();
switch (op) {
case ADD, MODIFY -> {
// replace key if necessary
replaceKey(cached, objectKey, objectToUpdate);
// update operation's object
cached.setObject(objectToUpdate);
}
case REMOVE ->
throw new IllegalStateException("Stale State exception: Invalid += after - for " + objectKey);
default -> throw new IllegalStateException("Stale State exception: Unhandled state " + op);
} // 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.
*
* <table border="1" summary="allowed operations">
* <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 objectKey
* the key for the object
* @param objectToRemove
* The object for which removal shall be registered.
*/
public void remove(String key, Object objectKey, Object objectToRemove) {
// BEWARE: you fix a bug here, be sure to update BOTH tables on the logic.
ObjectCache cached = this.cache.getElement(key, objectKey);
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, objectKey, objectToRemove, Operation.REMOVE);
this.cache.addElement(key, objectKey, 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}";
throw new IllegalArgumentException(
MessageFormat.format(msg, Long.toString(id), Operation.REMOVE.toString(), existingKey, key,
objectKey.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.removeElement(key, objectKey);
case MODIFY -> {
// replace key if necessary
replaceKey(cached, objectKey, objectToRemove);
// update operation's object
cached.setObject(objectToRemove);
cached.setOperation(Operation.REMOVE);
}
case REMOVE -> throw new IllegalStateException("Stale State exception: Invalid - after - for " + objectKey);
default -> throw new IllegalStateException("Stale State exception: Unhandled state " + op);
} // switch
}
// register the key
this.keySet.add(key);
}
/**
* 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, object);
}
/**
* Register the addition of the given object. Since no key is provided, the class name is used as a key.
*
* @param objectKey
* the key for the object
* @param object
* The object that shall be registered for addition
*/
public void add(Object objectKey, Object object) {
add(object.getClass().getName(), objectKey, 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, object);
}
/**
* Register the update of the given object. Since no key is provided, the class name is used as a key.
*
* @param objectKey
* the key for the object
* @param object
* The object that shall be registered for updating
*/
public void update(Object objectKey, Object object) {
update(object.getClass().getName(), objectKey, 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, object);
}
/**
* Register the removal of the given object. Since no key is provided, the class name is used as a key.
*
* @param objectKey
* the key for the object
* @param object
* The object that shall be registered for removal
*/
public void remove(Object objectKey, Object object) {
remove(object.getClass().getName(), objectKey, object);
}
/**
* Returns the ObjectCache for the given key and objectKey
*
* @param key
* the key under which it was registered
* @param objectKey
* the objectKey
*
* @return the ObjectCache, or null
*/
public ObjectCache getCache(String key, Object objectKey) {
return this.cache.getElement(key, objectKey);
}
/**
* Returns the element with the given key and objectKey
*
* @param key
* the key under which it was registered
* @param objectKey
* the objectKey
* @param <V>
* the class to which to cast the element to
*
* @return the element, or null
*/
public <V> V getElement(String key, Object objectKey) {
ObjectCache cache = this.cache.getElement(key, objectKey);
if (cache == null)
return null;
@SuppressWarnings("unchecked")
V element = (V) cache.getObject();
return element;
}
/**
* Return the {@link Operation} for the given keys
*
* @param key
* the key under which it was registered
* @param objectKey
* the objectKey
*
* @return the {@link Operation} for the given keys
*/
public Operation getOperation(String key, Object objectKey) {
ObjectCache objectCache = this.cache.getElement(key, objectKey);
if (objectCache == null)
return null;
return objectCache.getOperation();
}
/**
* Return true if the element with the given key and objectKey exist
*
* @param key
* the key under which it was registered
* @param objectKey
* the objectKey
*
* @return true if the element with the given key and objectKey exist
*/
public boolean hasElement(String key, Object objectKey) {
return this.cache.containsElement(key, objectKey);
}
/**
* Allows clearing the object cache for an element
*
* @param key
* the key under which it was registered
* @param objectKey
* the objectKey
*/
public void removeObjectCache(String key, Object objectKey) {
this.keySet.remove(key);
this.cache.removeElement(key, objectKey);
}
/**
* Streams all objects with the given operation and key
*
* @param operation
* the operation to match
* @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 Stream<Object> streamFor(String key, Operation operation) {
if (!this.cache.containsMap(key))
return Stream.empty();
return this.cache.getAllElements(key).stream() //
.filter(objectCache -> objectCache.getOperation() == operation) //
.map(ObjectCache::getObject);
}
/**
* Streams all objects with the given key
*
* @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 Stream<Object> streamFor(String key) {
if (!this.cache.containsMap(key))
return Stream.empty();
return this.cache.getAllElements(key).stream() //
.map(ObjectCache::getObject);
}
/**
* Streams all objects with the given key
*
* @return The list of all objects registered under the given key and that need to be added.
*/
public <V> Stream<V> streamFor(Class<V> clazz) {
return this.cache.stream().flatMap(m -> m.getValue().values().stream()) //
.filter(objectCache -> objectCache.getObject().getClass() == clazz) //
.map(objectCache -> {
@SuppressWarnings("unchecked")
V v = (V) objectCache.getObject();
return v;
});
}
/**
* Streams all objects with the given key
*
* @param clazz
* the class to match
* @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> Stream<V> streamFor(Class<V> clazz, String key) {
if (!this.cache.containsMap(key))
return Stream.empty();
return this.cache.getAllElements(key).stream() //
.filter(objectCache -> objectCache.getObject().getClass() == clazz) //
.map(objectCache -> {
@SuppressWarnings("unchecked")
V v = (V) objectCache.getObject();
return v;
});
}
/**
* Streams all objects with the given operation, key and clazz type
*
* @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> Stream<V> streamFor(Class<V> clazz, String key, Operation operation) {
return streamFor(key, operation).filter(o -> o.getClass() == clazz) //
.map(o -> {
@SuppressWarnings("unchecked")
V v = (V) o;
return v;
});
}
/**
* 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 Stream<Object> streamAdded(String key) {
return streamFor(key, Operation.ADD);
}
/**
* 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) {
return streamAdded(key).toList();
}
/**
* 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> Stream<V> streamAdded(Class<V> clazz, String key) {
return streamFor(clazz, key, Operation.ADD);
}
/**
* 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> List<V> getAdded(Class<V> clazz, String key) {
return streamAdded(clazz, key).toList();
}
/**
* 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 Stream<Object> streamUpdated(String key) {
return streamFor(key, Operation.MODIFY);
}
/**
* 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) {
return streamUpdated(key).toList();
}
/**
* 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> Stream<V> streamUpdated(Class<V> clazz, String key) {
return streamFor(clazz, key, Operation.MODIFY);
}
/**
* 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> List<V> getUpdated(Class<V> clazz, String key) {
return streamUpdated(clazz, key).toList();
}
/**
* 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 Stream<Object> streamRemoved(String key) {
return streamFor(key, Operation.REMOVE);
}
/**
* 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) {
return streamRemoved(key).toList();
}
/**
* 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> Stream<V> streamRemoved(Class<V> clazz, String key) {
return streamFor(clazz, key, Operation.REMOVE);
}
/**
* 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> List<V> getRemoved(Class<V> clazz, String key) {
return streamRemoved(clazz, key).toList();
}
/**
* 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> Stream<V> streamAll(Class<V> clazz, String key) {
return streamFor(clazz, key);
}
/**
* 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> List<V> getAll(Class<V> clazz, String key) {
return streamAll(clazz, key).toList();
}
/**
* 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> Stream<V> streamAll(Class<V> clazz) {
return streamFor(clazz);
}
/**
* 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> List<V> getAll(Class<V> clazz) {
return streamAll(clazz).toList();
}
/**
* 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 Stream<Object> streamAll(String key) {
return streamFor(key);
}
/**
* 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) {
return streamAll(key).toList();
}
/**
* 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) {
if (!this.cache.containsMap(key))
return Collections.emptyList();
return this.cache.getAllElements(key);
}
/**
* 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 size of the key set
*/
public int sizeKeySet() {
return this.keySet.size();
}
/**
* @return the number of objects registered in this filter
*/
public int sizeCache() {
return this.cache.size();
}
/**
* @return true if the cache is empty, false otherwise
*/
public boolean isEmpty() {
return this.cache.isEmpty();
}
/**
* @return get a unique transaction ID
*/
private static 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.");
ObjectFilter.id = 1;
}
return ObjectFilter.id;
}
}