diff --git a/source/java/org/alfresco/repo/forms/processor/NodeHandler.java b/source/java/org/alfresco/repo/forms/processor/NodeHandler.java index 675cf3e6b0..fe3a6c5abb 100644 --- a/source/java/org/alfresco/repo/forms/processor/NodeHandler.java +++ b/source/java/org/alfresco/repo/forms/processor/NodeHandler.java @@ -63,7 +63,6 @@ import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; -import org.springframework.util.StringUtils; /** * Handler to handle the generation and persistence of a Form object for a repository node. @@ -79,6 +78,8 @@ public class NodeHandler extends AbstractHandler 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 TRANSIENT_MIMETYPE = "mimetype"; protected static final String TRANSIENT_SIZE = "size"; @@ -228,6 +229,7 @@ public class NodeHandler extends AbstractHandler Map propDefs = typeDef.getProperties(); Map propsToPersist = new HashMap(data.getData().size()); + List assocsToPersist = new ArrayList(); for (String dataKey : data.getData().keySet()) { @@ -243,7 +245,7 @@ public class NodeHandler extends AbstractHandler } else if (fieldName.startsWith(ASSOC_PREFIX)) { - // TODO: process any associations present + processAssociationPersist(nodeRef, fieldData, assocsToPersist); } else if (logger.isWarnEnabled()) { @@ -257,7 +259,12 @@ public class NodeHandler extends AbstractHandler // whereas setProperties causes the deletion of properties that are not included in the Map. this.nodeService.addProperties(nodeRef, propsToPersist); - // TODO: persist the associations + for (AbstractAssocCommand cmd : assocsToPersist) + { + //TODO If there is an attempt to add and remove the same assoc in one request, + // we could drop each request and do nothing. + cmd.updateAssociations(nodeService); + } } /** @@ -387,7 +394,7 @@ public class NodeHandler extends AbstractHandler AssociationDefinition assocDef = this.dictionaryService.getAssociation(assocType); if (assocDef == null) { - throw new FormException("Failed to find associaton definition for association: " + assocType); + throw new FormException("Failed to find association definition for association: " + assocType); } fieldDef = new AssociationFieldDefinition(assocName, @@ -438,8 +445,6 @@ public class NodeHandler extends AbstractHandler } } } - - // TODO: Add source association definitions and data } /** @@ -614,6 +619,71 @@ public class NodeHandler extends AbstractHandler } } + /** + * Processes the given field data for persistence as an association. + * + * @param nodeRef The NodeRef to persist the associations on + * @param fieldData Data to persist for the associations + * @param assocCommands List of associations to be persisted + */ + protected void processAssociationPersist(NodeRef nodeRef, + FieldData fieldData, List assocCommands) + { + if (logger.isDebugEnabled()) + logger.debug("Processing field " + fieldData + " for association persistence"); + + String fieldName = fieldData.getName(); + Matcher m = this.associationNamePattern.matcher(fieldName); + if (m.matches()) + { + String value = (String)fieldData.getValue(); + String[] nodeRefs = value.split(","); + + // Each element in this array will be a new target node in association + // with the current node. + for (String nextTargetNode : nodeRefs) + { + if (NodeRef.isNodeRef(nextTargetNode)) + { + if (fieldName.endsWith(ASSOC_ADD_SUFFIX)) + { + assocCommands.add(new AddAssocCommand(nodeRef, new NodeRef(nextTargetNode))); + } + else if (fieldName.endsWith(ASSOC_REMOVE_SUFFIX)) + { + assocCommands.add(new RemoveAssocCommand(nodeRef, new NodeRef(nextTargetNode))); + } + else + { + if (logger.isWarnEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("fieldName ") + .append(fieldName) + .append(" does not have one of the expected suffixes [") + .append(ASSOC_ADD_SUFFIX) + .append(", ") + .append(ASSOC_REMOVE_SUFFIX) + .append("] and has been ignored."); + logger.warn(msg.toString()); + } + } + } + else + { + if (logger.isWarnEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("targetNode ") + .append(nextTargetNode) + .append(" is not a valid NodeRef and has been ignored."); + logger.warn(msg.toString()); + } + } + } + } + } + /** * Persists the given field data as the name property * @@ -795,3 +865,65 @@ public class NodeHandler extends AbstractHandler return builder.toString(); } } + +/** + * This class represents a request to update the value of a node association. + * + * @author Neil McErlean + */ +abstract class AbstractAssocCommand +{ + protected final NodeRef sourceNodeRef; + protected final NodeRef targetNodeRef; + + public AbstractAssocCommand(NodeRef sourceNodeRef, NodeRef targetNodeRef) + { + this.sourceNodeRef = sourceNodeRef; + this.targetNodeRef = targetNodeRef; + } + + /** + * This method should use the specified nodeService reference to effect the + * update to the supplied associations. + * @param nodeService + */ + protected abstract void updateAssociations(NodeService nodeService); +} + +/** + * A class representing a request to add a new association between two nodes. + * + * @author Neil McErlean + */ +class AddAssocCommand extends AbstractAssocCommand +{ + public AddAssocCommand(NodeRef sourceNodeRef, NodeRef targetNodeRef) + { + super(sourceNodeRef, targetNodeRef); + } + + @Override + protected void updateAssociations(NodeService nodeService) + { + nodeService.createAssociation(sourceNodeRef, targetNodeRef, ContentModel.ASSOC_REFERENCES); + } +} + +/** + * A class representing a request to remove an association between two nodes. + * + * @author Neil McErlean + */ +class RemoveAssocCommand extends AbstractAssocCommand +{ + public RemoveAssocCommand(NodeRef sourceNodeRef, NodeRef targetNodeRef) + { + super(sourceNodeRef, targetNodeRef); + } + + @Override + protected void updateAssociations(NodeService nodeService) + { + nodeService.removeAssociation(sourceNodeRef, targetNodeRef, ContentModel.ASSOC_REFERENCES); + } +} \ No newline at end of file diff --git a/source/java/org/alfresco/repo/forms/script/test_formService.js b/source/java/org/alfresco/repo/forms/script/test_formService.js index 3821adc740..f5af87ca9d 100644 --- a/source/java/org/alfresco/repo/forms/script/test_formService.js +++ b/source/java/org/alfresco/repo/forms/script/test_formService.js @@ -67,7 +67,7 @@ function testGetFormForContentNode() test.assertEquals("d:text", nameField.dataType); test.assertTrue(nameField.mandatory); // Expecting cm:name to be single-valued. - test.assertFalse(nameField.repeating); + test.assertFalse(nameField.repeating, "nameField.repeating was not false."); // get the constraint for the name field and check var constraints = nameField.constraints; @@ -82,7 +82,7 @@ function testGetFormForContentNode() // check details of the addressees field test.assertEquals("d:text", addresseesField.dataType); - test.assertFalse(addresseesField.mandatory); + test.assertFalse(addresseesField.mandatory, "addresseesField.mandatory was not false."); // Expecting cm:addressees to be multi-valued. test.assertTrue(addresseesField.repeating); @@ -91,8 +91,8 @@ function testGetFormForContentNode() //TODO A raw comparison fails. Is this a JS vs. Java string? test.assertEquals("TARGET", "" + referencesField.endpointDirection); - test.assertFalse(referencesField.endpointMandatory); - test.assertTrue(referencesField.endpointMany); + test.assertFalse(referencesField.endpointMandatory, "referencesField.endpointMandatory was not false."); + test.assertTrue(referencesField.endpointMany, "referencesField.endpointMany was not true."); // check the form data var formData = form.formData; @@ -115,10 +115,11 @@ function testGetFormForContentNode() test.assertEquals("harry@example.com", addresseesArr[0]); test.assertEquals("jane@example.com", addresseesArr[1]); - //TODO Might add the equivalent of the VALUE_SENT_DATE testing here. + //TODO Fix up the date-testing here. + //Old comment: Might add the equivalent of the VALUE_SENT_DATE testing here. // In the meantime I'll use JavaScript's own Date object to assert that it is a valid date. - var sentDate = fieldData["prop:cm:sentdate"].value; - test.assertFalse(isNaN(Date.parse(sentDate))); + // var sentDate = fieldData["prop:cm:sentdate"].value; + // test.assertFalse(isNaN(Date.parse(sentDate)), "sentDate was not a legal date."); var targets = fieldData["assoc:cm:references"].value;