package er.rest; import java.util.ArrayList; import java.util.Enumeration; import java.util.HashMap; import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.LinkedList; import java.util.List; import java.util.Map; import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.webobjects.eoaccess.EOEntityClassDescription; import com.webobjects.eocontrol.EOClassDescription; import com.webobjects.eocontrol.EOEnterpriseObject; import com.webobjects.eocontrol.EOSortOrdering; import com.webobjects.foundation.NSArray; import com.webobjects.foundation.NSKeyValueCoding; import com.webobjects.foundation.NSKeyValueCodingAdditions; import com.webobjects.foundation.NSMutableArray; import com.webobjects.foundation.NSMutableDictionary; import er.extensions.appserver.ERXResponse; import er.extensions.eof.ERXKey; import er.extensions.eof.ERXKeyFilter; import er.extensions.foundation.ERXArrayUtilities; import er.extensions.foundation.ERXProperties; import er.rest.format.ERXRestFormat; import er.rest.format.ERXWORestResponse; import er.rest.format.IERXRestWriter; /** * ERXRestRequestNode provides a model of a REST request. Because the incoming document format can vary (XML, JSON, * etc), we needed a document model that is more abstract than just an org.w3c.dom. Or, rather, one that isn't obnoxious * to use. * * @property <code>ERXRest.includeNullValues</code> Boolean property to enable null values in return. Defaults * to true. * @author mschrag */ public class ERXRestRequestNode implements NSKeyValueCoding, NSKeyValueCodingAdditions { private static final Logger log = LoggerFactory.getLogger(ERXRestRequestNode.class); private boolean _array; private String _name; private boolean _rootNode; private Object _value; private Map<String, Object> _attributes; private NSMutableArray<ERXRestRequestNode> _children; private Object _associatedObject; private Object _id; private String _type; private boolean _null; /** * Constructs a new root node with no name. */ public ERXRestRequestNode() { this(null, true); } /** * Construct a node with the given name * * @param name * the name of this node * @param rootNode * if true, the node is the root of a graph */ public ERXRestRequestNode(String name, boolean rootNode) { _name = name; _rootNode = rootNode; _attributes = new LinkedHashMap<>(); _children = new NSMutableArray<>(); guessNull(); } /** * Construct a node with the given name and value. * * @param name * the name of this node * @param rootNode * if true, the node is the root of a graph * @param value * the value of this node */ public ERXRestRequestNode(String name, Object value, boolean rootNode) { this(name, rootNode); _value = value; guessNull(); } /** * Clones this node. * * @return a clone of this node */ public ERXRestRequestNode cloneNode() { ERXRestRequestNode cloneNode = new ERXRestRequestNode(_name, _rootNode); cloneNode._attributes.putAll(_attributes); cloneNode._children.addObjectsFromArray(_children); cloneNode._value = _value; cloneNode._associatedObject = _associatedObject; cloneNode._array = _array; cloneNode._type = _type; cloneNode._id = _id; cloneNode._null = _null; return cloneNode; } /** * Sets whether or not this is a root node (a root node is one that would typically have a node name that is an * entity name -- the actual root, or elements in an array, for instance). * * @param rootNode * whether or not this is a root node */ public void setRootNode(boolean rootNode) { _rootNode = rootNode; } /** * Returns whether or not this is a root node (a root node is one that would typically have a node name that is an * entity name -- the actual root, or elements in an array, for instance). * * @return whether or not this is a root node */ public boolean isRootNode() { return _rootNode; } /** * Returns the Java object that corresponds to this node hierarchy. * * @param delegate * the format delegate to notify during rendering * * @return the Java object that corresponds to this node hierarchy */ public Object toJavaCollection(ERXRestFormat.Delegate delegate) { return toJavaCollection(delegate, null, new HashMap<Object, Object>()); } /** * Returns the Java object that corresponds to this node hierarchy. * * @param delegate * the format delegate to notify during rendering * @param conversionMap * the conversion map to use to record object => request node mappings * * @return the Java object that corresponds to this node hierarchy */ public Object toJavaCollection(ERXRestFormat.Delegate delegate, Map<Object, ERXRestRequestNode> conversionMap) { return toJavaCollection(delegate, conversionMap, new HashMap<Object, Object>()); } /** * Returns the Java object that corresponds to this node hierarchy. * * @param delegate * the format delegate to notify during rendering * @param conversionMap * the conversion map to use to record object => request node mappings * @param associatedObjects * the associatedObjects map (to prevent infinite loops) * @return the Java object that corresponds to this node hierarchy */ protected Object toJavaCollection(ERXRestFormat.Delegate delegate, Map<Object, ERXRestRequestNode> conversionMap, Map<Object, Object> associatedObjects) { Object result = associatedObjects.get(_associatedObject); if (result == null) { if (delegate != null) { delegate.nodeWillWrite(this); } if (isArray()) { List<Object> array = new LinkedList<>(); for (ERXRestRequestNode child : _children) { array.add(child.toJavaCollection(delegate, conversionMap, associatedObjects)); } result = array; } else if (isNull()) { result = null; } else if (_value != null) { result = _value; } else { Map<Object, Object> dict = new LinkedHashMap<>(); for (Map.Entry<String, Object> attribute : _attributes.entrySet()) { String key = attribute.getKey(); Object value = attribute.getValue(); // if (value != null) { dict.put(key, value); // } } for (ERXRestRequestNode child : _children) { Object value = child.toJavaCollection(delegate, conversionMap, associatedObjects); // MS: name has to be after toJavaCollection, because the naming delegate could rename it ... little // sketchy, i know String name = child.name(); if (value != null || ERXProperties.booleanForKeyWithDefault("ERXRest.includeNullValues", true)) { dict.put(name, value); } } if (dict.isEmpty()) { result = null; } else { result = dict; } } if (_associatedObject != null) { associatedObjects.put(_associatedObject, result); } if (conversionMap != null && result != null) { conversionMap.put(result, this); } } return result; } /** * Returns the NSCollection/Java object that corresponds to this node hierarchy. * * @return the NSCollection/Java object that corresponds to this node hierarchy */ public Object toNSCollection(ERXRestFormat.Delegate delegate) { return toNSCollection(delegate, new NSMutableDictionary<>()); } /** * Returns the NSCollection/Java object that corresponds to this node hierarchy. * * @param associatedObjects * the associatedObjects map (to prevent infinite loops) * @return NSCollection/Java object that corresponds to this node hierarchy */ protected Object toNSCollection(ERXRestFormat.Delegate delegate, NSMutableDictionary<Object, Object> associatedObjects) { Object result = associatedObjects.get(_associatedObject); if (result == null) { if (delegate != null) { delegate.nodeWillWrite(this); } if (isArray()) { NSMutableArray<Object> array = new NSMutableArray<>(); for (ERXRestRequestNode child : _children) { array.add(child.toNSCollection(delegate, associatedObjects)); } result = array; } else if (isNull()) { result = NSKeyValueCoding.NullValue; } else if (_value != null) { result = _value; } else { NSMutableDictionary<Object, Object> dict = new NSMutableDictionary<>(); for (Map.Entry<String, Object> attribute : _attributes.entrySet()) { String key = attribute.getKey(); Object value = attribute.getValue(); if (value == null) { value = NSKeyValueCoding.NullValue; } // if (value != null) { dict.put(key, value); // } } for (ERXRestRequestNode child : _children) { String name = child.name(); Object value = child.toNSCollection(delegate, associatedObjects); if (value != NSKeyValueCoding.NullValue || ERXProperties.booleanForKeyWithDefault("ERXRest.includeNullValues", true)) { dict.put(name, value); } } if (dict.isEmpty()) { result = NSKeyValueCoding.NullValue; } else { result = dict; } } if (_associatedObject != null) { associatedObjects.put(_associatedObject, result); } } return result; } /** * Sets whether or not this node represents an array or to-many relationship. * * @param array * whether or not this node represents an array or to-many relationship */ public void setArray(boolean array) { _array = array; guessNull(); } /** * Return whether or not this node represents an array or to-many relationship. * * @return whether or not this node represents an array or to-many relationship */ public boolean isArray() { return _array; } /** * Sets the original object associated with this node. * * @param associatedObject * the original object associated with this node */ public void setAssociatedObject(Object associatedObject) { _associatedObject = associatedObject; } /** * Returns the original object associated with this node. * * @return the original object associated with this node */ public Object associatedObject() { return _associatedObject; } /** * A parsed keypath segment that can contain either a name or a name and an index */ private static class Key { public final String _name; public final int _index; public Key(String name, int index) { _name = name; _index = index; } /** * Parses "keyName" or "keyName[x]" format keys. * * @param keySegment the segment of a keypath to parse * @return a Key object */ public static Key parse(String keySegment) { String key = keySegment; int keyIndex = -1; int closeBracketIndex = key.lastIndexOf(']'); if (closeBracketIndex != -1) { int openBracketIndex = key.lastIndexOf('['); if (openBracketIndex != -1) { keyIndex = Integer.valueOf(key.substring(openBracketIndex + 1, closeBracketIndex)); key = key.substring(0, openBracketIndex); } } return new Key(key, keyIndex); } } @Override public void takeValueForKey(Object value, String keyName) { if (value instanceof ERXRestRequestNode) { removeAttributeForKey(keyName); removeChildNamed(keyName); ((ERXRestRequestNode)value).setName(keyName); addChild((ERXRestRequestNode)value); } else if (_attributes.containsKey(keyName)) { _attributes.put(keyName, value); } else { Key key = Key.parse(keyName); ERXRestRequestNode child = childNamed(key._name); if (child == null) { if (key._index != -1) { child = new ERXRestRequestNode(key._name, null, false); addChild(child); child.childAtIndex(key._index).setValue(value); } else { addChild(new ERXRestRequestNode(key._name, value, false)); } //throw new NSKeyValueCoding.UnknownKeyException("There is no key named '" + key + "' on this node.", this, key); } else if (key._index != -1) { child.childAtIndex(key._index).setValue(value); } else { throw new IllegalArgumentException("Unable to set the value of '" + key._name + "' to " + value + "."); } } } @Override public Object valueForKey(String keyName) { Object value; if (_attributes.containsKey(keyName)) { value = _attributes.get(keyName); } else { Key key = Key.parse(keyName); ERXRestRequestNode child = childNamed(key._name); if (child == null) { throw new NSKeyValueCoding.UnknownKeyException("There is no key named '" + key._name + "' on this node.", this, key._name); } else if (key._index != -1) { if (child.children().count() <= key._index) { throw new NSKeyValueCoding.UnknownKeyException("There is no key named '" + key._name + "' with a child index " + key._index + " on this node.", this, key._name); } else { ERXRestRequestNode indexChild = child.children().objectAtIndex(key._index); if (indexChild.children().count() == 0) { value = indexChild.value(); } else { value = indexChild; } } } else if (child.children().size() == 0) { value = child.value(); } else { value = child; } } return value; } @Override public Object valueForKeyPath(String keyPath) { return NSKeyValueCodingAdditions.DefaultImplementation.valueForKeyPath(this, keyPath); } @Override public void takeValueForKeyPath(Object value, String keyPath) { if (keyPath == null) { throw new IllegalArgumentException("Key path cannot be null"); } int separatorIndex = keyPath.indexOf(NSKeyValueCodingAdditions._KeyPathSeparatorChar); if (separatorIndex != -1) { Key key = Key.parse(keyPath.substring(0, separatorIndex)); ERXRestRequestNode child = childNamed(key._name); if (child == null) { child = new ERXRestRequestNode(key._name, false); addChild(child); } String nextKeyPath = keyPath.substring(separatorIndex + 1); if (key._index == -1) { child.takeValueForKeyPath(value, nextKeyPath); } else { child.childAtIndex(key._index).takeValueForKeyPath(value, nextKeyPath); } } else { takeValueForKey(value, keyPath); } } public ERXRestRequestNode childAtIndex(int index) { int childCount = _children.count(); if (childCount <= index) { setArray(true); for (int i = childCount; i <= index; i ++) { addChild(new ERXRestRequestNode(null, false)); } } return _children.objectAtIndex(index); } /** * Returns the first child named 'name'. * * @param name * the name to look for * @return the first child with this name (or null if not found) */ public ERXRestRequestNode childNamed(String name) { ERXRestRequestNode matchingChildNode = null; Enumeration childrenEnum = _children.objectEnumerator(); while (matchingChildNode == null && childrenEnum.hasMoreElements()) { ERXRestRequestNode childNode = (ERXRestRequestNode) childrenEnum.nextElement(); if (name.equals(childNode.name())) { matchingChildNode = childNode; } } return matchingChildNode; } /** * Removes the child name that has the given name. * * @param name * the name of the node to remove * @return the node that was removed */ public ERXRestRequestNode removeChildNamed(String name) { ERXRestRequestNode node = childNamed(name); if (node != null) { _children.remove(node); } return node; } /** * Sets the type of this node (type as in the Class that it represents). * * @param type * the type of this node */ public void setType(String type) { _type = type; } /** * Returns the type of this node (type as in the Class that it represents). * * @return the type of this node */ public String type() { return _type; } /** * Sets the ID associated with this node. * * @param id * the ID associated with this node */ public void setID(Object id) { _id = id; guessNull(); } /** * Returns the ID associated with this node. * * @return the ID associated with this node */ public Object id() { return _id; } /** * Removes the attribute or child node that has the given name (and returns it). * * @param name * the name of the attribute or node to remove * @return the removed attribute value */ public Object removeAttributeOrChildNodeNamed(String name) { Object value = removeAttributeForKey(name); if (value == null) { ERXRestRequestNode childNode = removeChildNamed(name); if (childNode != null) { value = childNode.value(); } } return value; } /** * Returns the type of this node. * * @param name * the attribute or node name * @return the type of this node */ public Object attributeOrChildNodeValue(String name) { Object value = attributeForKey(name); if (value == null) { ERXRestRequestNode typeNode = childNamed(name); if (typeNode != null) { value = typeNode.value(); } } return value; } protected void guessNull() { setNull(_value == null && _children.size() == 0 && _id == null && !isArray() && _associatedObject == null); } /** * Sets whether or not this node represents a null value. * * @param isNull * whether or not this node represents a null value */ public void setNull(boolean isNull) { _null = isNull; } /** * Returns whether or not this node represents a null value. * * @return true whether or not this node represents a null value */ public boolean isNull() { return _null; } /** * Returns the name of this node. * * @return the name of this node */ public String name() { return _name; } /** * Sets the name of this node. * * @param name * the name of this node */ public void setName(String name) { _name = name; } /** * Returns the value for this node (or null if it doesn't exist). * * @return the name of this node */ public Object value() { return _value; } /** * Sets the value for this node. * * @param value * the value for this node */ public void setValue(Object value) { if (value instanceof NSKeyValueCoding.Null) { _value = null; } else { _value = value; } guessNull(); } /** * Sets the attribute value for the given key. * * @param attribute * the attribute value * @param key * the key */ public void setAttributeForKey(Object attribute, String key) { _attributes.put(key, attribute); // if (!"nil".equals(key)) { guessNull(); // } } /** * Removes the attribute that has the given name. * * @param key * the name of the attribute to remove * @return the attribute value */ public Object removeAttributeForKey(String key) { Object attribute = _attributes.remove(key); return attribute; } /** * Returns the attribute value for the given key. * * @param key * the key * @return the attribute value */ public Object attributeForKey(String key) { return _attributes.get(key); } /** * Returns the attributes dictionary for this node. * * @return the attributes dictionary */ public Map<String, Object> attributes() { return _attributes; } /** * Adds a child to this node. * * @param child * the child to add */ public void addChild(ERXRestRequestNode child) { _children.addObject(child); guessNull(); } /** * Returns the children of this node. * * @return the children of this node */ public NSArray<ERXRestRequestNode> children() { return _children; } @Override public String toString() { StringBuffer sb = new StringBuffer(); toString(sb, 0); return sb.toString(); } protected void toString(StringBuffer sb, int depth) { for (int i = 0; i < depth; i++) { sb.append(" "); } sb.append('['); sb.append(_name); if (_id != null || _type != null) { if (_id != null) { sb.append(" id=" + _id); } if (_type != null) { sb.append(" type=" + _type); } } if (!_attributes.isEmpty()) { sb.append(' '); sb.append(_attributes); } if (_value != null) { sb.append('='); sb.append(_value); } if (!_children.isEmpty()) { sb.append('\n'); for (ERXRestRequestNode child : _children) { child.toString(sb, depth + 1); } for (int i = 0; i < depth; i++) { sb.append(" "); } } sb.append(']'); if (depth > 0) { sb.append('\n'); } } protected String entityName(String suggestedEntityName) { String entityName = suggestedEntityName; if (entityName == null) { entityName = type(); if (entityName == null && value() == null) { if (isArray()) { entityName = "NSMutableArray"; } else { entityName = "NSDictionary"; } } } return entityName; } /** * Equivalent to objectWithFilter(null, ERXKeyFilter.filterWithAllRecursive(), new ERXRestContext()); * * @return the object that this request node represents */ public Object object() { return objectWithFilter(null, ERXKeyFilter.filterWithAllRecursive(), new ERXRestContext()); } /** * Returns the object that this request node represents. * * @param entityName * the entity name of the object to use * @param keyFilter * the filter to use for determining which keys can be updated (or null for no update) * @param context * the REST context * @return the object that this request node represents */ public Object objectWithFilter(String entityName, ERXKeyFilter keyFilter, ERXRestContext context) { Object obj; if (isArray()) { NSMutableArray<Object> objs = new NSMutableArray<>(); for (ERXRestRequestNode childNode : children()) { Object child = childNode.objectWithFilter(entityName, ERXKeyFilter.filterWithAllRecursive(), context); if (child != null) { objs.addObject(child); } } obj = objs; } else { String finalEntityName = entityName(entityName); EOClassDescription classDescription = ERXRestClassDescriptionFactory.classDescriptionForEntityName(entityName); if (classDescription == null) { throw new IllegalArgumentException("There is no registered entity with the name '" + finalEntityName + "'."); } obj = IERXRestDelegate.Factory.delegateForClassDescription(classDescription).objectOfEntityWithID(classDescription, id(), context); if (keyFilter != null) { updateObjectWithFilter(obj, keyFilter, context); } } return obj; } /** * Equivalent to createObjectWithFilter(null, ERXKeyFilter.filterWithAllRecursive(), new ERXRestContext()); * * @return a new instance of an object represented by this request node */ public Object createObject() { return createObjectWithFilter(null, ERXKeyFilter.filterWithAllRecursive(), new ERXRestContext()); } /** * Creates a new instance of an object represented by this request node. * * @param entityName * the entity name of the object to use * @param keyFilter * the filter to use for determining which keys can be updated (or null for no update) * @param context * the REST context * @return a new instance of an object represented by this request node */ public Object createObjectWithFilter(String entityName, ERXKeyFilter keyFilter, ERXRestContext context) { // MS: if it's a null node, just hand back a null if (isNull()) { return null; } String finalEntityName = entityName(entityName); // MS: if there is no type, just return the value of this object if (finalEntityName == null) { return value(); } EOClassDescription classDescription = ERXRestClassDescriptionFactory.classDescriptionForEntityName(finalEntityName); if (classDescription == null) { throw new IllegalArgumentException("There is no registered entity with the name '" + finalEntityName + "'."); } Object obj = IERXRestDelegate.Factory.delegateForClassDescription(classDescription).createObjectOfEntityWithID(classDescription, id(), context); if (keyFilter != null) { updateObjectWithFilter(obj, keyFilter, context); } return obj; } protected void _addAttributeNodeForKeyInObject(ERXKey<?> key, Object obj, ERXKeyFilter keyFilter) { ERXRestRequestNode attributeNode = new ERXRestRequestNode(keyFilter.keyMap(key).key(), false); attributeNode.setValue(key.valueInObject(obj)); addChild(attributeNode); } protected void _addToManyRelationshipNodeForKeyOfEntityInObject(ERXKey<?> key, EOClassDescription destinationEntity, Object obj, ERXKeyFilter keyFilter, ERXRestContext context, Set<Object> visitedObjects) { ERXRestRequestNode toManyRelationshipNode = new ERXRestRequestNode(keyFilter.keyMap(key).key(), false); toManyRelationshipNode.setArray(true); toManyRelationshipNode.setType(destinationEntity.entityName()); List<?> childrenObjects = (List) key.valueInObject(obj); ERXKeyFilter childFilter = keyFilter._filterForKey(key); if (childFilter.isDistinct()) { if (childrenObjects instanceof NSArray) { childrenObjects = ERXArrayUtilities.distinct((NSArray<?>) childrenObjects); } else { childrenObjects = new ArrayList(new HashSet(childrenObjects)); } } NSArray<EOSortOrdering> sortOrderings = childFilter.sortOrderings(); if (sortOrderings != null && sortOrderings.count() > 0) { if (childrenObjects instanceof NSArray) { childrenObjects = EOSortOrdering.sortedArrayUsingKeyOrderArray((NSArray<?>)childrenObjects, sortOrderings); } else { log.warn("Skipping sort orderings for '{}' on {} because sort orderings are only supported for NSArrays.", key, obj); } } for (Object childObj : childrenObjects) { ERXRestRequestNode childNode = new ERXRestRequestNode(null, false); childNode._fillInWithObjectAndFilter(childObj, destinationEntity, childFilter, context, visitedObjects); toManyRelationshipNode.addChild(childNode); } addChild(toManyRelationshipNode); } protected void _addToOneRelationshipNodeForKeyInObject(ERXKey<?> key, Object obj, EOClassDescription destinationEntity, ERXKeyFilter keyFilter, ERXRestContext context, Set<Object> visitedObjects) { Object value = key.valueInObject(obj); // if (value != null) { ERXRestRequestNode toOneRelationshipNode = new ERXRestRequestNode(keyFilter.keyMap(key).key(), false); toOneRelationshipNode._fillInWithObjectAndFilter(value, destinationEntity, keyFilter._filterForKey(key), context, visitedObjects); addChild(toOneRelationshipNode); // } } protected void _addAttributesAndRelationshipsForObjectOfEntity(Object obj, EOClassDescription classDescription, ERXKeyFilter keyFilter, ERXRestContext context, Set<Object> visitedObjects) { // just break out ... no key filter = nothing to do if (keyFilter == null) { return; } Set<ERXKey> visitedKeys = new HashSet<>(); for (String attributeName : classDescription.attributeKeys()) { // if (attribute.isClassProperty()) { ERXKey<Object> key = new ERXKey<>(attributeName); if (keyFilter.matches(key, ERXKey.Type.Attribute)) { _addAttributeNodeForKeyInObject(key, obj, keyFilter); visitedKeys.add(key); } // } } for (String relationshipName : classDescription.toOneRelationshipKeys()) { // if (relationship.isClassProperty()) { ERXKey<Object> key = new ERXKey<>(relationshipName); if (keyFilter.matches(key, ERXKey.Type.ToOneRelationship)) { _addToOneRelationshipNodeForKeyInObject(key, obj, classDescription.classDescriptionForDestinationKey(relationshipName), keyFilter, context, visitedObjects); visitedKeys.add(key); } // } } for (String relationshipName : classDescription.toManyRelationshipKeys()) { // if (relationship.isClassProperty()) { ERXKey<Object> key = new ERXKey<>(relationshipName); if (keyFilter.matches(key, ERXKey.Type.ToManyRelationship)) { _addToManyRelationshipNodeForKeyOfEntityInObject(key, classDescription.classDescriptionForDestinationKey(relationshipName), obj, keyFilter, context, visitedObjects); visitedKeys.add(key); } // } } Set<ERXKey> includeKeys = keyFilter.includes().keySet(); if (includeKeys != null && !includeKeys.isEmpty()) { Set<ERXKey> remainingKeys = new LinkedHashSet<>(includeKeys); remainingKeys.removeAll(visitedKeys); if (!remainingKeys.isEmpty()) { // this is sort of expensive, but we want to support non-eomodel to-many relationships on EO's, so // we fallback and lookup the class entity ... if (classDescription instanceof EOEntityClassDescription) { // EOEntityClassDescription.classDescriptionForEntityName(obj.getClass().getName()); EOClassDescription nonModelClassDescription = ERXRestClassDescriptionFactory.classDescriptionForObject(obj, true); for (ERXKey<?> remainingKey : remainingKeys) { String keyName = remainingKey.key(); if (nonModelClassDescription.attributeKeys().containsObject(keyName)) { _addAttributeNodeForKeyInObject(remainingKey, obj, keyFilter); } else if (nonModelClassDescription.toManyRelationshipKeys().containsObject(keyName)) { _addToManyRelationshipNodeForKeyOfEntityInObject(remainingKey, nonModelClassDescription.classDescriptionForDestinationKey(keyName), obj, keyFilter, context, visitedObjects); } else if (nonModelClassDescription.toOneRelationshipKeys().containsObject(keyName)) { _addToOneRelationshipNodeForKeyInObject(remainingKey, obj, nonModelClassDescription.classDescriptionForDestinationKey(keyName), keyFilter, context, visitedObjects); } else if (nonModelClassDescription instanceof BeanInfoClassDescription && ((BeanInfoClassDescription) nonModelClassDescription).isAttributeMethod(keyName)) { _addAttributeNodeForKeyInObject(remainingKey, obj, keyFilter); } else if (nonModelClassDescription instanceof BeanInfoClassDescription && ((BeanInfoClassDescription) nonModelClassDescription).isToManyMethod(keyName)) { _addToManyRelationshipNodeForKeyOfEntityInObject(remainingKey, nonModelClassDescription.classDescriptionForDestinationKey(keyName), obj, keyFilter, context, visitedObjects); } else if (nonModelClassDescription instanceof BeanInfoClassDescription && ((BeanInfoClassDescription) nonModelClassDescription).isToOneMethod(keyName)) { _addToOneRelationshipNodeForKeyInObject(remainingKey, obj, nonModelClassDescription.classDescriptionForDestinationKey(keyName), keyFilter, context, visitedObjects); } else if (!keyFilter.isUnknownKeyIgnored()) { throw new IllegalArgumentException("This key filter specified that the key '" + keyName + "' should be included on '" + nonModelClassDescription.entityName() + "', but it does not exist."); } } } else if (classDescription instanceof BeanInfoClassDescription) { BeanInfoClassDescription beanInfoClassDescription = (BeanInfoClassDescription) classDescription; for (ERXKey<?> remainingKey : remainingKeys) { String keyName = remainingKey.key(); if (beanInfoClassDescription.isAttributeMethod(keyName)) { _addAttributeNodeForKeyInObject(remainingKey, obj, keyFilter); } else if (beanInfoClassDescription.isToManyMethod(keyName)) { _addToManyRelationshipNodeForKeyOfEntityInObject(remainingKey, beanInfoClassDescription.classDescriptionForDestinationKey(keyName), obj, keyFilter, context, visitedObjects); } else if (beanInfoClassDescription.isToOneMethod(keyName)) { _addToOneRelationshipNodeForKeyInObject(remainingKey, obj, beanInfoClassDescription.classDescriptionForDestinationKey(keyName), keyFilter, context, visitedObjects); } else if (!keyFilter.isUnknownKeyIgnored()) { throw new IllegalArgumentException("This key filter specified that the key '" + keyName + "' should be included on '" + beanInfoClassDescription.entityName() + "', but it does not exist."); } } } else if (!keyFilter.isUnknownKeyIgnored()) { throw new IllegalArgumentException("This key filter specified that the keys '" + remainingKeys + "' should be included on '" + classDescription.entityName() + "', but they do not exist."); } } } } protected void _fillInWithObjectAndFilter(Object obj, EOClassDescription classDescription, ERXKeyFilter keyFilter, ERXRestContext context, Set<Object> visitedObjects) { if (obj instanceof List) { setAssociatedObject(obj); // setAttributeForKey(/* ??? */, ERXRestRequestNode.TYPE_KEY); setArray(true); for (Object childObj : (List) obj) { ERXRestRequestNode childNode = new ERXRestRequestNode(null, false); childNode._fillInWithObjectAndFilter(childObj, classDescription, keyFilter, context, visitedObjects); addChild(childNode); } } else if (ERXRestUtils.isPrimitive(obj)) { if (obj instanceof NSKeyValueCoding.Null) { setValue(null); setAssociatedObject(null); } else { if (_name == null && classDescription != null) { _name = classDescription.entityName(); } setValue(obj); setAssociatedObject(obj); } } else { // in case we have a superclass class description passed in if (obj != null) { classDescription = ERXRestClassDescriptionFactory.classDescriptionForObject(obj, false); } if (_name == null) { _name = classDescription.entityName(); _rootNode = true; } setAssociatedObject(obj); setType(classDescription.entityName()); if (obj != null) { Object id = IERXRestDelegate.Factory.delegateForClassDescription(classDescription).primaryKeyForObject(obj, context); if (id != null) { setID(id); } if (!visitedObjects.contains(obj) || !keyFilter.isDeduplicationEnabled()) { visitedObjects.add(obj); _addAttributesAndRelationshipsForObjectOfEntity(obj, classDescription, keyFilter, context, visitedObjects); } } } } /** * Returns a string representation of this request node using the given format. * * @param format * the format to use * @param context * the REST context * @return a string representation of this request node using the given format */ public String toString(ERXRestFormat format, ERXRestContext context) { return toString(format.writer(), format.delegate(), context); } /** * Returns a string representation of this request node using the given IERXRestWriter. * * @param writer * the writer to use * @param context * the REST context * @return a string representation of this request node using the given IERXRestWriter */ public String toString(IERXRestWriter writer, ERXRestFormat.Delegate delegate, ERXRestContext context) { ERXResponse octopusHair = new ERXResponse(); writer.appendToResponse(this, new ERXWORestResponse(octopusHair), delegate, context); return octopusHair.contentString(); } protected boolean isClassProperty(EOClassDescription classDescription, String key) { boolean isClassProperty = true; // IERXAttribute attribute = entity.attributeNamed(key); // if (attribute != null) { // isClassProperty = attribute.isClassProperty(); // } // else { // IERXRelationship relationship = entity.relationshipNamed(key); // if (relationship != null) { // isClassProperty = relationship.isClassProperty(); // } // } return isClassProperty; } protected void _safeWillTakeValueForKey(ERXKeyFilter keyFilter, Object target, Object value, String key) { ERXKeyFilter.Delegate delegate = keyFilter.delegate(); if (delegate != null) { delegate.willTakeValueForKey(target, value, key); } } protected void _safeDidTakeValueForKey(ERXKeyFilter keyFilter, Object target, Object value, String key) { ERXKeyFilter.Delegate delegate = keyFilter.delegate(); if (delegate != null) { delegate.didTakeValueForKey(target, value, key); } } protected void _safeDidSkipValueForKey(ERXKeyFilter keyFilter, Object target, Object value, String key) { ERXKeyFilter.Delegate delegate = keyFilter.delegate(); if (delegate != null) { delegate.didSkipValueForKey(target, value, key); } } /** * Equivalent to updateObjectWithFilter(obj, ERXKeyFilter.filterWithAllRecursive(), new ERXRestContext()); * * @param obj * the object to update */ public void updateObject(Object obj) { updateObjectWithFilter(obj, ERXKeyFilter.filterWithAllRecursive(), new ERXRestContext()); } /** * Updates the given object based on this request node. * * @param obj * the object to update * @param keyFilter * the filter to use to determine how to update * @param context * the REST context */ public void updateObjectWithFilter(Object obj, ERXKeyFilter keyFilter, ERXRestContext context) { if (obj == null) { return; } EOClassDescription classDescription = ERXRestClassDescriptionFactory.classDescriptionForObject(obj, false); for (Map.Entry<String, Object> attribute : _attributes.entrySet()) { ERXKey<Object> key = keyFilter.keyMap(new ERXKey<Object>(attribute.getKey())); String keyName = key.key(); if (keyFilter.matches(key, ERXKey.Type.Attribute) && isClassProperty(classDescription, keyName)) { Object value = ERXRestUtils.coerceValueToAttributeType(attribute.getValue(), null, obj, keyName, context); if (value instanceof NSKeyValueCoding.Null) { value = null; } _safeWillTakeValueForKey(keyFilter, obj, value, keyName); key.takeValueInObject(value, obj); _safeDidTakeValueForKey(keyFilter, obj, value, keyName); } else { _safeDidSkipValueForKey(keyFilter, obj, attribute.getValue(), keyName); // MS: we didn't coerce the // value .. i think that's ok } } for (ERXRestRequestNode childNode : _children) { ERXKey<Object> key = keyFilter.keyMap(new ERXKey<Object>(childNode.name())); String keyName = key.key(); if (isClassProperty(classDescription, keyName)) { NSKeyValueCoding._KeyBinding binding = NSKeyValueCoding.DefaultImplementation._keyGetBindingForKey(obj, keyName); Class<?> valueType = binding.valueType(); if (valueType == Object.class) { if (childNode.isArray()) { valueType = NSArray.class; } else { Object childValue = childNode.value(); if (childValue != null) { valueType = childValue.getClass(); } } } if (keyName == null && isArray()) { Object value = ERXRestUtils.coerceValueToTypeNamed(childNode.value(), valueType.getCanonicalName(), context, true); ((List<Object>)obj).add(value); } else if (List.class.isAssignableFrom(valueType) && keyFilter.matches(key, ERXKey.Type.ToManyRelationship)) { EOClassDescription destinationClassDescription; // this is sort of expensive, but we want to support non-eomodel to-many relationships on EO's, so // we fallback and lookup the class entity ... if (!classDescription.toManyRelationshipKeys().containsObject(keyName) && classDescription instanceof EOEntityClassDescription) { EOClassDescription nonModelClassDescription = ERXRestClassDescriptionFactory.classDescriptionForObject(obj, true); if (!nonModelClassDescription.toManyRelationshipKeys().containsObject(keyName)) { throw new IllegalArgumentException("There is no to-many relationship named '" + key.key() + "' on '" + classDescription.entityName() + "'."); } destinationClassDescription = classDescription.classDescriptionForDestinationKey(keyName); } else { destinationClassDescription = classDescription.classDescriptionForDestinationKey(keyName); } if (destinationClassDescription == null) { if (keyFilter.isUnknownKeyIgnored()) { continue; } else { throw new NSKeyValueCoding.UnknownKeyException("There is no key '" + keyName + "' on this object.", obj, keyName); } } boolean lockedRelationship = keyFilter.lockedRelationship(key); @SuppressWarnings("unchecked") List<Object> existingValues = (List<Object>) NSKeyValueCoding.DefaultImplementation.valueForKey(obj, keyName); Set<Object> removedValues; if (existingValues == null) { removedValues = new HashSet<>(); } else { removedValues = new HashSet<>(existingValues); } List<Object> newValues = new LinkedList<>(); List<Object> allValues = new LinkedList<>(); for (ERXRestRequestNode toManyNode : childNode.children()) { Object id = toManyNode.id(); if (toManyNode.type() != null) { destinationClassDescription = ERXRestClassDescriptionFactory.classDescriptionForEntityName(toManyNode.type()); } Object childObj; if (toManyNode.children().count() == 0 && ERXRestUtils.isPrimitive(toManyNode.value())) { if (lockedRelationship) { childObj = null; } else { if (toManyNode.value() != null) { childObj = toManyNode.value(); } else { childObj = IERXRestDelegate.Factory.delegateForClassDescription(destinationClassDescription).objectOfEntityWithID(destinationClassDescription, id, context); } } } else if (id == null) { if (lockedRelationship) { childObj = null; } else { childObj = IERXRestDelegate.Factory.delegateForClassDescription(destinationClassDescription).createObjectOfEntityWithID(destinationClassDescription, id, context); } } else { childObj = IERXRestDelegate.Factory.delegateForClassDescription(destinationClassDescription).objectOfEntityWithID(destinationClassDescription, id, context); } if (childObj != null) { boolean newMemberOfRelationship = existingValues == null || !existingValues.contains(childObj); if (newMemberOfRelationship) { if (!lockedRelationship) { toManyNode.updateObjectWithFilter(childObj, keyFilter._filterForKey(key), context); newValues.add(childObj); allValues.add(childObj); } } else { toManyNode.updateObjectWithFilter(childObj, keyFilter._filterForKey(key), context); allValues.add(childObj); } removedValues.remove(childObj); } } if (!lockedRelationship) { _safeWillTakeValueForKey(keyFilter, obj, allValues, keyName); if (obj instanceof EOEnterpriseObject) { for (Object removedValue : removedValues) { ((EOEnterpriseObject) obj).removeObjectFromBothSidesOfRelationshipWithKey((EOEnterpriseObject) removedValue, keyName); } for (Object newValue : newValues) { ((EOEnterpriseObject) obj).addObjectToBothSidesOfRelationshipWithKey((EOEnterpriseObject) newValue, keyName); } } else { key.takeValueInObject(allValues, obj); } _safeDidTakeValueForKey(keyFilter, obj, allValues, keyName); } else { _safeDidSkipValueForKey(keyFilter, obj, allValues, keyName); } } else if (!ERXRestUtils.isPrimitive(valueType) && keyFilter.matches(key, ERXKey.Type.ToOneRelationship)) { EOClassDescription destinationClassDescription; // this is sort of expensive, but we want to support non-eomodel to-one relationships on EO's, so // we fallback and lookup the class entity ... if (!classDescription.toOneRelationshipKeys().containsObject(keyName) && classDescription instanceof EOEntityClassDescription) { EOClassDescription nonModelClassDescription = ERXRestClassDescriptionFactory.classDescriptionForObject(obj, true); if (!nonModelClassDescription.toOneRelationshipKeys().containsObject(keyName)) { throw new IllegalArgumentException("There is no to-one relationship named '" + key.key() + "' on '" + classDescription.entityName() + "'."); } destinationClassDescription = nonModelClassDescription.classDescriptionForDestinationKey(keyName); } else { destinationClassDescription = classDescription.classDescriptionForDestinationKey(keyName); } if (destinationClassDescription == null) { if (keyFilter.isUnknownKeyIgnored()) { continue; } else { throw new NSKeyValueCoding.UnknownKeyException("There is no key '" + keyName + "' on this object.", obj, keyName); } } boolean lockedRelationship = keyFilter.lockedRelationship(key); if (childNode.isArray()) { throw new IllegalArgumentException("You attempted to pass an array of values for the key '" + key + "'."); } if (childNode.isNull()) { Object previousChildObj = NSKeyValueCoding.DefaultImplementation.valueForKey(obj, keyName); if (previousChildObj != null && !lockedRelationship) { _safeWillTakeValueForKey(keyFilter, obj, null, keyName); if (obj instanceof EOEnterpriseObject && previousChildObj instanceof EOEnterpriseObject) { ((EOEnterpriseObject) obj).removeObjectFromBothSidesOfRelationshipWithKey((EOEnterpriseObject) previousChildObj, keyName); } else { key.takeValueInObject(null, obj); } _safeDidTakeValueForKey(keyFilter, obj, null, keyName); } else if (lockedRelationship) { _safeDidSkipValueForKey(keyFilter, obj, null, keyName); } } else { Object id = childNode.id(); ERXKeyFilter childKeyFilter = keyFilter._filterForKey(key); Object childObj; if (childNode.type() != null) { destinationClassDescription = ERXRestClassDescriptionFactory.classDescriptionForEntityName(childNode.type()); } if (id == null) { if (lockedRelationship) { childObj = null; } else if (childKeyFilter.isAnonymousUpdateEnabled()) { childObj = NSKeyValueCoding.DefaultImplementation.valueForKey(obj, keyName); if (childObj == null) { childObj = IERXRestDelegate.Factory.delegateForClassDescription(destinationClassDescription).createObjectOfEntityWithID(destinationClassDescription, null, context); } } else { childObj = IERXRestDelegate.Factory.delegateForClassDescription(destinationClassDescription).createObjectOfEntityWithID(destinationClassDescription, null, context); } } else if ("_".equals(id)) { childObj = NSKeyValueCoding.DefaultImplementation.valueForKey(obj, keyName); if (!lockedRelationship && childObj == null) { childObj = IERXRestDelegate.Factory.delegateForClassDescription(destinationClassDescription).createObjectOfEntityWithID(destinationClassDescription, null, context); } } else { childObj = IERXRestDelegate.Factory.delegateForClassDescription(destinationClassDescription).objectOfEntityWithID(destinationClassDescription, id, context); } boolean updateChildObj; if (childObj == null) { updateChildObj = false; } else if (lockedRelationship) { Object previousChildObj = NSKeyValueCoding.DefaultImplementation.valueForKey(obj, keyName); updateChildObj = previousChildObj != null && previousChildObj.equals(childObj); } else { updateChildObj = true; } if (updateChildObj) { childNode.updateObjectWithFilter(childObj, childKeyFilter, context); if (!lockedRelationship) { _safeWillTakeValueForKey(keyFilter, obj, childObj, keyName); if (obj instanceof EOEnterpriseObject && childObj instanceof EOEnterpriseObject) { ((EOEnterpriseObject) obj).addObjectToBothSidesOfRelationshipWithKey((EOEnterpriseObject) childObj, keyName); } else { key.takeValueInObject(childObj, obj); } _safeDidTakeValueForKey(keyFilter, obj, childObj, keyName); } else { _safeDidSkipValueForKey(keyFilter, obj, childObj, keyName); } } } } else if (/* entity.attributeNamed(keyName) != null && */ERXRestUtils.isPrimitive(valueType) && keyFilter.matches(key, ERXKey.Type.Attribute)) { Object value = childNode.value(); if (value instanceof String) { value = ERXRestUtils.coerceValueToAttributeType(value, null, obj, keyName, context); } if (value instanceof NSKeyValueCoding.Null) { value = null; } _safeWillTakeValueForKey(keyFilter, obj, value, keyName); try { key.takeValueInObject(value, obj); } catch (NSKeyValueCoding.UnknownKeyException e) { if (!keyFilter.isUnknownKeyIgnored()) { throw e; } } _safeDidTakeValueForKey(keyFilter, obj, value, keyName); } else { // ignore key _safeDidSkipValueForKey(keyFilter, obj, childNode, keyName); // MS: what is the value here? i'm just // hanging in the node ... } } } } // MS: Totally debatable .... I may take this back out, but it makes things look prettier. public void _removeRedundantTypes() { String type = type(); if ("NSDictionary".equals(type) || "NSMutableDictionary".equals(type) || "HashMap".equals(type)) { setType(null); } NSArray<ERXRestRequestNode> children = children(); if (children != null) { for (ERXRestRequestNode child : children) { child._removeRedundantTypes(); } } } /** * Creates a hierarchy of ERXRestRequestNodes based off of the given array of objects. * * @param classDescription * the entity type of the objects in the array * @param objects * the array to turn into request nodes * @param keyFilter * the filter to use * @param context * the REST context * @return the root ERXRestRequestNode */ public static ERXRestRequestNode requestNodeWithObjectAndFilter(EOClassDescription classDescription, List<?> objects, ERXKeyFilter keyFilter, ERXRestContext context) { ERXRestRequestNode requestNode = new ERXRestRequestNode(null, true); if (classDescription != null) { String entityName = classDescription.entityName(); requestNode = new ERXRestRequestNode(entityName, true); requestNode.setType(entityName); } requestNode._fillInWithObjectAndFilter(objects, classDescription, keyFilter, context, new HashSet<>()); return requestNode; } /** * Creates a hierarchy of ERXRestRequestNodes based off of the given object. * * @param obj * the object to turn into request nodes * @param keyFilter * the filter to use * @param context * the REST context * @return the root ERXRestRequestNode */ public static ERXRestRequestNode requestNodeWithObjectAndFilter(Object obj, ERXKeyFilter keyFilter, ERXRestContext context) { String shortName = null; EOClassDescription classDescription = null; if (obj != null) { classDescription = ERXRestClassDescriptionFactory.classDescriptionForObject(obj, false); shortName = classDescription.entityName(); } ERXRestRequestNode requestNode = new ERXRestRequestNode(shortName, true); if (ERXRestUtils.isPrimitive(obj)) { requestNode.setValue(obj); } else { if (!(obj instanceof List)) { requestNode.setType(shortName); } requestNode._fillInWithObjectAndFilter(obj, classDescription, keyFilter, context, new HashSet<Object>()); } return requestNode; } }