diff --git a/config/alfresco/dbscripts/upgrade/2.2/org.hibernate.dialect.MySQLInnoDBDialect/upgrade-from-2.1.sql b/config/alfresco/dbscripts/upgrade/2.2/org.hibernate.dialect.MySQLInnoDBDialect/upgrade-from-2.1.sql index b2566c1531..f62d6a509c 100644 --- a/config/alfresco/dbscripts/upgrade/2.2/org.hibernate.dialect.MySQLInnoDBDialect/upgrade-from-2.1.sql +++ b/config/alfresco/dbscripts/upgrade/2.2/org.hibernate.dialect.MySQLInnoDBDialect/upgrade-from-2.1.sql @@ -1047,12 +1047,16 @@ ALTER TABLE alf_map_attribute_entries ; ALTER TABLE alf_transaction DROP INDEX idx_commit_time_ms; -- (optional) +ALTER TABLE alf_transaction + ADD COLUMN commit_time_ms BIGINT NULL +; -- (optional) ALTER TABLE alf_transaction DROP INDEX FKB8761A3A9AE340B7, DROP FOREIGN KEY FKB8761A3A9AE340B7, ADD INDEX fk_alf_txn_svr (server_id), ADD CONSTRAINT fk_alf_txn_svr FOREIGN KEY (server_id) REFERENCES alf_server (id), ADD INDEX idx_alf_txn_ctms (commit_time_ms) ; +UPDATE alf_transaction SET commit_time_ms = id WHERE commit_time_ms IS NULL; ALTER TABLE avm_child_entries DROP INDEX fk_avm_ce_child, DROP FOREIGN KEY fk_avm_ce_child; -- (optional) ALTER TABLE avm_child_entries DROP INDEX fk_avm_ce_parent, DROP FOREIGN KEY fk_avm_ce_parent; -- (optional) diff --git a/config/alfresco/public-services-security-context.xml b/config/alfresco/public-services-security-context.xml index aed7aac5e3..c62f51a069 100644 --- a/config/alfresco/public-services-security-context.xml +++ b/config/alfresco/public-services-security-context.xml @@ -366,6 +366,7 @@ org.alfresco.service.cmr.repository.NodeService.getProperties=ACL_NODE.0.sys:base.ReadProperties org.alfresco.service.cmr.repository.NodeService.getProperty=ACL_NODE.0.sys:base.ReadProperties org.alfresco.service.cmr.repository.NodeService.setProperties=ACL_NODE.0.sys:base.WriteProperties + org.alfresco.service.cmr.repository.NodeService.addProperties=ACL_NODE.0.sys:base.WriteProperties org.alfresco.service.cmr.repository.NodeService.setProperty=ACL_NODE.0.sys:base.WriteProperties org.alfresco.service.cmr.repository.NodeService.removeProperty=ACL_NODE.0.sys:base.WriteProperties org.alfresco.service.cmr.repository.NodeService.getParentAssocs=ACL_NODE.0.sys:base.ReadProperties diff --git a/source/java/org/alfresco/repo/avm/AVMNodeService.java b/source/java/org/alfresco/repo/avm/AVMNodeService.java index 37b95e1f37..04c1ce9456 100644 --- a/source/java/org/alfresco/repo/avm/AVMNodeService.java +++ b/source/java/org/alfresco/repo/avm/AVMNodeService.java @@ -1343,6 +1343,14 @@ public class AVMNodeService extends AbstractNodeServiceImpl implements NodeServi } } + public void addProperties(NodeRef nodeRef, Map properties) + { + // Overwrite the current properties + Map currentProperties = getProperties(nodeRef); + currentProperties.putAll(properties); + setProperties(nodeRef, currentProperties); + } + static QName [] fgBuiltinProperties = new QName [] { ContentModel.PROP_CREATED, diff --git a/source/java/org/alfresco/repo/copy/CopyServiceImpl.java b/source/java/org/alfresco/repo/copy/CopyServiceImpl.java index f434b2063d..a6291b9228 100644 --- a/source/java/org/alfresco/repo/copy/CopyServiceImpl.java +++ b/source/java/org/alfresco/repo/copy/CopyServiceImpl.java @@ -608,7 +608,8 @@ public class CopyServiceImpl implements CopyService Collection policies = this.onCopyNodeDelegate.getList(sourceClassRef); if (policies.isEmpty() == true) { - defaultOnCopy(sourceClassRef, sourceNodeRef, copyDetails); + Map sourceNodeProperties = this.nodeService.getProperties(sourceNodeRef); + defaultOnCopy(sourceClassRef, sourceNodeRef, sourceNodeProperties, copyDetails); } else { @@ -626,7 +627,11 @@ public class CopyServiceImpl implements CopyService * @param sourceNodeRef the source node reference * @param copyDetails details of the state being copied */ - private void defaultOnCopy(QName classRef, NodeRef sourceNodeRef, PolicyScope copyDetails) + private void defaultOnCopy( + QName classRef, + NodeRef sourceNodeRef, + Map sourceNodeProperties, + PolicyScope copyDetails) { ClassDefinition classDefinition = this.dictionaryService.getClass(classRef); if (classDefinition != null) @@ -641,8 +646,11 @@ public class CopyServiceImpl implements CopyService Map propertyDefinitions = classDefinition.getProperties(); for (QName propertyName : propertyDefinitions.keySet()) { - Serializable propValue = this.nodeService.getProperty(sourceNodeRef, propertyName); - copyDetails.addProperty(classDefinition.getName(), propertyName, propValue); + Serializable propValue = sourceNodeProperties.get(propertyName); + if (propValue != null) + { + copyDetails.addProperty(classDefinition.getName(), propertyName, propValue); + } } // Copy the associations (child and target) @@ -684,10 +692,16 @@ public class CopyServiceImpl implements CopyService Map props = copyDetails.getProperties(); if (props != null) { + Map copyProps = new HashMap(props); for (QName propName : props.keySet()) { - this.nodeService.setProperty(destinationNodeRef, propName, props.get(propName)); + Serializable value = props.get(propName); + if (value == null) + { + copyProps.remove(propName); + } } + this.nodeService.addProperties(destinationNodeRef, copyProps); } } diff --git a/source/java/org/alfresco/repo/exporter/ExporterComponent.java b/source/java/org/alfresco/repo/exporter/ExporterComponent.java index d215a2a731..136b3a34c9 100644 --- a/source/java/org/alfresco/repo/exporter/ExporterComponent.java +++ b/source/java/org/alfresco/repo/exporter/ExporterComponent.java @@ -579,7 +579,7 @@ public class ExporterComponent catch(TypeConversionException e) { exporter.warning("Value of property " + property + " could not be converted to xml string"); - exporter.value(nodeRef, property, value.toString(), index); + exporter.value(nodeRef, property, (value == null ? null : value.toString()), index); } } else @@ -590,7 +590,15 @@ public class ExporterComponent { // export an empty url for the content ContentData contentData = (ContentData)value; - ContentData noContentURL = new ContentData("", contentData.getMimetype(), contentData.getSize(), contentData.getEncoding()); + ContentData noContentURL = null; + if (contentData == null) + { + noContentURL = new ContentData("", null, 0L, "UTF-8"); + } + else + { + noContentURL = new ContentData("", contentData.getMimetype(), contentData.getSize(), contentData.getEncoding()); + } exporter.content(nodeRef, property, null, noContentURL, index); exporter.warning("Skipped content for property " + property + " on node " + nodeRef); } diff --git a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java index b20d4f8029..60dcd9592b 100644 --- a/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java +++ b/source/java/org/alfresco/repo/node/BaseNodeServiceTest.java @@ -110,6 +110,7 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest public static final QName ASPECT_QNAME_MANDATORY = QName.createQName(NAMESPACE, "mandatoryaspect"); public static final QName ASPECT_QNAME_WITH_DEFAULT_VALUE = QName.createQName(NAMESPACE, "withDefaultValue"); public static final QName PROP_QNAME_TEST_TITLE = QName.createQName(NAMESPACE, "title"); + public static final QName PROP_QNAME_TEST_DESCRIPTION = QName.createQName(NAMESPACE, "description"); public static final QName PROP_QNAME_TEST_CONTENT = QName.createQName(NAMESPACE, "content"); public static final QName PROP_QNAME_BOOLEAN_VALUE = QName.createQName(NAMESPACE, "booleanValue"); public static final QName PROP_QNAME_INTEGER_VALUE = QName.createQName(NAMESPACE, "integerValue"); @@ -709,6 +710,36 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest 0, nodeService.getTargetAssocs(sourceNodeRef, RegexQNamePattern.MATCH_ALL).size()); } + /** + * Test {@link https://issues.alfresco.com/jira/browse/ALFCOM-2299 ALFCOM-2299} + */ + public void testAspectRemovalCascadeDelete() throws Exception + { + // Create a node to add the aspect to + NodeRef sourceNodeRef = nodeService.createNode( + rootNodeRef, + ASSOC_TYPE_QNAME_TEST_CHILDREN, + QName.createQName(BaseNodeServiceTest.NAMESPACE, "testAspectRemovalCascadeDelete"), + ContentModel.TYPE_CONTAINER).getChildRef(); + + // Add the aspect to the source node and add a child using an association defined on the aspect + nodeService.addAspect(sourceNodeRef, ASPECT_WITH_ASSOCIATIONS, null); + NodeRef targetNodeRef = nodeService.createNode( + sourceNodeRef, + ASSOC_ASPECT_CHILD_ASSOC, + QName.createQName(BaseNodeServiceTest.NAMESPACE, "testAspectRemovalCascadeDelete"), + ContentModel.TYPE_CONTAINER).getChildRef(); + + assertTrue("Child node must exist", nodeService.exists(targetNodeRef)); + // Now remove the aspect from the source node and check that the target node was cascade-deleted + nodeService.removeAspect(sourceNodeRef, ASPECT_WITH_ASSOCIATIONS); + assertFalse("Child node must have been cascade-deleted", nodeService.exists(targetNodeRef)); + + // Commit for good measure + setComplete(); + endTransaction(); + } + private static final QName ASPECT_QNAME_TEST_RENDERED = QName.createQName(NAMESPACE, "rendered"); private static final QName ASSOC_TYPE_QNAME_TEST_RENDITION = QName.createQName(NAMESPACE, "rendition-page"); private static final QName TYPE_QNAME_TEST_RENDITION_PAGE = QName.createQName(NAMESPACE, "rendition-page"); @@ -1205,6 +1236,26 @@ public abstract class BaseNodeServiceTest extends BaseSpringTest } } + public void setAddProperties() throws Exception + { + Map properties = nodeService.getProperties(rootNodeRef); + // Add an aspect with a default value + nodeService.addAspect(rootNodeRef, ASPECT_QNAME_TEST_TITLED, null); + assertNull("Expected null property", nodeService.getProperty(rootNodeRef, PROP_QNAME_TEST_TITLE)); + assertNull("Expected null property", nodeService.getProperty(rootNodeRef, PROP_QNAME_TEST_DESCRIPTION)); + + // Now add a map of two properties and check + Map addProperties = new HashMap(11); + addProperties.put(PROP_QNAME_TEST_TITLE, "Title"); + addProperties.put(PROP_QNAME_TEST_DESCRIPTION, "Description"); + nodeService.addProperties(rootNodeRef, addProperties); + + // Check + Map checkProperties = nodeService.getProperties(rootNodeRef); + assertEquals("Title", checkProperties.get(PROP_QNAME_TEST_TITLE)); + assertEquals("Description", checkProperties.get(PROP_QNAME_TEST_DESCRIPTION)); + } + public void testRemoveProperty() throws Exception { Map properties = nodeService.getProperties(rootNodeRef); diff --git a/source/java/org/alfresco/repo/node/FullNodeServiceTest.java b/source/java/org/alfresco/repo/node/FullNodeServiceTest.java index d7235e05fb..5a552031ff 100644 --- a/source/java/org/alfresco/repo/node/FullNodeServiceTest.java +++ b/source/java/org/alfresco/repo/node/FullNodeServiceTest.java @@ -204,6 +204,37 @@ public class FullNodeServiceTest extends BaseNodeServiceTest nodeService.getProperty(nodeRef, BaseNodeServiceTest.PROP_QNAME_ML_TEXT_VALUE)); } + public void testMLValuesOnAddProperties() throws Exception + { + Map properties = new HashMap(); + fillProperties(BaseNodeServiceTest.TYPE_QNAME_TEST_MANY_PROPERTIES, properties); + // Replace the MLText value with a plain string + properties.put(BaseNodeServiceTest.PROP_QNAME_ML_TEXT_VALUE, "Bonjour"); + // Now switch to French + I18NUtil.setContentLocale(Locale.FRENCH); + // Add an aspect + NodeRef nodeRef = rootNodeRef; + nodeService.addProperties(nodeRef, properties); + // Now switch to English + I18NUtil.setContentLocale(Locale.ENGLISH); + // Set the english property + nodeService.setProperty(nodeRef, BaseNodeServiceTest.PROP_QNAME_ML_TEXT_VALUE, "Hello"); + + // Switch back to French and get the value + I18NUtil.setContentLocale(Locale.FRENCH); + assertEquals( + "Expected French value property", + "Bonjour", + nodeService.getProperty(nodeRef, BaseNodeServiceTest.PROP_QNAME_ML_TEXT_VALUE)); + + // Switch back to English and get the value + I18NUtil.setContentLocale(Locale.ENGLISH); + assertEquals( + "Expected English value property", + "Hello", + nodeService.getProperty(nodeRef, BaseNodeServiceTest.PROP_QNAME_ML_TEXT_VALUE)); + } + /** * {@inheritDoc} * diff --git a/source/java/org/alfresco/repo/node/MLPropertyInterceptor.java b/source/java/org/alfresco/repo/node/MLPropertyInterceptor.java index de2db3b9cd..e86b1efe8b 100644 --- a/source/java/org/alfresco/repo/node/MLPropertyInterceptor.java +++ b/source/java/org/alfresco/repo/node/MLPropertyInterceptor.java @@ -215,6 +215,27 @@ public class MLPropertyInterceptor implements MethodInterceptor nodeService.setProperties(nodeRef, convertedProperties); // Done } + else if (methodName.equals("addProperties")) + { + NodeRef nodeRef = (NodeRef) args[0]; + Map newProperties =(Map) args[1]; + + // Get the pivot translation, if appropriate + NodeRef pivotNodeRef = getPivotNodeRef(nodeRef); + + // Get the current properties for the node + Map currentProperties = nodeService.getProperties(nodeRef); + // Convert all properties + Map convertedProperties = convertInboundProperties( + currentProperties, + newProperties, + contentLocale, + nodeRef, + pivotNodeRef); + // Now complete the call by passing the converted properties + nodeService.addProperties(nodeRef, convertedProperties); + // Done + } else if (methodName.equals("setProperty")) { NodeRef nodeRef = (NodeRef) args[0]; diff --git a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java index 27a96fa658..420365ec0d 100644 --- a/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java +++ b/source/java/org/alfresco/repo/node/db/DbNodeServiceImpl.java @@ -627,6 +627,7 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl // Remove child associations // We have to iterate over the associations and remove all those between the parent and child final List> assocsToDelete = new ArrayList>(5); + final List> nodesToDelete = new ArrayList>(5); NodeDaoService.ChildAssocRefQueryCallback callback = new NodeDaoService.ChildAssocRefQueryCallback() { public boolean handle( @@ -635,8 +636,16 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl Pair childNodePair ) { - // Add it - assocsToDelete.add(childAssocPair); + // Double check that it's not a primary association. If so, we can't delete it and + // have to delete the child node directly and with full archival. + if (childAssocPair.getSecond().isPrimary()) + { + nodesToDelete.add(childNodePair); + } + else + { + assocsToDelete.add(childAssocPair); + } // No recurse return false; } @@ -656,6 +665,13 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl invokeOnDeleteChildAssociation(assocRef); } + // Cascade-delete any nodes that were attached to primary associations + for (Pair childNodePair : nodesToDelete) + { + NodeRef childNodeRef = childNodePair.getSecond(); + this.deleteNode(childNodeRef); + } + // Remove regular associations Map nodeAssocDefs = aspectDef.getAssociations(); Collection> nodeAssocPairs = nodeDaoService.getNodeAssocsToAndFrom(nodeId); @@ -910,26 +926,44 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst(); QName assocTypeQName = childAssocRef.getTypeQName(); QName assocQName = childAssocRef.getQName(); - // Delete the association - invokeBeforeDeleteChildAssociation(childAssocRef); - boolean deleted = nodeDaoService.deleteChildAssoc(parentNodeId, childNodeId, assocTypeQName, assocQName); - if (deleted) + Pair assocPair = nodeDaoService.getChildAssoc( + parentNodeId, childNodeId, assocTypeQName, assocQName); + if (assocPair == null) { - invokeOnDeleteChildAssociation(childAssocRef); + // No association exists + return false; + } + Long assocId = assocPair.getFirst(); + ChildAssociationRef assocRef = assocPair.getSecond(); + if (assocRef.isPrimary()) + { + NodeRef childNodeRef = assocRef.getChildRef(); + // Delete the child node + this.deleteNode(childNodeRef); + // Done + return true; + } + else + { + // Delete the association + invokeBeforeDeleteChildAssociation(childAssocRef); + nodeDaoService.deleteChildAssoc(assocId); + invokeOnDeleteChildAssociation(childAssocRef); + // Index + nodeIndexer.indexDeleteChildAssociation(childAssocRef); + // Done + return true; } - // Index - nodeIndexer.indexDeleteChildAssociation(childAssocRef); - // Done - return deleted; } public boolean removeSeconaryChildAssociation(ChildAssociationRef childAssocRef) { Long parentNodeId = getNodePairNotNull(childAssocRef.getParentRef()).getFirst(); Long childNodeId = getNodePairNotNull(childAssocRef.getChildRef()).getFirst(); - QName typeQName = childAssocRef.getTypeQName(); - QName qname = childAssocRef.getQName(); - Pair assocPair = nodeDaoService.getChildAssoc(parentNodeId, childNodeId, typeQName, qname); + QName assocTypeQName = childAssocRef.getTypeQName(); + QName assocQName = childAssocRef.getQName(); + Pair assocPair = nodeDaoService.getChildAssoc( + parentNodeId, childNodeId, assocTypeQName, assocQName); if (assocPair == null) { // No association exists @@ -1150,6 +1184,34 @@ public class DbNodeServiceImpl extends AbstractNodeServiceImpl nodeIndexer.indexUpdateNode(nodeRef); } + public void addProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException + { + Pair nodePair = getNodePairNotNull(nodeRef); + Long nodeId = nodePair.getFirst(); + + extractIntrinsicProperties(properties); + + // Invoke policy behaviours + Map propertiesBefore = getPropertiesImpl(nodePair); + invokeBeforeUpdateNode(nodeRef); + + // Change each property + for (Map.Entry entry : properties.entrySet()) + { + QName propertyQName = entry.getKey(); + Serializable propertyValue = entry.getValue(); + setPropertyImpl(nodeId, propertyQName, propertyValue); + } + + // Invoke policy behaviours + Map propertiesAfter = getPropertiesImpl(nodePair); + invokeOnUpdateNode(nodeRef); + invokeOnUpdateProperties(nodeRef, propertiesBefore, propertiesAfter); + + // Index + nodeIndexer.indexUpdateNode(nodeRef); + } + private void setPropertiesImpl(Long nodeId, Map properties) { // Get the cm:name and uuid for special handling diff --git a/source/java/org/alfresco/repo/search/impl/lucene/index/IndexInfo.java b/source/java/org/alfresco/repo/search/impl/lucene/index/IndexInfo.java index b32d58d901..9a75570361 100644 --- a/source/java/org/alfresco/repo/search/impl/lucene/index/IndexInfo.java +++ b/source/java/org/alfresco/repo/search/impl/lucene/index/IndexInfo.java @@ -2416,9 +2416,21 @@ public class IndexInfo implements IndexMonitor */ public static void main(String[] args) throws Throwable { - - String indexLocation = args[0]; - IndexInfo ii = new IndexInfo(new File(indexLocation), null); + for (int i = 0; i < args.length; i++) + { + File indexLocation = new File(args[i]); + if (!indexLocation.exists()) + { + System.err.println("Index directory doesn't exist: " + indexLocation); + continue; + } + readIndexInfo(indexLocation); + } + } + + private static void readIndexInfo(File indexLocation) throws Throwable + { + IndexInfo ii = new IndexInfo(indexLocation, null); ii.readWriteLock.writeLock().lock(); try diff --git a/source/java/org/alfresco/repo/version/NodeServiceImpl.java b/source/java/org/alfresco/repo/version/NodeServiceImpl.java index df20dbd8ee..7380353e7a 100644 --- a/source/java/org/alfresco/repo/version/NodeServiceImpl.java +++ b/source/java/org/alfresco/repo/version/NodeServiceImpl.java @@ -402,6 +402,15 @@ public class NodeServiceImpl implements NodeService, VersionModel throw new UnsupportedOperationException(MSG_UNSUPPORTED); } + /** + * @throws UnsupportedOperationException always + */ + public void addProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException + { + // This operation is not supported for a version store + throw new UnsupportedOperationException(MSG_UNSUPPORTED); + } + /** * @throws UnsupportedOperationException always */ diff --git a/source/java/org/alfresco/service/cmr/repository/NodeService.java b/source/java/org/alfresco/service/cmr/repository/NodeService.java index 13c94788eb..211391fb08 100644 --- a/source/java/org/alfresco/service/cmr/repository/NodeService.java +++ b/source/java/org/alfresco/service/cmr/repository/NodeService.java @@ -382,13 +382,25 @@ public interface NodeService *

* NOTE: Null values are allowed. * - * @param nodeRef - * @param properties all the properties of the node keyed by their qualified names + * @param nodeRef the node to chance + * @param properties all the properties of the node keyed by their qualified names * @throws InvalidNodeRefException if the node could not be found */ @Auditable(key = Auditable.Key.ARG_0 ,parameters = {"nodeRef", "properties"}) public void setProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException; + /** + * Add all given properties to the node. + *

+ * NOTE: Null values are allowed and will replace the existing value. + * + * @param nodeRef the node to change + * @param properties the properties to change, keyed by their qualified names + * @throws InvalidNodeRefException if the node could not be found + */ + @Auditable(key = Auditable.Key.ARG_0 ,parameters = {"nodeRef", "properties"}) + public void addProperties(NodeRef nodeRef, Map properties) throws InvalidNodeRefException; + /** * Sets the value of a property to be any Serializable instance. *