diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/DataKeyInfo.java b/source/java/org/alfresco/repo/forms/processor/workflow/DataKeyInfo.java new file mode 100644 index 0000000000..81a907aee1 --- /dev/null +++ b/source/java/org/alfresco/repo/forms/processor/workflow/DataKeyInfo.java @@ -0,0 +1,91 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.repo.forms.processor.workflow; + +import org.alfresco.service.namespace.QName; + +/** + * @author Nick Smith + * + */ +public class DataKeyInfo +{ + private final String dataKey; + private final QName qName; + private final FieldType fieldType; + private final boolean isAdd; + + private DataKeyInfo(String dataKey, QName qName, FieldType fieldType, boolean isAdd) + { + this.dataKey = dataKey; + this.qName = qName; + this.fieldType = fieldType; + this.isAdd = isAdd; + } + + public static DataKeyInfo makeAssociationDataKeyInfo(String dataKey, QName qName, boolean isAdd) + { + return new DataKeyInfo(dataKey, qName, FieldType.ASSOCIATION, isAdd); + } + + public static DataKeyInfo makePropertyDataKeyInfo(String dataKey, QName qName) + { + return new DataKeyInfo(dataKey, qName, FieldType.PROPERTY, true); + } + + public static DataKeyInfo makeTransientDataKeyInfo(String dataKey) + { + return new DataKeyInfo(dataKey, null, FieldType.TRANSIENT, true); + } + + /** + * @return the dataKey + */ + public String getDataKey() + { + return dataKey; + } + + /** + * @return the qName + */ + public QName getqName() + { + return qName; + } + + + /** + * @return the fieldType + */ + public FieldType getFieldType() + { + return fieldType; + } + + /** + * @return the isAdd + */ + public boolean isAdd() + { + return isAdd; + } + +} diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/DataKeyMatcher.java b/source/java/org/alfresco/repo/forms/processor/workflow/DataKeyMatcher.java new file mode 100644 index 0000000000..4c2fdbbee8 --- /dev/null +++ b/source/java/org/alfresco/repo/forms/processor/workflow/DataKeyMatcher.java @@ -0,0 +1,111 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.repo.forms.processor.workflow; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; + +import static org.alfresco.repo.forms.processor.node.FormFieldConstants.*; + +/** + * @author Nick Smith + * + */ +public class DataKeyMatcher +{ + /** + * A regular expression which can be used to match property names. These + * names will look like "prop_cm_name". The pattern can also be + * used to extract the "cm" and the "name" parts. + */ + private final static Pattern propertyNamePattern = Pattern.compile("(^[a-zA-Z0-9]+)_([a-zA-Z0-9_]+$)"); + + public DataKeyMatcher(NamespaceService namespaceService) + { + this.namespaceService = namespaceService; + } + + /** + * A regular expression which can be used to match association names. 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. + */ + private final static Pattern associationNamePattern = Pattern.compile("(^[a-zA-Z0-9]+)_([a-zA-Z0-9_]+)(_[a-zA-Z]+$)"); + + private final NamespaceService namespaceService; + + /** + * + * @param dataKey + * @return + */ + public DataKeyInfo match(String dataKey) + { + if (dataKey.startsWith(PROP_DATA_PREFIX)) + { + return matchProperty(dataKey); + } + else if(dataKey.startsWith(ASSOC_DATA_PREFIX)) + { + return matchAssociation(dataKey); + } + + // No match found. + return null; + } + + private DataKeyInfo matchAssociation(String dataKey) + { + String keyName = dataKey.substring(ASSOC_DATA_PREFIX.length()); + Matcher matcher = associationNamePattern.matcher(keyName); + if (!matcher.matches()) + { + return null; + } + QName qName = getQName(matcher); + String suffix = matcher.group(3); + boolean isAdd = !(ASSOC_DATA_REMOVED_SUFFIX.equals(suffix)); + return DataKeyInfo.makeAssociationDataKeyInfo(keyName, qName, isAdd); + } + + private DataKeyInfo matchProperty(String dataKey) + { + String keyName = dataKey.substring(PROP_DATA_PREFIX.length()); + Matcher matcher = propertyNamePattern.matcher(keyName); + if (matcher.matches()) + { + QName qName = getQName(matcher); + return DataKeyInfo.makePropertyDataKeyInfo(keyName, qName); + } + return DataKeyInfo.makeTransientDataKeyInfo(keyName); + } + + private QName getQName(Matcher matcher) + { + String prefix = matcher.group(1); + String localName = matcher.group(2); + QName qName = QName.createQName(prefix, localName, namespaceService); + return qName; + } +} diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/FieldType.java b/source/java/org/alfresco/repo/forms/processor/workflow/FieldType.java new file mode 100644 index 0000000000..486fc558a1 --- /dev/null +++ b/source/java/org/alfresco/repo/forms/processor/workflow/FieldType.java @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2005-2010 Alfresco Software Limited. + * + * This file is part of Alfresco + * + * Alfresco is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * Alfresco is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Lesser General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with Alfresco. If not, see . + */ + +package org.alfresco.repo.forms.processor.workflow; + +/** + * @author Nick Smith + * + */ +public enum FieldType +{ + ASSOCIATION, + PROPERTY, + TRANSIENT; +} diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessor.java b/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessor.java new file mode 100644 index 0000000000..b6b7f6e4c5 --- /dev/null +++ b/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessor.java @@ -0,0 +1,235 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.repo.forms.processor.workflow; + +import java.io.Serializable; +import java.util.Map; + +import org.alfresco.repo.forms.FormData; +import org.alfresco.repo.forms.Item; +import org.alfresco.repo.forms.FormData.FieldData; +import org.alfresco.repo.forms.processor.FieldProcessorRegistry; +import org.alfresco.repo.forms.processor.node.ContentModelFormProcessor; +import org.alfresco.repo.forms.processor.node.ItemData; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.QName; +import org.alfresco.util.ParameterCheck; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * @author Nick Smith + */ +public class TaskFormProcessor extends ContentModelFormProcessor +{ + /** Logger */ + private static final Log LOGGER = LogFactory.getLog(TaskFormProcessor.class); + private static final TypedPropertyValueGetter valueGetter = new TypedPropertyValueGetter(); + + private DataKeyMatcher keyMatcher; + private WorkflowService workflowService; + + // Constructor for Spring + public TaskFormProcessor() + { + super(); + } + + // Constructor for tests. + public TaskFormProcessor(WorkflowService workflowService, NamespaceService namespaceService, + DictionaryService dictionaryService, FieldProcessorRegistry fieldProcessorRegistry) + { + this.workflowService = workflowService; + this.namespaceService = namespaceService; + this.dictionaryService = dictionaryService; + this.fieldProcessorRegistry = fieldProcessorRegistry; + this.keyMatcher = new DataKeyMatcher(namespaceService); + } + + @Override + protected WorkflowTask getTypedItem(Item item) + { + ParameterCheck.mandatory("item", item); + String id = item.getId(); + WorkflowTask task = workflowService.getTaskById(id); + return task; + } + + @Override + protected WorkflowTask internalPersist(WorkflowTask task, FormData data) + { + TaskUpdater updater = new TaskUpdater(task.id, workflowService); + ItemData itemData = makeItemData(task); + for (FieldData fieldData : data) + { + addFieldToSerialize(updater, itemData, fieldData); + } + return updater.update(); + } + + private void addFieldToSerialize(TaskUpdater updater, + ItemData itemData, + FieldData fieldData) + { + String name = fieldData.getName(); + DataKeyInfo keyInfo = keyMatcher.match(name); + if ((keyInfo == null || FieldType.TRANSIENT == keyInfo.getFieldType()) && + LOGGER.isWarnEnabled()) + { + LOGGER.warn("Ignoring unrecognized field: " + name); + } + + if (keyInfo != null) + { + QName fullName = keyInfo.getqName(); + Object rawValue = fieldData.getValue(); + if (FieldType.PROPERTY == keyInfo.getFieldType()) + { + Serializable propValue = getPropertyValueToPersist(fullName, rawValue, itemData); + // TODO What if the user wants to set prop to null? + if (propValue != null) + { + updater.addProperty(fullName, propValue); + } + } + else if (FieldType.ASSOCIATION == keyInfo.getFieldType()) + { + if (rawValue instanceof String) + { + updater.changeAssociation(fullName, (String) rawValue, keyInfo.isAdd()); + } + } + } + } + + private Serializable getPropertyValueToPersist(QName fullName, + Object value, + ItemData itemData) + { + PropertyDefinition propDef = itemData.getPropertyDefinition(fullName); + if (propDef == null) + { + propDef = dictionaryService.getProperty(fullName); + } + if (propDef != null) + { + return valueGetter.getValue(value, propDef); + } + return null; + } + + /* + * (non-Javadoc) + * + * @see + * org.alfresco.repo.forms.processor.FilteredFormProcessor#getItemType(java + * .lang.Object) + */ + @Override + protected String getItemType(WorkflowTask item) + { + TypeDefinition typeDef = item.definition.metadata; + return typeDef.getName().toPrefixString(namespaceService); + } + + /* + * (non-Javadoc) + * + * @see + * org.alfresco.repo.forms.processor.FilteredFormProcessor#getItemURI(java + * .lang.Object) + */ + @Override + protected String getItemURI(WorkflowTask item) + { + // TODO Check this URL is OK. + return "/api/task-instances/" + item.id; + } + + /* + * @see + * org.alfresco.repo.forms.processor.task.ContentModelFormProcessor#getLogger + * () + */ + @Override + protected Log getLogger() + { + return LOGGER; + } + + @Override + protected TypeDefinition getBaseType(WorkflowTask task) + { + return task.definition.metadata; + } + + @Override + protected Map getPropertyValues(WorkflowTask task) + { + return task.properties; + } + + @Override + protected Map getAssociationValues(WorkflowTask item) + { + return item.properties; + } + + @Override + protected Map getTransientValues(WorkflowTask item) + { + return null; + } + + /** + * Sets the Workflow Service. + * + * @param workflowService + */ + public void setWorkflowService(WorkflowService workflowService) + { + this.workflowService = workflowService; + } + + /* + * (non-Javadoc) + * + * @seeorg.alfresco.repo.forms.processor.task.ContentModelFormProcessor# + * setNamespaceService(org.alfresco.service.namespace.NamespaceService) + */ + @Override + public void setNamespaceService(NamespaceService namespaceService) + { + super.setNamespaceService(namespaceService); + this.keyMatcher = new DataKeyMatcher(namespaceService); + } + +} diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessorTest.java b/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessorTest.java new file mode 100644 index 0000000000..23f51eac10 --- /dev/null +++ b/source/java/org/alfresco/repo/forms/processor/workflow/TaskFormProcessorTest.java @@ -0,0 +1,507 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.repo.forms.processor.workflow; + +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyMap; +import static org.mockito.Matchers.anyString; +import static org.mockito.Matchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import junit.framework.TestCase; + +import org.alfresco.repo.forms.FieldDefinition; +import org.alfresco.repo.forms.Form; +import org.alfresco.repo.forms.FormData; +import org.alfresco.repo.forms.Item; +import org.alfresco.repo.forms.FormData.FieldData; +import org.alfresco.repo.forms.processor.node.DefaultFieldProcessor; +import org.alfresco.repo.forms.processor.node.FormFieldConstants; +import org.alfresco.repo.forms.processor.node.MockClassAttributeDefinition; +import org.alfresco.repo.forms.processor.node.MockFieldProcessorRegistry; +import org.alfresco.repo.workflow.WorkflowModel; +import org.alfresco.service.cmr.dictionary.AssociationDefinition; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.DictionaryService; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.alfresco.service.cmr.dictionary.TypeDefinition; +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.workflow.WorkflowException; +import org.alfresco.service.cmr.workflow.WorkflowNode; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.cmr.workflow.WorkflowTaskDefinition; +import org.alfresco.service.cmr.workflow.WorkflowTaskState; +import org.alfresco.service.namespace.NamespaceService; +import org.alfresco.service.namespace.NamespaceServiceMemoryImpl; +import org.alfresco.service.namespace.QName; +import org.mockito.ArgumentCaptor; +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +/** + * @author Nick Smith + */ +public class TaskFormProcessorTest extends TestCase +{ + private static final String TASK_DEF_NAME = "TaskDef"; + private static final String TASK_ID = "Real Id"; + private static final QName DESC_NAME = WorkflowModel.PROP_DESCRIPTION; + private static final QName STATUS_NAME = WorkflowModel.PROP_STATUS; + private static final QName PROP_WITH_ = QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "some_prop"); + private static final QName ACTORS_NAME = WorkflowModel.ASSOC_POOLED_ACTORS; + private static final QName ASSIGNEE_NAME = WorkflowModel.ASSOC_ASSIGNEE; + private static final QName ASSOC_WITH_ = QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "some_assoc"); + private static final NodeRef FAKE_NODE = new NodeRef(NamespaceService.BPM_MODEL_1_0_URI + "/FakeNode"); + + private WorkflowService workflowService; + private TaskFormProcessor processor; + private WorkflowTask task; + private NamespaceService namespaceService; + private WorkflowTask newTask; + + public void testGetTypedItem() throws Exception + { + try + { + processor.getTypedItem(null); + fail("Should have thrown an Exception here!"); + } + catch (IllegalArgumentException e) + { + // Do nothing! + } + + try + { + processor.getTypedItem(new Item("task", "bad id")); + fail("Should have thrown an Exception here!"); + } + catch (WorkflowException e) + { + // Do nothing! + } + + Item item = new Item("task", TASK_ID); + WorkflowTask result = processor.getTypedItem(item); + assertNotNull(result); + assertEquals(TASK_ID, result.id); + } + + public void testGenerateSetsItemAndUrl() throws Exception + { + Item item = new Item("task", TASK_ID); + Form form = processor.generate(item, null, null, null); + Item formItem = form.getItem(); + assertEquals(item.getId(), formItem.getId()); + assertEquals(item.getKind(), formItem.getKind()); + String expType = NamespaceService.BPM_MODEL_PREFIX + ":" + TASK_DEF_NAME; + assertEquals(expType, formItem.getType()); + assertEquals("/api/task-instances/" + TASK_ID, formItem.getUrl()); + } + + public void testGenerateSingleProperty() + { + // Check Status field is added to Form. + String fieldName = STATUS_NAME.toPrefixString(namespaceService); + List fields = Arrays.asList(fieldName); + Form form = processForm(fields); + checkSingleProperty(form, fieldName, WorkflowTaskState.IN_PROGRESS); + + // Check Status field is added to Form, when explicitly typed as a + // property. + String fullPropertyName = "prop:" + fieldName; + fields = Arrays.asList(fullPropertyName); + form = processForm(fields); + checkSingleProperty(form, fieldName, WorkflowTaskState.IN_PROGRESS); + } + + public void testGenerateSingleAssociation() + { + // Check Assignee field is added to Form. + String fieldName = ASSIGNEE_NAME.toPrefixString(namespaceService); + List fields = Arrays.asList(fieldName); + Form form = processForm(fields); + Serializable fieldData = (Serializable) Arrays.asList(FAKE_NODE.toString()); +// checkSingleAssociation(form, fieldName, fieldData); + + // Check Assignee field is added to Form, when explicitly typed as an + // association. + String fullAssociationName = "assoc:" + fieldName; + fields = Arrays.asList(fullAssociationName); + form = processForm(fields); +// checkSingleAssociation(form, fieldName, fieldData); + } + + public void testIgnoresUnknownFields() throws Exception + { + String fakeFieldName = NamespaceService.BPM_MODEL_PREFIX + ":" + "Fake Field"; + String statusFieldName = STATUS_NAME.toPrefixString(namespaceService); + List fields = Arrays.asList(fakeFieldName, statusFieldName); + Form form = processForm(fields); + checkSingleProperty(form, statusFieldName, WorkflowTaskState.IN_PROGRESS); + } + + public void testGenerateDefaultForm() throws Exception + { + Form form = processForm(null); + List fieldDefs = form.getFieldDefinitionNames(); + assertEquals(6, fieldDefs.size()); + assertTrue(fieldDefs.contains(ASSIGNEE_NAME.toPrefixString(namespaceService))); + assertTrue(fieldDefs.contains(ACTORS_NAME.toPrefixString(namespaceService))); + assertTrue(fieldDefs.contains(DESC_NAME.toPrefixString(namespaceService))); + assertTrue(fieldDefs.contains(STATUS_NAME.toPrefixString(namespaceService))); + + Serializable fieldData = (Serializable) Arrays.asList(FAKE_NODE.toString()); + FormData formData = form.getFormData(); + assertEquals(4, formData.getNumberOfFields()); +// assertEquals(fieldData, formData.getFieldData("assoc_bpm_assignee").getValue()); + assertEquals(WorkflowTaskState.IN_PROGRESS, formData.getFieldData("prop_bpm_status").getValue()); + } + + public void testPersistPropertyChanged() throws Exception + { + String fieldName = DESC_NAME.toPrefixString(namespaceService); + String dataKey = makeDataKeyName(fieldName, FormFieldConstants.PROP_DATA_PREFIX); + String value = "New Description"; + + processPersist(dataKey, value); + + Map actualProperties = retrievePropertyies(); + assertEquals(1, actualProperties.size()); + assertEquals(value, actualProperties.get(DESC_NAME)); + } + + public void testPersistPropertyWith_() throws Exception + { + String fieldName = PROP_WITH_.toPrefixString(namespaceService); + String dataKey = makeDataKeyName(fieldName, FormFieldConstants.PROP_DATA_PREFIX); + String value = "New _ Value"; + + processPersist(dataKey, value); + + Map actualProperties = retrievePropertyies(); + assertEquals(1, actualProperties.size()); + assertEquals(value, actualProperties.get(PROP_WITH_)); + } + + @SuppressWarnings("unchecked") + private Map retrievePropertyies() + { + ArgumentCaptor mapArg = ArgumentCaptor.forClass(Map.class); + verify(workflowService).updateTask(eq(TASK_ID), mapArg.capture(), anyMap(), anyMap()); + return mapArg.getValue(); + } + + public void testPersistAssociationAdded() throws Exception + { + String fieldName = ACTORS_NAME.toPrefixString(namespaceService); + String dataKey = makeDataKeyName(fieldName, FormFieldConstants.ASSOC_DATA_PREFIX); + dataKey = dataKey + FormFieldConstants.ASSOC_DATA_ADDED_SUFFIX; + String nodeRef1 = FAKE_NODE.toString() + "1"; + String nodeRef2 = FAKE_NODE.toString() + "2"; + String value = nodeRef1 + ", " + nodeRef2; + processPersist(dataKey, value); + + Map> actualAddedAssocs = retrieveAssociations(true); + assertEquals(1, actualAddedAssocs.size()); + List nodeRefs = actualAddedAssocs.get(ACTORS_NAME); + assertNotNull(nodeRefs); + assertEquals(2, nodeRefs.size()); + assertTrue(nodeRefs.contains(new NodeRef(nodeRef1))); + assertTrue(nodeRefs.contains(new NodeRef(nodeRef2))); + } + + public void testPersistAssociationsRemoved() throws Exception + { + String fieldName = ASSIGNEE_NAME.toPrefixString(namespaceService); + String dataKey = makeDataKeyName(fieldName, FormFieldConstants.ASSOC_DATA_PREFIX); + dataKey = dataKey + FormFieldConstants.ASSOC_DATA_REMOVED_SUFFIX; + String value = FAKE_NODE.toString(); + processPersist(dataKey, value); + + Map> actualRemovedAssocs = retrieveAssociations(false); + assertEquals(1, actualRemovedAssocs.size()); + List nodeRefs = actualRemovedAssocs.get(ASSIGNEE_NAME); + assertNotNull(nodeRefs); + assertEquals(1, nodeRefs.size()); + assertTrue(nodeRefs.contains(FAKE_NODE)); + } + + public void testPersistAssociationAddedWith_() throws Exception + { + String fieldName = ASSOC_WITH_.toPrefixString(namespaceService); + String dataKey = makeDataKeyName(fieldName, FormFieldConstants.ASSOC_DATA_PREFIX); + dataKey = dataKey + FormFieldConstants.ASSOC_DATA_ADDED_SUFFIX; + String nodeRef1 = FAKE_NODE.toString() + "1"; + String nodeRef2 = FAKE_NODE.toString() + "2"; + String value = nodeRef1 + ", " + nodeRef2; + processPersist(dataKey, value); + + Map> actualAddedAssocs = retrieveAssociations(true); + assertEquals(1, actualAddedAssocs.size()); + List nodeRefs = actualAddedAssocs.get(ASSOC_WITH_); + assertNotNull(nodeRefs); + assertEquals(2, nodeRefs.size()); + assertTrue(nodeRefs.contains(new NodeRef(nodeRef1))); + assertTrue(nodeRefs.contains(new NodeRef(nodeRef2))); + } + + @SuppressWarnings("unchecked") + private Map> retrieveAssociations(boolean added) + { + ArgumentCaptor mapArg = ArgumentCaptor.forClass(Map.class); + if (added) + { + verify(workflowService).updateTask(eq(TASK_ID), anyMap(), mapArg.capture(), anyMap()); + } + else + { + verify(workflowService).updateTask(eq(TASK_ID), anyMap(), anyMap(), mapArg.capture()); + } + return mapArg.getValue(); + } + + private void processPersist(String dataKey, String value) + { + FormData data = new FormData(); + data.addFieldData(dataKey, value); + Item item = new Item("task", TASK_ID); + WorkflowTask persistedItem = (WorkflowTask) processor.persist(item, data); + assertEquals(newTask, persistedItem); + } + + private Form processForm(List fields) + { + Item item = new Item("task", TASK_ID); + Form form = processor.generate(item, fields, null, null); + return form; + } + + private void checkSingleProperty(Form form, String fieldName, Serializable fieldData) + { + checkSingleField(form, fieldName, fieldData, "prop_"); + + } + + private void checkSingleAssociation(Form form, String fieldName, Serializable fieldData) + { + checkSingleField(form, fieldName, fieldData, "assoc_"); + + } + + private void checkSingleField(Form form, String fieldName, Serializable fieldData, String prefix) + { + List fieldDefs = form.getFieldDefinitions(); + assertEquals(1, fieldDefs.size()); + FieldDefinition fieldDef = fieldDefs.get(0); + assertEquals(fieldName, fieldDef.getName()); + String dataKey = fieldDef.getDataKeyName(); + String expDataKey = makeDataKeyName(fieldName, prefix); + assertEquals(expDataKey, dataKey); + FieldData data = form.getFormData().getFieldData(dataKey); + assertEquals(fieldData, data.getValue()); + } + + /** + * @param fieldName + * @param prefix + * @return + */ + private String makeDataKeyName(String fieldName, String prefix) + { + return prefix + fieldName.replace(":", "_"); + } + + /* + * @see junit.framework.TestCase#setUp() + */ + @Override + protected void setUp() throws Exception + { + super.setUp(); + task = makeTask(); + workflowService = makeWorkflowService(); + DictionaryService dictionaryService = makeDictionaryService(); + namespaceService = makeNamespaceService(); + MockFieldProcessorRegistry fieldProcessorRegistry = new MockFieldProcessorRegistry(namespaceService, + dictionaryService); + DefaultFieldProcessor defaultProcessor = makeDefaultFieldProcessor(dictionaryService); + processor = makeTaskFormProcessor(dictionaryService, fieldProcessorRegistry, defaultProcessor); + } + + private TaskFormProcessor makeTaskFormProcessor(DictionaryService dictionaryService, + MockFieldProcessorRegistry fieldProcessorRegistry, DefaultFieldProcessor defaultProcessor) + { + TaskFormProcessor processor1 = new TaskFormProcessor(); + processor1.setWorkflowService(workflowService); + processor1.setNamespaceService(namespaceService); + processor1.setDictionaryService(dictionaryService); + processor1.setFieldProcessorRegistry(fieldProcessorRegistry); + return processor1; + } + + private DefaultFieldProcessor makeDefaultFieldProcessor(DictionaryService dictionaryService) throws Exception + { + DefaultFieldProcessor defaultProcessor = new DefaultFieldProcessor(); + defaultProcessor.setDictionaryService(dictionaryService); + defaultProcessor.setNamespaceService(namespaceService); + defaultProcessor.afterPropertiesSet(); + return defaultProcessor; + } + + private WorkflowTask makeTask() + { + WorkflowTask result = new WorkflowTask(); + result.id = TASK_ID; + result.state = WorkflowTaskState.IN_PROGRESS; + result.definition = makeTaskDefinition(); + result.properties = makeTaskProperties(); + return result; + } + + private HashMap makeTaskProperties() + { + HashMap properties = new HashMap(); + properties.put(STATUS_NAME, WorkflowTaskState.IN_PROGRESS); + properties.put(ASSIGNEE_NAME, FAKE_NODE); + return properties; + } + + private WorkflowTaskDefinition makeTaskDefinition() + { + WorkflowTaskDefinition definition = new WorkflowTaskDefinition(); + definition.id = "DefinitionId"; + definition.metadata = makeTypeDef(); + definition.node = mock(WorkflowNode.class); + return definition; + } + + private TypeDefinition makeTypeDef() + { + TypeDefinition typeDef = mock(TypeDefinition.class); + QName name = QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, TASK_DEF_NAME); + when(typeDef.getName()).thenReturn(name); + + // Set up task property definitions + Map propertyDefs = makeTaskPropertyDefs(); + when(typeDef.getProperties()).thenReturn(propertyDefs); + + // Set up task association definitions. + Map associationDefs = makeTaskAssociationDefs(); + when(typeDef.getAssociations()).thenReturn(associationDefs); + return typeDef; + } + + private Map makeTaskPropertyDefs() + { + Map properties = new HashMap(); + QName textType = DataTypeDefinition.TEXT; + + // Add a Description property + PropertyDefinition descValue = MockClassAttributeDefinition.mockPropertyDefinition(DESC_NAME, textType); + properties.put(DESC_NAME, descValue); + + // Add a Status property + PropertyDefinition titleValue = MockClassAttributeDefinition.mockPropertyDefinition(STATUS_NAME, textType); + properties.put(STATUS_NAME, titleValue); + + // Add a Status property + PropertyDefinition with_ = MockClassAttributeDefinition.mockPropertyDefinition(PROP_WITH_, textType); + properties.put(PROP_WITH_, with_); + + return properties; + } + + private Map makeTaskAssociationDefs() + { + Map associations = new HashMap(); + QName actorName = QName.createQName(NamespaceService.BPM_MODEL_1_0_URI, "Actor"); + + // Add Assigneee association + MockClassAttributeDefinition assigneeDef = MockClassAttributeDefinition.mockAssociationDefinition( + ASSIGNEE_NAME, actorName); + associations.put(ASSIGNEE_NAME, assigneeDef); + + // Add Assigneee association + MockClassAttributeDefinition actorsDef = MockClassAttributeDefinition.mockAssociationDefinition(ACTORS_NAME, + actorName); + associations.put(ACTORS_NAME, actorsDef); + + // Add association with _ + MockClassAttributeDefinition with_ = MockClassAttributeDefinition.mockAssociationDefinition(ASSOC_WITH_, + actorName); + associations.put(ASSOC_WITH_, with_); + + return associations; + } + + private NamespaceService makeNamespaceService() + { + NamespaceServiceMemoryImpl nsService = new NamespaceServiceMemoryImpl(); + nsService.registerNamespace(NamespaceService.BPM_MODEL_PREFIX, NamespaceService.BPM_MODEL_1_0_URI); + return nsService; + } + + @SuppressWarnings("unchecked") + private DictionaryService makeDictionaryService() + { + DictionaryService mock = mock(DictionaryService.class); + when(mock.getAnonymousType((QName) any(), (Collection) any())).thenReturn(task.definition.metadata); + return mock; + } + + @SuppressWarnings("unchecked") + private WorkflowService makeWorkflowService() + { + WorkflowService service = mock(WorkflowService.class); + when(service.getTaskById(anyString())).thenAnswer(new Answer() + { + + public WorkflowTask answer(InvocationOnMock invocation) throws Throwable + { + String id = (String) invocation.getArguments()[0]; + if (TASK_ID.equals(id)) + return task; + else + throw new WorkflowException("Task Id not found!"); + } + }); + this.newTask = new WorkflowTask(); + newTask.id = TASK_ID; + when(service.updateTask(anyString(), anyMap(), anyMap(), anyMap())).thenReturn(newTask); + return service; + } +} diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/TaskUpdater.java b/source/java/org/alfresco/repo/forms/processor/workflow/TaskUpdater.java new file mode 100644 index 0000000000..10626c18ab --- /dev/null +++ b/source/java/org/alfresco/repo/forms/processor/workflow/TaskUpdater.java @@ -0,0 +1,153 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.repo.forms.processor.workflow; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.alfresco.service.cmr.repository.NodeRef; +import org.alfresco.service.cmr.workflow.WorkflowService; +import org.alfresco.service.cmr.workflow.WorkflowTask; +import org.alfresco.service.namespace.QName; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +/** + * A utility class for updating workflow tasks. This is a stateful object that + * accumulates a set of updates to a task and then commits all the updates when + * the update() method is called. + * + * @author Nick Smith + */ +public class TaskUpdater +{ + /** Logger */ + private static final Log LOGGER = LogFactory.getLog(TaskUpdater.class); + + private final String taskId; + private final WorkflowService workflowService; + private Map properties = new HashMap(); + private Map> add = new HashMap>(); + private Map> remove = new HashMap>(); + + public TaskUpdater(String taskId, WorkflowService workflowService) + { + this.taskId = taskId; + this.workflowService = workflowService; + } + + public WorkflowTask update() + { + WorkflowTask result = workflowService.updateTask(taskId, properties, add, remove); + properties = new HashMap(); + add = new HashMap>(); + remove = new HashMap>(); + return result; + } + + public void addProperty(QName name, Serializable value) + { + properties.put(name, value); + } + + public void addAssociation(QName name, List value) + { + add.put(name, value); + } + + public void removeAssociation(QName name, List value) + { + remove.put(name, value); + } + + public boolean changeAssociation(QName name, String nodeRefs, boolean isAdd) + { + List value = getNodeRefs(nodeRefs); + if (value == null) + { + return false; + } + Map> map = getAssociationMap(isAdd); + if (map != null) + { + map.put(name, value); + return true; + } + return false; + } + + /** + * @param suffix + * @return + */ + private Map> getAssociationMap(boolean isAdd) + { + Map> map = null; + if (isAdd) + { + map = add; + } + else + { + map = remove; + } + return map; + } + + private List getNodeRefs(Object value) + { + String[] nodeRefIds = ((String) value).split(","); + List nodeRefs = new ArrayList(nodeRefIds.length); + for (String nodeRefString : nodeRefIds) + { + String nodeRefId = nodeRefString.trim(); + if (NodeRef.isNodeRef(nodeRefId)) + { + NodeRef nodeRef = new NodeRef(nodeRefId); + nodeRefs.add(nodeRef); + } + else + { + logNodeRefError(nodeRefId); + } + } + return nodeRefs; + } + + private void logNodeRefError(String nodeRefId) + { + if (LOGGER.isWarnEnabled()) + { + StringBuilder msg = new StringBuilder(); + msg.append("Target Node: ").append(nodeRefId); + msg.append(" is not a valid NodeRef and has been ignored."); + LOGGER.warn(msg.toString()); + } + } +} diff --git a/source/java/org/alfresco/repo/forms/processor/workflow/TypedPropertyValueGetter.java b/source/java/org/alfresco/repo/forms/processor/workflow/TypedPropertyValueGetter.java new file mode 100644 index 0000000000..822fb023f1 --- /dev/null +++ b/source/java/org/alfresco/repo/forms/processor/workflow/TypedPropertyValueGetter.java @@ -0,0 +1,196 @@ +/* + * Copyright (C) 2005-2009 Alfresco Software Limited. + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + + * You should have received a copy of the GNU General Public License + * along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. + + * As a special exception to the terms and conditions of version 2.0 of + * the GPL, you may redistribute this Program in connection with Free/Libre + * and Open Source Software ("FLOSS") applications as described in Alfresco's + * FLOSS exception. You should have recieved a copy of the text describing + * the FLOSS exception, and it is also available here: + * http://www.alfresco.com/legal/licensing" + */ + +package org.alfresco.repo.forms.processor.workflow; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +import org.alfresco.repo.forms.FormException; +import org.alfresco.service.cmr.dictionary.DataTypeDefinition; +import org.alfresco.service.cmr.dictionary.PropertyDefinition; +import org.json.JSONArray; +import org.json.JSONException; +import org.springframework.extensions.surf.util.I18NUtil; + +/** + * @author Nick Smith + */ +public class TypedPropertyValueGetter +{ + public static final String ON = "on"; + + public Serializable getValue(Object value, PropertyDefinition propDef) + { + if (value == null) + { + return null; + } + + Serializable typedValue = null; + // before persisting check data type of property + if (propDef.isMultiValued()) + { + typedValue = processMultiValuedType(value); + } + else if (isBooleanProperty(propDef)) + { + typedValue = processBooleanValue(value); + } + else if (isLocaleProperty(propDef)) + { + typedValue = processLocaleValue(value); + } + else if (value instanceof String) + { + String valStr = (String) value; + + // make sure empty strings stay as empty strings, everything else + // should be represented as null + if(valStr.isEmpty() && !isTextProperty(propDef)) + { + // Do nothing, leave typedValue as null. + } + else + { + typedValue = valStr; + } + } + else if (value instanceof Serializable) + { + typedValue = (Serializable) value; + } + else + { + throw new FormException("Property values must be of a Serializable type! Value type: " + value.getClass()); + } + return typedValue; + } + + private boolean isTextProperty(PropertyDefinition propDef) + { + return propDef.getDataType().getName().equals(DataTypeDefinition.TEXT) || + propDef.getDataType().getName().equals(DataTypeDefinition.MLTEXT); + } + + private Boolean processBooleanValue(Object value) + { + // check for browser representation of true, that being "on" + if (value instanceof String && ON.equals(value)) + { + return Boolean.TRUE; + } + else + { + return Boolean.FALSE; // TODO Check this line is OK with Gav. + } + } + + private boolean isBooleanProperty(PropertyDefinition propDef) + { + return propDef.getDataType().getName().equals(DataTypeDefinition.BOOLEAN); + } + + private Serializable processLocaleValue(Object value) + { + if (value instanceof String) + { + return I18NUtil.parseLocale((String) value); + } + else + { + throw new FormException("Locale property values must be represented as a String! Value is of type: " + + value.getClass()); + } + } + + private boolean isLocaleProperty(PropertyDefinition propDef) + { + return propDef.getDataType().getName().equals(DataTypeDefinition.LOCALE); + } + + private Serializable processMultiValuedType(Object value) + { + // depending on client the value could be a comma separated string, + // a List object or a JSONArray object + if (value instanceof String) + { + String stringValue = (String) value; + return processMultiValueString(stringValue); + } + else if (value instanceof JSONArray) + { + // if value is a JSONArray convert to List of Object + JSONArray jsonArr = (JSONArray) value; + return processJSONArray(jsonArr); + } + else if (value instanceof List) + { + // persist the list + return (Serializable) value; + } + else + { + throw new FormException("The value is an unsupported multi-value type: " + value); + } + } + + private Serializable processJSONArray(JSONArray jsonArr) + { + int arrLength = jsonArr.length(); + ArrayList list = new ArrayList(arrLength); + try + { + for (int x = 0; x < arrLength; x++) + { + list.add(jsonArr.get(x)); + } + } + catch (JSONException je) + { + throw new FormException("Failed to convert JSONArray to List", je); + } + return list; + } + + private Serializable processMultiValueString(String stringValue) + { + if (stringValue.length() == 0) + { + // empty string for multi-valued properties + // should be stored as null + return null; + } + else + { + // if value is a String convert to List of String persist the List + String[] values = stringValue.split(","); + List valueList = Arrays.asList(values); + return new ArrayList(valueList); + } + } +}