From c49ebae2d7039bcebf82db8f8b3a3f75f48e86a9 Mon Sep 17 00:00:00 2001 From: Gavin Cornwell Date: Thu, 7 May 2009 21:19:22 +0000 Subject: [PATCH] - Improved handling of associations in NodeHandler - Child association data now correctly returned - Added support for duplicate named properties and associations i.e. those referred to using the prop: or assoc: prefixes git-svn-id: https://svn.alfresco.com/repos/alfresco-enterprise/alfresco/HEAD/root@14239 c4b6b30b-aa2e-2d43-bbcb-ca4b014f7261 --- .../repo/forms/processor/NodeHandler.java | 507 ++++++++---------- 1 file changed, 232 insertions(+), 275 deletions(-) diff --git a/source/java/org/alfresco/repo/forms/processor/NodeHandler.java b/source/java/org/alfresco/repo/forms/processor/NodeHandler.java index 68bf0b263b..c73697a585 100644 --- a/source/java/org/alfresco/repo/forms/processor/NodeHandler.java +++ b/source/java/org/alfresco/repo/forms/processor/NodeHandler.java @@ -27,7 +27,6 @@ package org.alfresco.repo.forms.processor; import java.io.Serializable; import java.util.ArrayList; import java.util.HashMap; -import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.StringTokenizer; @@ -78,11 +77,13 @@ public class NodeHandler extends AbstractHandler { private static final Log logger = LogFactory.getLog(NodeHandler.class); - protected static final String PROP_PREFIX = "prop_"; - protected static final String ASSOC_PREFIX = "assoc_"; - protected static final String ASSOC_ADD_SUFFIX = "_added"; - protected static final String ASSOC_REMOVE_SUFFIX = "_removed"; + protected static final String PROP = "prop"; + protected static final String ASSOC = "assoc"; protected static final String DATA_KEY_SEPARATOR = "_"; + protected static final String PROP_DATA_PREFIX = PROP + DATA_KEY_SEPARATOR; + protected static final String ASSOC_DATA_PREFIX = ASSOC + DATA_KEY_SEPARATOR; + protected static final String ASSOC_DATA_ADDED_SUFFIX = DATA_KEY_SEPARATOR + "added"; + protected static final String ASSOC_DATA_REMOVED_SUFFIX = DATA_KEY_SEPARATOR + "removed"; protected static final String TRANSIENT_MIMETYPE = "mimetype"; protected static final String TRANSIENT_SIZE = "size"; @@ -103,24 +104,24 @@ public class NodeHandler extends AbstractHandler /** * A regular expression which can be used to match property names. - * These names will look like "prop:cm:name". + * These names will look like "prop_cm_name". * The pattern can also be used to extract the "cm" and the "name" parts. */ - protected Pattern propertyNamePattern = Pattern.compile(PROP_PREFIX + "(.*){1}?_(.*){1}?"); + protected Pattern propertyNamePattern = Pattern.compile(PROP_DATA_PREFIX + "(.*){1}?_(.*){1}?"); /** * A regular expression which can be used to match tranisent property names. - * These names will look like "prop:name". + * These names will look like "prop_name". * The pattern can also be used to extract the "name" part. */ - protected Pattern transientPropertyPattern = Pattern.compile(PROP_PREFIX + "(.*){1}?"); + protected Pattern transientPropertyPattern = Pattern.compile(PROP_DATA_PREFIX + "(.*){1}?"); /** * A regular expression which can be used to match association names. - * These names will look like "assoc:cm:references_added". + * These names will look like "assoc_cm_references_added". * The pattern can also be used to extract the "cm", the "name" and the suffix parts. */ - protected Pattern associationNamePattern = Pattern.compile(ASSOC_PREFIX + "(.*){1}?_(.*){1}?(_[a-zA-Z]+)"); + protected Pattern associationNamePattern = Pattern.compile(ASSOC_DATA_PREFIX + "(.*){1}?_(.*){1}?(_[a-zA-Z]+)"); /** * Sets the node service @@ -210,7 +211,6 @@ public class NodeHandler extends AbstractHandler // setup field definitions and data generateAllPropertyFields(nodeRef, form); generateAllAssociationFields(nodeRef, form); - generateAllChildAssociationFields(nodeRef, form); generateTransientFields(nodeRef, form); } } @@ -238,61 +238,95 @@ public class NodeHandler extends AbstractHandler this.nodeService.getAspects(nodeRef)); Map propDefs = typeDef.getProperties(); Map assocDefs = typeDef.getAssociations(); - Map childAssocDefs = typeDef.getChildAssociations(); Map propValues = this.nodeService.getProperties(nodeRef); for (String fieldName : fields) { // try and split the field name String[] parts = fieldName.split(":"); - if (parts.length == 2) + if (parts.length == 2 || parts.length == 3) { - // create qname of field name - String qNamePrefix = parts[0]; - String localName = parts[1]; - QName fullQName = QName.createQName(qNamePrefix, localName, namespaceService); - - // lookup property def on node - PropertyDefinition propDef = propDefs.get(fullQName); - if (propDef != null) + boolean foundField = false; + boolean tryProperty = true; + boolean tryAssociation = true; + String qNamePrefix = null; + String localName = null; + + if (parts.length == 2) { - // generate the property field - generatePropertyField(propDef, propValues.get(propDef.getName()), form); + qNamePrefix = parts[0]; + localName = parts[1]; } else { - // look for association defined for the type - ChildAssociationDefinition childAssocDef = childAssocDefs.get(fullQName); - if (childAssocDef != null) + // if there are 3 parts to the field name the first one represents + // whether the field is a property or association i.e. prop:prefix:local + // or assoc:prefix:local, determine the prefix and ensure it's valid + if (PROP.equals(parts[0])) { - // generate the association field - // TODO: see if we can get just the specific child assoc data - generateAssociationField(childAssocDef, - this.nodeService.getChildAssocs(nodeRef), form); + tryAssociation = false; + } + else if (ASSOC.equals(parts[0])) + { + tryProperty = false; } else { - AssociationDefinition assocDef = assocDefs.get(fullQName); - if (assocDef != null) - { - // generate the association field - generateAssociationField(assocDef, - this.nodeService.getTargetAssocs(nodeRef, fullQName), form); - } - else - { - // still not found the field, is it a force'd field? - if (forcedFields != null && forcedFields.size() > 0 && - forcedFields.contains(fieldName)) - { - generateForcedField(fullQName, form); - } - else if (logger.isDebugEnabled()) - { - logger.debug("Ignoring field \"" + fieldName + - "\" as it is not defined for the current node and it does not appear in the 'force' list"); - } - } + if (logger.isWarnEnabled()) + logger.warn("\"" + parts[0] + "\" is an invalid prefix for requesting a property or association"); + + continue; + } + + qNamePrefix = parts[1]; + localName = parts[2]; + } + + // create qname of field name + QName fullQName = QName.createQName(qNamePrefix, localName, namespaceService); + + // try the field as a property + if (tryProperty) + { + // lookup property def on node + PropertyDefinition propDef = propDefs.get(fullQName); + if (propDef != null) + { + // generate the property field + generatePropertyField(propDef, propValues.get(fullQName), form); + + // no need to try and find an association + tryAssociation = false; + foundField = true; + } + } + + // try the field as an association + if (tryAssociation) + { + AssociationDefinition assocDef = assocDefs.get(fullQName); + if (assocDef != null) + { + // generate the association field + generateAssociationField(assocDef, + retrieveAssociationValues(nodeRef, assocDef), form); + + foundField = true; + } + } + + // still not found the field, is it a force'd field? + if (!foundField) + { + if (forcedFields != null && forcedFields.size() > 0 && + forcedFields.contains(fieldName)) + { + generateForcedField(fieldName, form); + } + else if (logger.isDebugEnabled()) + { + logger.debug("Ignoring field \"" + fieldName + + "\" as it is not defined for the current node and it does not appear in the 'force' list"); } } } @@ -333,39 +367,96 @@ public class NodeHandler extends AbstractHandler /** * Generates a field definition for the given field that is being forced - * to show + * to show. * - * @param fieldName QName of the field to force + * @param fieldName Name of the field to force * @param form The Form instance to populated */ - protected void generateForcedField(QName fieldName, Form form) + protected void generateForcedField(String fieldName, Form form) { if (logger.isDebugEnabled()) - logger.debug("Attempting to force the inclusion of field \"" + - fieldName.toPrefixString(this.namespaceService) + "\""); + logger.debug("Attempting to force the inclusion of field \"" + fieldName + "\""); - // lookup the field as a property in the model - PropertyDefinition propDef = this.dictionaryService.getProperty(fieldName); - if (propDef != null) + String[] parts = fieldName.split(":"); + if (parts.length == 2 || parts.length == 3) { - // generate the property field - generatePropertyField(propDef, null, form); - } - else - { - // lookup the field as an association in the model - AssociationDefinition assocDef = this.dictionaryService.getAssociation(fieldName); - if (assocDef != null) + boolean foundField = false; + boolean tryProperty = true; + boolean tryAssociation = true; + String qNamePrefix = null; + String localName = null; + + if (parts.length == 2) { - // generate the association field - generateAssociationField(assocDef, null, form); + qNamePrefix = parts[0]; + localName = parts[1]; } - else if (logger.isDebugEnabled()) + else { - logger.debug("Ignoring field \"" + fieldName.toPrefixString(this.namespaceService) + + // if there are 3 parts to the field name the first one represents + // whether the field is a property or association i.e. prop:prefix:local + // or assoc:prefix:local, determine the prefix and ensure it's valid + if (PROP.equals(parts[0])) + { + tryAssociation = false; + } + else if (ASSOC.equals(parts[0])) + { + tryProperty = false; + } + else + { + if (logger.isWarnEnabled()) + logger.warn("\"" + parts[0] + "\" is an invalid prefix for requesting a property or association"); + + return; + } + + qNamePrefix = parts[1]; + localName = parts[2]; + } + + // create qname of field name + QName fullQName = QName.createQName(qNamePrefix, localName, namespaceService); + + if (tryProperty) + { + // lookup the field as a property in the whole model + PropertyDefinition propDef = this.dictionaryService.getProperty(fullQName); + if (propDef != null) + { + // generate the property field + generatePropertyField(propDef, null, form); + + // no need to try and find an association + tryAssociation = false; + foundField = true; + } + } + + if (tryAssociation) + { + // lookup the field as an association in the whole model + AssociationDefinition assocDef = this.dictionaryService.getAssociation(fullQName); + if (assocDef != null) + { + // generate the association field + generateAssociationField(assocDef, null, form); + + foundField = true; + } + } + + if (!foundField && logger.isDebugEnabled()) + { + logger.debug("Ignoring field \"" + fieldName + "\" as it is not defined for the current node and can not be found in any model"); } } + else if (logger.isWarnEnabled()) + { + logger.warn("Ignoring unrecognised field \"" + fieldName + "\""); + } } /** @@ -420,7 +511,7 @@ public class NodeHandler extends AbstractHandler fieldDef.setRepeating(propDef.isMultiValued()); // define the data key name and set - String dataKeyName = PROP_PREFIX + nameParts[0] + DATA_KEY_SEPARATOR + nameParts[1]; + String dataKeyName = PROP_DATA_PREFIX + nameParts[0] + DATA_KEY_SEPARATOR + nameParts[1]; fieldDef.setDataKeyName(dataKeyName); // setup constraints for the property @@ -513,7 +604,7 @@ public class NodeHandler extends AbstractHandler */ protected void generateMimetypePropertyField(ContentData content, Form form) { - String dataKeyName = PROP_PREFIX + TRANSIENT_MIMETYPE; + String dataKeyName = PROP_DATA_PREFIX + TRANSIENT_MIMETYPE; PropertyFieldDefinition mimetypeField = new PropertyFieldDefinition( TRANSIENT_MIMETYPE, DataTypeDefinition.TEXT.toPrefixString( this.namespaceService)); @@ -532,7 +623,7 @@ public class NodeHandler extends AbstractHandler */ protected void generateEncodingPropertyField(ContentData content, Form form) { - String dataKeyName = PROP_PREFIX + TRANSIENT_ENCODING; + String dataKeyName = PROP_DATA_PREFIX + TRANSIENT_ENCODING; PropertyFieldDefinition encodingField = new PropertyFieldDefinition( TRANSIENT_ENCODING, DataTypeDefinition.TEXT.toPrefixString( this.namespaceService)); @@ -551,7 +642,7 @@ public class NodeHandler extends AbstractHandler */ protected void generateSizePropertyField(ContentData content, Form form) { - String dataKeyName = PROP_PREFIX + TRANSIENT_SIZE; + String dataKeyName = PROP_DATA_PREFIX + TRANSIENT_SIZE; PropertyFieldDefinition sizeField = new PropertyFieldDefinition( TRANSIENT_SIZE, DataTypeDefinition.LONG.toPrefixString( this.namespaceService)); @@ -569,183 +660,19 @@ public class NodeHandler extends AbstractHandler * @param nodeRef The NodeRef of the node being setup * @param form The Form instance to populate */ - @SuppressWarnings("unchecked") protected void generateAllAssociationFields(NodeRef nodeRef, Form form) { - // ******************************************************** - // re-factor this to gen. all assocs defs and not values - // ******************************************************** + // get data dictionary definition for the node + QName type = this.nodeService.getType(nodeRef); + TypeDefinition typeDef = this.dictionaryService.getAnonymousType(type, + this.nodeService.getAspects(nodeRef)); - // add target association data - List associations = this.nodeService.getTargetAssocs(nodeRef, - RegexQNamePattern.MATCH_ALL); - - if (associations.size() > 0) + // iterate round the association defintions and setup field definition + Map assocDefs = typeDef.getAssociations(); + for (AssociationDefinition assocDef : assocDefs.values()) { - // create internal cache of association definitions created - Map assocFieldDefs = - new HashMap(associations.size()); - - for (AssociationRef assoc : associations) - { - // get the name of the association - QName assocType = assoc.getTypeQName(); - String assocName = assocType.toPrefixString(this.namespaceService); - String assocValue = assoc.getTargetRef().toString(); - String dataKeyName = ASSOC_PREFIX + assocName.replace(":", DATA_KEY_SEPARATOR); - - // setup the field definition for the association if it hasn't before - AssociationFieldDefinition fieldDef = assocFieldDefs.get(assocName); - if (fieldDef == null) - { - AssociationDefinition assocDef = this.dictionaryService.getAssociation(assocType); - if (assocDef == null) - { - throw new FormException("Failed to find association definition for association: " + assocType); - } - - fieldDef = new AssociationFieldDefinition(assocName, - assocDef.getTargetClass().getName().toPrefixString( - this.namespaceService), Direction.TARGET); - String title = assocDef.getTitle(); - if (title == null) - { - title = assocName; - } - fieldDef.setLabel(title); - fieldDef.setDescription(assocDef.getDescription()); - fieldDef.setProtectedField(assocDef.isProtected()); - fieldDef.setEndpointMandatory(assocDef.isTargetMandatory()); - fieldDef.setEndpointMany(assocDef.isTargetMany()); - fieldDef.setDataKeyName(dataKeyName); - - // add definition to Form and to internal cache - form.addFieldDefinition(fieldDef); - assocFieldDefs.put(assocName, fieldDef); - } - - if (fieldDef.isEndpointMany()) - { - List targets = null; - - // add the value as a List (or add to the list if the form data - // is already present) - FieldData fieldData = form.getFormData().getData().get(dataKeyName); - if (fieldData == null) - { - targets = new ArrayList(4); - form.addData(dataKeyName, targets); - } - else - { - targets = (List)fieldData.getValue(); - } - - // add the assoc value to the list - targets.add(assocValue); - } - else - { - // there should only be one value - form.addData(dataKeyName, assocValue); - } - } - } - } - - /** - * Sets up the field definitions for the node's child associations. - * - * @param nodeRef The NodeRef of the node being setup - * @param form The Form instance to populate - */ - protected void generateAllChildAssociationFields(NodeRef nodeRef, Form form) - { - List childAssocs = this.nodeService.getChildAssocs(nodeRef); - if (childAssocs.isEmpty()) - { - return; - } - // create internal cache of association definitions created - Map childAssocFieldDefs = - new HashMap(childAssocs.size()); - - // All child associations are from the same parent node. - // However, the type of the child associations may not all be the same. - // This Map will have assocNames (e.g. sys:children) as a key and will - // have a List of child noderefs as values. - // So there will be one list for each of the child association *types*. - Map> childrenNodes = new HashMap>(); - - for (ChildAssociationRef childAssoc : childAssocs) - { - // get the name of the association - QName assocTypeName = childAssoc.getTypeQName(); - String assocName = assocTypeName.toPrefixString(this.namespaceService); - String dataKeyName = ASSOC_PREFIX + assocName.replace(":", DATA_KEY_SEPARATOR); - - // setup the field definition for the association if it hasn't before - AssociationFieldDefinition fieldDef = childAssocFieldDefs.get(assocName); - if (fieldDef == null) - { - AssociationDefinition assocDef = this.dictionaryService.getAssociation(assocTypeName); - if (assocDef == null || assocDef instanceof ChildAssociationDefinition == false) - { - throw new FormException("Failed to find association definition for child association: " - + assocTypeName); - } - - fieldDef = new AssociationFieldDefinition(assocName, - assocDef.getTargetClass().getName().toPrefixString( - this.namespaceService), Direction.TARGET); - String title = assocDef.getTitle(); - if (title == null) - { - title = assocName.toString(); - } - fieldDef.setLabel(title); - fieldDef.setDescription(assocDef.getDescription()); - fieldDef.setProtectedField(assocDef.isProtected()); - fieldDef.setEndpointMandatory(assocDef.isTargetMandatory()); - fieldDef.setEndpointMany(assocDef.isTargetMany()); - fieldDef.setDataKeyName(dataKeyName); - - // add definition to Form and to internal cache - form.addFieldDefinition(fieldDef); - childAssocFieldDefs.put(assocName, fieldDef); - } - - if (childrenNodes.containsKey(assocName) == false) - { - childrenNodes.put(assocName, new ArrayList()); - } - childrenNodes.get(assocName).add(childAssoc); - } - for (String associationName : childrenNodes.keySet()) - { - List values = childrenNodes.get(associationName); - - // We don't want the whitespace or enclosing square brackets that come - // with java.util.List.toString(), hence the custom String construction. - /*StringBuilder assocValue = new StringBuilder(); - for (Iterator iter = values.iterator(); iter.hasNext(); ) - { - ChildAssociationRef nextChild = iter.next(); - assocValue.append(nextChild.getChildRef().toString()); - if (iter.hasNext()) - { - assocValue.append(","); - } - } - form.addData(ASSOC_PREFIX + associationName, assocValue.toString());*/ - - List nodeRefs = new ArrayList(4); - for (Iterator iter = values.iterator(); iter.hasNext(); ) - { - ChildAssociationRef nextChild = iter.next(); - nodeRefs.add(nextChild.getChildRef().toString()); - } - form.addData(ASSOC_PREFIX + associationName.replace(":", DATA_KEY_SEPARATOR), nodeRefs); + this.generateAssociationField(assocDef, + retrieveAssociationValues(nodeRef, assocDef), form); } } @@ -777,36 +704,66 @@ public class NodeHandler extends AbstractHandler fieldDef.setEndpointMany(assocDef.isTargetMany()); // define the data key name and set - String dataKeyName = ASSOC_PREFIX + nameParts[0] + DATA_KEY_SEPARATOR + nameParts[1]; + String dataKeyName = ASSOC_DATA_PREFIX + nameParts[0] + DATA_KEY_SEPARATOR + nameParts[1]; fieldDef.setDataKeyName(dataKeyName); // add definition to the form form.addFieldDefinition(fieldDef); - // add the association value to the form - // determine the type of association values data and extract accordingly - List values = new ArrayList(4); - for (Object value : assocValues) + if (assocValues != null) { - if (value instanceof ChildAssociationRef) + // add the association value to the form + // determine the type of association values data and extract accordingly + List values = new ArrayList(4); + for (Object value : assocValues) { - values.add(((ChildAssociationRef)value).getChildRef().toString()); - } - else if (value instanceof AssociationRef) - { - values.add(((AssociationRef)value).getTargetRef().toString()); - } - else - { - values.add(value.toString()); + if (value instanceof ChildAssociationRef) + { + values.add(((ChildAssociationRef)value).getChildRef().toString()); + } + else if (value instanceof AssociationRef) + { + values.add(((AssociationRef)value).getTargetRef().toString()); + } + else + { + values.add(value.toString()); + } } + + // Add the list as the value for the association. + // TODO: Do we also return a well known named list of association names + // for each noderef so that clients do not have extra work to do + // to display the current values to the user + form.addData(dataKeyName, values); + } + } + + /** + * Retrieves the values of the given association definition on the given node. + * + * @param nodeRef The node to get the association values for + * @param assocDef The association definition to look for values for + * @return List of values for association or null of the association does + * not exist for the given node. + */ + @SuppressWarnings("unchecked") + protected List retrieveAssociationValues(NodeRef nodeRef, AssociationDefinition assocDef) + { + List assocValues = null; + + // get the list of values (if any) for the association + if (assocDef instanceof ChildAssociationDefinition) + { + assocValues = this.nodeService.getChildAssocs(nodeRef, assocDef.getName(), + RegexQNamePattern.MATCH_ALL); + } + else + { + assocValues = this.nodeService.getTargetAssocs(nodeRef, assocDef.getName()); } - // Add the list as the value for the association. - // TODO: Do we also return a well known named list of association names - // for each noderef so that clients do not have extra work to do - // to display the current values to the user - form.addData(dataKeyName, values); + return assocValues; } /* @@ -851,11 +808,11 @@ public class NodeHandler extends AbstractHandler { String fieldName = fieldData.getName(); - if (fieldName.startsWith(PROP_PREFIX)) + if (fieldName.startsWith(PROP_DATA_PREFIX)) { processPropertyPersist(nodeRef, propDefs, fieldData, propsToPersist); } - else if (fieldName.startsWith(ASSOC_PREFIX)) + else if (fieldName.startsWith(ASSOC_DATA_PREFIX)) { processAssociationPersist(nodeRef, assocDefs, childAssocDefs, fieldData, assocsToPersist); } @@ -1048,7 +1005,7 @@ public class NodeHandler extends AbstractHandler { if (NodeRef.isNodeRef(nextTargetNode)) { - if (assocSuffix.equals(ASSOC_ADD_SUFFIX)) + if (assocSuffix.equals(ASSOC_DATA_ADDED_SUFFIX)) { if (assocDef.isChild()) { @@ -1061,7 +1018,7 @@ public class NodeHandler extends AbstractHandler fullQNameFromJSON)); } } - else if (assocSuffix.equals(ASSOC_REMOVE_SUFFIX)) + else if (assocSuffix.equals(ASSOC_DATA_REMOVED_SUFFIX)) { if (assocDef.isChild()) { @@ -1082,9 +1039,9 @@ public class NodeHandler extends AbstractHandler msg.append("fieldName ") .append(fieldName) .append(" does not have one of the expected suffixes [") - .append(ASSOC_ADD_SUFFIX) + .append(ASSOC_DATA_ADDED_SUFFIX) .append(", ") - .append(ASSOC_REMOVE_SUFFIX) + .append(ASSOC_DATA_REMOVED_SUFFIX) .append("] and has been ignored."); logger.warn(msg.toString()); }