package er.extensions.eof;
import java.util.LinkedHashMap;
import java.util.Map;
import com.webobjects.eocontrol.EOSortOrdering;
import com.webobjects.foundation.NSArray;
import com.webobjects.foundation.NSMutableDictionary;
import com.webobjects.foundation.NSMutableSet;
import com.webobjects.foundation.NSSet;
/**
* ERXKeyFilter provides a way to specify hierarchical rules for
* including and excluding ERXKeys. This is useful if you need
* to perform operations on a set of EO's and optional relationships
* and attributes within those EO's. As an example, ERXRest uses
* ERXKeyFilter to programmatically specify which attributes and
* relationships will be rendered for a particular root EO.
* <p>
* ERXKeyFilter is a hierarchical mapping between ERXKeys (single key,
* not keypaths), whether an include or exclude
* rule should be applied for that key, and if it's an include, the
* next set of filter rules to apply to the destination object.
* <pre><code>
* ERXKeyFilter companyFilter = ERXKeyFilter.filterWithAttribtues();
* ERXKeyFilter remindersFilter = companyFilter.include(Company.REMINDERS);
* remindersFilter.include(Reminder.SUMMARY);
* ERXKeyFilter reminderAuthorFilter = remindersFilter.include(Reminder.AUTHOR);
* reminderAuthorFilter.includeAll();
* reminderAuthorFilter.exclude(Author.HUGE_RELATIONSHIP);
* </code></pre>
* For keys representing to-many relationships you can set a distinct flag
* if you want to filter that relationship to return only distinct objects.
* For this you can either pass in a key with the unique operator of
* ERXArrayUtilities or explicitly set the flag:
* <pre><code>
* ERXKeyFilter companyFilter = ERXKeyFilter.filterWithAttribtues();
* companyFilter.include(ERXKey.unique(Company.EMPLOYEES));
* companyFilter.include(Company.CLIENTS).setDistinct(true);
* </code></pre>
*
* more method comments to come ...
*
* @author mschrag
*/
public class ERXKeyFilter {
/**
* ERXKeyFilter.Base defines the base rule that is applied
* to a filter.
*
* @author mschrag
*/
public static enum Base {
None, Attributes, AttributesAndToOneRelationships, All;
}
private ERXKeyFilter.Base _base;
private Map<ERXKey, ERXKeyFilter> _includes;
private NSMutableSet<ERXKey> _excludes;
private NSMutableSet<ERXKey> _lockedRelationships;
private Map<ERXKey, ERXKey> _map;
private NSArray<EOSortOrdering> _sortOrderings;
private ERXKeyFilter.Base _nextBase;
private ERXKeyFilter.Delegate _delegate;
private boolean _deduplicationEnabled;
private boolean _anonymousUpdateEnabled;
private boolean _unknownKeyIgnored;
private boolean _distinct;
/**
* Creates a new ERXKeyFilter.
*
* @param base the base rule to apply
*/
public ERXKeyFilter(ERXKeyFilter.Base base) {
this(base, ERXKeyFilter.Base.None);
}
/**
* Creates a new ERXKeyFilter.
*
* @param base the base rule to apply
* @param nextBase the next base rule to apply
*/
public ERXKeyFilter(ERXKeyFilter.Base base, ERXKeyFilter.Base nextBase) {
_base = base;
_nextBase = nextBase;
_includes = new LinkedHashMap<>();
_excludes = new NSMutableSet<>();
_lockedRelationships = new NSMutableSet<>();
_map = new NSMutableDictionary<>();
_deduplicationEnabled = true;
_anonymousUpdateEnabled = false;
}
/**
* Associate a filter delegate with this filter.
*
* @param delegate the delegate to associate
*/
public void setDelegate(ERXKeyFilter.Delegate delegate) {
_delegate = delegate;
for (ERXKeyFilter includedFilter : _includes.values()) {
includedFilter.setDelegate(delegate);
}
}
/**
* Returns the filter delegate for this filter.
*
* @return the delegate
*/
public ERXKeyFilter.Delegate delegate() {
return _delegate;
}
/**
* Adds a key mapping to this filter.
*
* @param fromKey the key to map from
* @param toKey the key to map to
*/
public void addMap(ERXKey fromKey, ERXKey toKey) {
_map.put(fromKey, toKey);
}
/**
* Adds a key mapping to this filter.
*
* @param fromKey the key to map from
* @param toKey the key to map to
*/
public void addMap(String fromKey, String toKey) {
addMap(new ERXKey(toKey), new ERXKey(fromKey));
}
/**
* Returns the key that is mapped to from the given input key.
*
* @param <T> the type of the key (which doesn't change)
* @param fromKey the key to map from
* @return the key that maps to the given key
*/
public <T> ERXKey<T> keyMap(ERXKey<T> fromKey) {
ERXKey<T> toKey = _map.get(fromKey);
if (toKey == null) {
toKey = fromKey;
}
return toKey;
}
/**
* Returns the key that is mapped to from the given input key.
*
* @param fromKey the key to map from
* @return the key that maps to the given key
*/
public String keyMap(String fromKey) {
ERXKey toKey = keyMap(new ERXKey(fromKey));
if (toKey == null) {
return fromKey;
}
return toKey.key();
}
/**
* Shortcut to return a new ERXKeyFilter(None)
* @return a new ERXKeyFilter(None)
*/
public static ERXKeyFilter filterWithNone() {
return new ERXKeyFilter(ERXKeyFilter.Base.None);
}
/**
* Shortcut to return a new ERXKeyFilter(Attributes)
* @return a new ERXKeyFilter(Attributes)
*/
public static ERXKeyFilter filterWithAttributes() {
return new ERXKeyFilter(ERXKeyFilter.Base.Attributes);
}
/**
* Shortcut to return a new ERXKeyFilter()
* @param keys the keys to include
* @return a new ERXKeyFilter(None) with the included keys
*/
public static ERXKeyFilter filterWithKeys(ERXKey<?>... keys) {
ERXKeyFilter keyFilter = new ERXKeyFilter(ERXKeyFilter.Base.None);
for (ERXKey<?> key : keys) {
keyFilter.include(key);
}
return keyFilter;
}
/**
* Shortcut to return a new ERXKeyFilter()
* @param keys the keys to include
* @return a new ERXKeyFilter(None) with the included keys
*/
public static ERXKeyFilter filterWithKeys(String... keys) {
ERXKeyFilter keyFilter = new ERXKeyFilter(ERXKeyFilter.Base.None);
for (String key : keys) {
keyFilter.include(new ERXKey(key));
}
return keyFilter;
}
/**
* Shortcut to return a new ERXKeyFilter(AttributesAndToOneRelationships)
* @return a new ERXKeyFilter(AttributesAndToOneRelationships)
*/
public static ERXKeyFilter filterWithAttributesAndToOneRelationships() {
return new ERXKeyFilter(ERXKeyFilter.Base.AttributesAndToOneRelationships);
}
/**
* Shortcut to return a new ERXKeyFilter(All)
* @return a new ERXKeyFilter(All)
*/
public static ERXKeyFilter filterWithAll() {
return new ERXKeyFilter(ERXKeyFilter.Base.All);
}
/**
* Shortcut to return a new ERXKeyFilter(All, All)
* @return a new ERXKeyFilter(All, All)
*/
public static ERXKeyFilter filterWithAllRecursive() {
return new ERXKeyFilter(ERXKeyFilter.Base.All, ERXKeyFilter.Base.All);
}
/**
* Returns the base rule for this filter.
*
* @return the base rule for this filter
*/
public ERXKeyFilter.Base base() {
return _base;
}
/**
* Sets the base rule to All.
*/
public void includeAll() {
setBase(ERXKeyFilter.Base.All);
}
/**
* Sets the base rule to Attributes.
*/
public void includeAttributes() {
setBase(ERXKeyFilter.Base.Attributes);
}
/**
* Sets the base rule to AttribtuesAndToOneRelationships.
*/
public void includeAttributesAndToOneRelationships() {
setBase(ERXKeyFilter.Base.AttributesAndToOneRelationships);
}
/**
* Sets the base rule to None.
*/
public void includeNone() {
setBase(ERXKeyFilter.Base.None);
}
/**
* Sets the base rule.
*
* @param base the base rule
*/
public void setBase(ERXKeyFilter.Base base) {
_base = base;
}
/**
* Returns the base that is used for subkeys of this key by default.
*
* @return the base that is used for subkeys of this key by default
*/
public ERXKeyFilter.Base nextBase() {
return _nextBase;
}
protected ERXKeyFilter createFilter(ERXKeyFilter.Base base) {
return new ERXKeyFilter(base);
}
protected ERXKeyFilter createNextFilter() {
ERXKeyFilter filter = createFilter(_nextBase);
filter.setDelegate(_delegate);
filter.setNextBase(_nextBase);
filter.setDeduplicationEnabled(_deduplicationEnabled);
filter.setAnonymousUpdateEnabled(_anonymousUpdateEnabled);
filter.setUnknownKeyIgnored(_unknownKeyIgnored);
return filter;
}
/**
* Sets the base that is used for subkeys of this key by default.
* @param nextBase the base that is used for subkeys of this key by default
* @return this (for chaining)
*/
public ERXKeyFilter setNextBase(ERXKeyFilter.Base nextBase) {
_nextBase = nextBase;
return this;
}
/**
* Sets whether nodes without ids will result in an anonymous update or an object creation. An anonymous update
* means that whatever object currently exists will remain, and the nested object graph will be applied as
* a recursive update.
*
* @param anonymousUpdateEnabled whether nodes without ids will result in an anonymous update or an object creation
*/
public void setAnonymousUpdateEnabled(boolean anonymousUpdateEnabled) {
_anonymousUpdateEnabled = anonymousUpdateEnabled;
}
/**
* Returns whether or not nodes without ids will result in an anonymous update or an object creation.
*
* @return whether or not nodes without ids will result in an anonymous update or an object creation
*/
public boolean isAnonymousUpdateEnabled() {
return _anonymousUpdateEnabled;
}
/**
* Sets whether or not duplicate objects are collapsed to just an id in the filtered graph.
* This only applies to filters used to render object graphs.
*
* @param deduplicationEnabled if true, duplicate objects are collapsed into ids
*/
public void setDeduplicationEnabled(boolean deduplicationEnabled) {
_deduplicationEnabled = deduplicationEnabled;
}
/**
* Returns whether or not duplicate objects are collapsed to just an id.
*
* @return whether or not duplicate objects are collapsed to just an id
*/
public boolean isDeduplicationEnabled() {
return _deduplicationEnabled;
}
/**
* Returns the filter for the given key, or creates a "nextBase" filter
* if there isn't one. This should usually only be called when you
* know exactly what you're doing, as this doesn't fully interpret
* include/exclude rules.
*
* @param key the key to lookup
* @return the key filter
*/
public ERXKeyFilter _filterForKey(ERXKey key) {
ERXKeyFilter filter = _includes.get(key);
if (filter == null) {
filter = createNextFilter();
}
return filter;
}
/**
* Returns the filter for the given key, or creates a "nextBase" filter
* if there isn't one. This should usually only be called when you
* know exactly what you're doing, as this doesn't fully interpret
* include/exclude rules.
*
* @param key the key to lookup
* @return the key filter
*/
public ERXKeyFilter _filterForKey(String key) {
return _filterForKey(new ERXKey(key));
}
/**
* Returns the included keys and the next filters they map to.
*
* @return the included keys and the next filters they map to
*/
public Map<ERXKey, ERXKeyFilter> includes() {
return _includes;
}
/**
* Returns the set of keys that are explicitly excluded.
*
* @return the set of keys that are explicitly excluded
*/
public NSSet<ERXKey> excludes() {
return _excludes;
}
/**
* Returns the set of relationships that are locked (i.e. cannot be replaced).
*
* @return the set of relationships that are locked (i.e. cannot be replaced)
*/
public NSSet<ERXKey> lockedRelationships() {
return _lockedRelationships;
}
/**
* Includes the given set of keys in this filter, wrapping them in ERXKey objects for you.
*
* @param keyNames the names of the keys to include
*/
public void include(String... keyNames) {
for (String keyName : keyNames) {
include(new ERXKey<Object>(keyName));
}
}
/**
* Includes the given key in this filter, wrapping it in an ERXKey object for you.
*
* @param keyName the key to include
* @param existingFilter the existing filter to use for this key
* @return the next filter
*/
public ERXKeyFilter include(String keyName, ERXKeyFilter existingFilter) {
return include(new ERXKey<Object>(keyName), existingFilter);
}
/**
* Includes the given set of keys in this filter.
*
* @param keys the keys to include
*/
public void include(ERXKey... keys) {
for (ERXKey key : keys) {
include(key);
}
}
/**
* Returns whether or not the given key is included in this filter.
*
* @param key the key to lookup
* @return whether or not the given key is included in this filter
*/
public boolean includes(ERXKey key) {
return _includes.containsKey(key);
}
/**
* Returns whether or not the given key is included in this filter.
*
* @param key the key to lookup
* @return whether or not the given key is included in this filter
*/
public boolean includes(String key) {
return _includes.containsKey(new ERXKey(key));
}
/**
* Includes the given key in this filter.
*
* @param key the key to include
* @return the next filter
*/
public ERXKeyFilter include(ERXKey key) {
return include(key, null);
}
/**
* Includes the given key in this filter.
*
* @param key the key to include
* @return the next filter
*/
public ERXKeyFilter include(String key) {
return include(new ERXKey(key));
}
/**
* Includes the given key in this filter.
*
* @param key the key to include
* @param existingFilter the existing filter to use for this key
* @return the next filter
*/
public ERXKeyFilter include(ERXKey key, ERXKeyFilter existingFilter) {
ERXKeyFilter filter;
String keyPath = key.key();
int dotIndex = keyPath.indexOf('.');
boolean useUnique = false;
if (dotIndex != -1 && keyPath.startsWith("@unique.")) {
keyPath = keyPath.substring(dotIndex + 1);
dotIndex = keyPath.indexOf('.');
useUnique = true;
key = new ERXKey(keyPath);
}
if (dotIndex == -1) {
if (existingFilter != null) {
_includes.put(key, existingFilter);
_excludes.removeObject(key);
filter = existingFilter;
}
else {
filter = _includes.get(key);
if (filter == null) {
filter = createNextFilter();
_includes.put(key, filter);
_excludes.removeObject(key);
}
}
filter.setDistinct(useUnique);
}
else {
ERXKeyFilter subFilter = include(new ERXKey(keyPath.substring(0, dotIndex)), null);
subFilter.setDistinct(useUnique);
filter = subFilter.include(new ERXKey(keyPath.substring(dotIndex + 1)), existingFilter);
}
return filter;
}
/**
* Returns whether or not the given key is excluded.
*
* @param key the key to lookup
* @return whether or not the given key is excluded
*/
public boolean excludes(ERXKey key) {
return _excludes.contains(key);
}
/**
* Returns whether or not the given key is excluded.
*
* @param key the key to lookup
* @return whether or not the given key is excluded
*/
public boolean excludes(String key) {
return excludes(new ERXKey(key));
}
/**
* Returns whether or not the given relationship is locked (i.e. value's attributes can be updated but not the relationship cannot be changed).
*
* @param key the key to lookup
* @return whether or not the given relationship is locked
*/
public boolean lockedRelationship(ERXKey key) {
return _lockedRelationships.contains(key);
}
/**
* Returns whether or not the given relationship is locked (i.e. value's attributes can be updated but not the relationship cannot be changed).
*
* @param key the key to lookup
* @return whether or not the given relationship is locked
*/
public boolean lockedRelationship(String key) {
return lockedRelationship(new ERXKey(key));
}
/**
* Locks the given relationship on this filter.
*
* @param keys the relationships to lock
*/
public void lockRelationship(ERXKey... keys) {
for (ERXKey key : keys) {
String keyPath = key.key();
int dotIndex = keyPath.indexOf('.');
if (dotIndex == -1) {
_lockedRelationships.addObject(key);
//_includes.removeObjectForKey(key);
}
else {
ERXKeyFilter subFilter = include(new ERXKey(keyPath.substring(0, dotIndex)));
subFilter.lockRelationship(new ERXKey(keyPath.substring(dotIndex + 1)));
}
}
}
/**
* Locks the given relationship on this filter.
*
* @param keys the relationships to lock
*/
public void lockRelationship(String... keys) {
for (String keyPath : keys) {
int dotIndex = keyPath.indexOf('.');
if (dotIndex == -1) {
_lockedRelationships.addObject(new ERXKey(keyPath));
//_includes.removeObjectForKey(key);
}
else {
ERXKeyFilter subFilter = include(new ERXKey(keyPath.substring(0, dotIndex)));
subFilter.lockRelationship(new ERXKey(keyPath.substring(dotIndex + 1)));
}
}
}
/**
* Excludes the given keys from this filter.
*
* @param keys the keys to exclude
*/
public void exclude(ERXKey... keys) {
for (ERXKey key : keys) {
String keyPath = key.key();
int dotIndex = keyPath.indexOf('.');
if (dotIndex == -1) {
_excludes.addObject(key);
_includes.remove(key);
}
else {
ERXKeyFilter subFilter = include(new ERXKey(keyPath.substring(0, dotIndex)));
subFilter.exclude(new ERXKey(keyPath.substring(dotIndex + 1)));
}
}
}
/**
* Excludes the given keys from this filter.
*
* @param keys the keys to exclude
*/
public void exclude(String... keys) {
for (String keyPath : keys) {
int dotIndex = keyPath.indexOf('.');
if (dotIndex == -1) {
ERXKey key = new ERXKey(keyPath);
_excludes.addObject(key);
_includes.remove(key);
}
else {
ERXKeyFilter subFilter = include(new ERXKey(keyPath.substring(0, dotIndex)));
subFilter.exclude(new ERXKey(keyPath.substring(dotIndex + 1)));
}
}
}
/**
* Restricts this filter to only allow the given keys.
*
* @param keys the keys to restrict to
*/
public void only(ERXKey... keys) {
_base = ERXKeyFilter.Base.None;
_includes.clear();
_excludes.clear();
for (ERXKey key : keys) {
include(key);
}
}
/**
* Restricts this filter to only allow the given keys.
*
* @param keys the keys to restrict to
*/
public void only(String... keys) {
_base = ERXKeyFilter.Base.None;
_includes.clear();
_excludes.clear();
for (String key : keys) {
include(key);
}
}
/**
* Restricts this filter to only allow the given key.
*
* @param key the only key to allow
* @return the next filter
*/
public ERXKeyFilter only(ERXKey key) {
_base = ERXKeyFilter.Base.None;
_includes.clear();
_excludes.clear();
return include(key);
}
/**
* Restricts this filter to only allow the given key.
*
* @param key the only key to allow
* @return the next filter
*/
public ERXKeyFilter only(String key) {
_base = ERXKeyFilter.Base.None;
_includes.clear();
_excludes.clear();
return include(key);
}
/**
* Sets the sort orderings that will be applied by this key filter. The actual meaning of this
* is up to the code that applies this key filter to an object graph. A common example would be
* if you want to selectively sort the results of a to-many relationship that this filter
* is applied to.
*
* @param sortOrderings the sort orderings that will be applied by this key filter
*/
public void setSortOrderings(NSArray<EOSortOrdering> sortOrderings) {
_sortOrderings = sortOrderings;
}
/**
* Returns the sort orderings that will be applied by this key filter.
*
* @return the sort orderings that will be applied by this key filter
*/
public NSArray<EOSortOrdering> sortOrderings() {
return _sortOrderings;
}
/**
* Sets whether or not unknown keys are ignored rather than throwing an unknown key exception.
*
* @param unknownKeyIgnored if true, unknown keys are ignored
*/
public void setUnknownKeyIgnored(boolean unknownKeyIgnored) {
_unknownKeyIgnored = unknownKeyIgnored;
}
/**
* Returns whether or not unknown keys are ignored rather than throwing an unknown key exception.
*
* @return whether or not unknown keys are ignored rather than throwing an unknown key exception
*/
public boolean isUnknownKeyIgnored() {
return _unknownKeyIgnored;
}
/**
* Sets whether or not a to-many relationship should return only distinct objects.
*
* @param distinct if <code>true</code> and the key represents a to-many only distinct objects
* will be returned
* @return this filter
*/
public ERXKeyFilter setDistinct(boolean distinct) {
_distinct = distinct;
return this;
}
/**
* Returns whether or not a to-many relationship should return only distinct objects.
*
* @return whether or not a to-many relationship should return only distinct objects.
*/
public boolean isDistinct() {
return _distinct;
}
/**
* Returns whether or not the given key (of the given type, if known) is included in this filter.
*
* @param key the key to lookup
* @param type the type of the key (if known)
* @return whether or not this filter matches the key
*/
public boolean matches(ERXKey key, ERXKey.Type type) {
boolean matches = false;
if (includes(key) && !excludes(key)) {
matches = true;
}
else if (_base == ERXKeyFilter.Base.None) {
matches = includes(key) && !excludes(key);
}
else if (_base == ERXKeyFilter.Base.Attributes) {
if (type == ERXKey.Type.Attribute) {
matches = includes(key) || !excludes(key);
}
}
else if (_base == ERXKeyFilter.Base.AttributesAndToOneRelationships) {
if (type == ERXKey.Type.Attribute || type == ERXKey.Type.ToOneRelationship) {
matches = includes(key) || !excludes(key);
}
}
else if (_base == ERXKeyFilter.Base.All) {
matches = !excludes(key);
}
else {
throw new IllegalArgumentException("Unknown base '" + _base + "'.");
}
return matches;
}
/**
* Returns whether or not the given key (of the given type, if known) is included in this filter.
*
* @param key the key to lookup
* @param type the type of the key (if known)
* @return whether or not this filter matches the key
*/
public boolean matches(String key, ERXKey.Type type) {
return matches(new ERXKey(key), type);
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
sb.append("[ERXKeyFilter: base=" + _base);
if (_distinct) {
sb.append("; distinct");
}
if (!_includes.isEmpty()) {
sb.append("; includes=" + _includes + "");
}
if (!_excludes.isEmpty()) {
sb.append("; excludes=" + _excludes);
}
if (!_lockedRelationships.isEmpty()) {
sb.append("; excludesRelationships=" + _lockedRelationships);
}
sb.append(']');
return sb.toString();
}
/**
* ERXKeyFilter.Delegate defines an interface for receiving notifications when your
* filter is applied to an object graph. This gives you the opportunity to do some
* validation and security checks for more complex scenarios.
*
* @author mschrag
*/
public interface Delegate {
/**
* Called prior to pushing the given value into obj.key.
*
* @param target the target object
* @param value the value it will be set on
* @param key the key that will be set
* @throws SecurityException if you shouldn't be doing this
*/
public void willTakeValueForKey(Object target, Object value, String key) throws SecurityException;
/**
* Called after pushing the given value into obj.key. Most filters will be applied
* to EO's inside an editing context, and it may be more convenient to do security validation
* after the fact (before commit) than enforcing it in willTakeValue. This is your chance.
*
* @param target the target object
* @param value the value that was set
* @param key the key that was set
* @throws SecurityException if someone was naughty
*/
public void didTakeValueForKey(Object target, Object value, String key) throws SecurityException;
/**
* Called after skipping a key. You could choose to enforce more strict security and
* throw an exception in this case (rather than a silent skip default behavior).
*
* @param target the target object
* @param value the value that was skipped
* @param key the key that was skipped
* @throws SecurityException if someone was naughty
*/
public void didSkipValueForKey(Object target, Object value, String key) throws SecurityException;
}
}