diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/metadata-delegation-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/metadata-delegation-context.xml
new file mode 100644
index 0000000000..106cf0cb90
--- /dev/null
+++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/metadata-delegation-context.xml
@@ -0,0 +1,123 @@
+
+
+
+
+
+
+
+
+
+
+
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationAdminService
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${server.transaction.mode.default}
+
+
+
+
+
+
+
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationAdminService.attachDelegate=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationAdminService.detachDelegate=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationAdminService.getDefinedDelegations=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationAdminService.getDelegationFor=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationAdminService.getDelegationsFrom=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationAdminService.*=ACL_DENY
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationService
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ ${server.transaction.mode.default}
+
+
+
+
+
+
+
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationService.hasDelegateForAspect=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationService.getDelegateFor=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationService.getDelegateProperties=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationService.getDelegateProperty=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationService.hasAspectOnDelegate=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationService.getDelegations=ACL_ALLOW
+ org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationService.*=ACL_DENY
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/rm-server/config/alfresco/module/org_alfresco_module_rm/module-context.xml b/rm-server/config/alfresco/module/org_alfresco_module_rm/module-context.xml
index f75a9ff12d..921a34aaf0 100644
--- a/rm-server/config/alfresco/module/org_alfresco_module_rm/module-context.xml
+++ b/rm-server/config/alfresco/module/org_alfresco_module_rm/module-context.xml
@@ -267,9 +267,12 @@
+
+
+
-
+
diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/Delegation.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/Delegation.java
new file mode 100644
index 0000000000..05a2e3caef
--- /dev/null
+++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/Delegation.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.InvalidDelegation;
+import org.alfresco.service.cmr.dictionary.DictionaryService;
+import org.alfresco.service.namespace.QName;
+
+import java.util.Collections;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A Delegation is a definition of a {@link #aspects set of aspects} whose metadata are to be delegated.
+ * Using a Delegation, you can attach a delegate node to any node and {@code hasAspect} and
+ * {@code getPropert[y|ies]} calls can be delegated to the delegate node.
+ *
+ * The connection between the nodes is made with a specified {@link #assocType peer association}.
+ *
+ * Note that a Delegation is not an instance of a link between two nodes, but the definition of such a link.
+ *
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public class Delegation
+{
+ private DictionaryService dictionaryService;
+ private DelegationRegistry delegationRegistry;
+ private Set aspects;
+ private QName assocType;
+
+ public Delegation()
+ {
+ // Intentionally empty
+ }
+
+ public void setDictionaryService(DictionaryService service)
+ {
+ this.dictionaryService = service;
+ }
+
+ public void setDelegationRegistry(DelegationRegistry registry)
+ {
+ this.delegationRegistry = registry;
+ }
+
+ public void setAssocType(QName assocType)
+ {
+ this.assocType = assocType;
+ }
+
+ public void setAspects(Set aspects)
+ {
+ this.aspects = aspects;
+ }
+
+ public void validateAndRegister()
+ {
+ if (this.assocType == null)
+ {
+ throw new InvalidDelegation("Illegal null assocType");
+ }
+ if (aspects == null || aspects.isEmpty())
+ {
+ throw new InvalidDelegation("Illegal null or empty aspects set");
+ }
+ if (dictionaryService.getAssociation(assocType) == null)
+ {
+ throw new InvalidDelegation("Association not found: " + assocType);
+ }
+ for (QName aspect : aspects)
+ {
+ if (dictionaryService.getAspect(aspect) == null)
+ {
+ throw new InvalidDelegation("Aspect not found: " + aspect);
+ }
+ }
+
+ this.delegationRegistry.register(this);
+ }
+
+ /** Gets the type of the peer association linking the node to its delegate. */
+ public QName getAssocType()
+ {
+ return assocType;
+ }
+
+ /** Gets the set of aspects which are being delegated. */
+ public Set getAspects()
+ {
+ return Collections.unmodifiableSet(aspects);
+ }
+
+ @Override public int hashCode()
+ {
+ return Objects.hash(aspects, assocType);
+ }
+
+ @Override public boolean equals(Object other)
+ {
+ boolean result = false;
+ if (other instanceof Delegation)
+ {
+ Delegation that = (Delegation)other;
+ result = this.aspects.equals(that.aspects) &&
+ this.assocType.equals(that.assocType);
+ }
+ return result;
+ }
+
+ @Override public String toString()
+ {
+ StringBuilder result = new StringBuilder();
+ result.append(this.getClass().getSimpleName()).append(':')
+ .append("--").append(assocType).append("->")
+ .append(aspects);
+ return result.toString();
+ }
+}
diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationAdminService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationAdminService.java
new file mode 100644
index 0000000000..0a49ce4d2c
--- /dev/null
+++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationAdminService.java
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import static org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.ChainedDelegationUnsupported;
+
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.namespace.QName;
+
+import java.util.Set;
+
+/**
+ * A service to manage the delegation of aspect metadata.
+ * Using this service a node can be {@link #attachDelegate linked} to a delegate node for a configured set of aspects.
+ * (Note that this delegate node must already exist within the database.)
+ * Then any read request for relevant metadata such as hasAspect or getProperties can be delegated to the
+ * linked node.
+ *
+ * For a link to be made, there must be a {@link #getDefinedDelegations() defined Delegation} already in the system.
+ * This means that a peer-association type will have to have been declared and that a spring bean will have to have
+ * defined which aspects are to be handled by this {@code Delegation}.
+ *
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public interface DelegationAdminService
+{
+ /**
+ * Creates a link between two nodes such that the first {@code nodeRef} can 'inherit' or reuse some aspect
+ * metadata from another node - the {@code delegateNodeRef}.
+ *
+ * Note that links can currently only extend between two pairs of nodes and cannot be chained.
+ *
+ * @param nodeRef the node which is to inherit additional metadata.
+ * @param delegateNodeRef the node which will provide the additional metadata.
+ * @param assocType the type of the peer association which will link the two nodes.
+ * @return a {@link Delegation} object which defines the link type.
+ * @throws ChainedDelegationUnsupported if an attempt is made to attach nodes such that a chain would be made.
+ */
+ Delegation attachDelegate(NodeRef nodeRef, NodeRef delegateNodeRef, QName assocType);
+
+ /**
+ * Removes an existing metadata delegation link between two nodes.
+ *
+ * @param nodeRef the node which has been linked to a delegate.
+ * @param assocType the type of the peer assocation forming the link.
+ * @return the removed {@link Delegation}.
+ */
+ Delegation detachDelegate(NodeRef nodeRef, QName assocType);
+
+ /**
+ * Gets the set of defined {@link Delegation}s.
+ *
+ * @return the set of defined Delegations.
+ */
+ Set getDefinedDelegations();
+
+ /**
+ * Gets the {@link Delegation} which contains the specified {@code aspectName} if there is one.
+ * Note that only one {@link Delegation} may declare that it handles any particular aspect.
+ *
+ * @param aspectName the name of the aspect whose {@link Delegation} is sought.
+ * @return the {@link Delegation} which handles the specified aspect, if there is one.
+ */
+ Delegation getDelegationFor(QName aspectName);
+
+ /**
+ * Gets the set of {@link Delegation}s which are in effect from the specified {@code nodeRef}.
+ * From these, you can retrieve the types of peer associations which are linked to the specified
+ * {@code nodeRef} as well as the aspect types that are handled.
+ *
+ * @param nodeRef the NodeRef whose delegations are sought.
+ * @return the set of {@link Delegation}s from the specified nodeRef.
+ */
+ Set getDelegationsFrom(NodeRef nodeRef);
+}
diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationAdminServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationAdminServiceImpl.java
new file mode 100644
index 0000000000..0a2463ba05
--- /dev/null
+++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationAdminServiceImpl.java
@@ -0,0 +1,167 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import static org.alfresco.util.collections.CollectionUtils.transform;
+
+import org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.ChainedDelegationUnsupported;
+import org.alfresco.service.cmr.repository.AssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.collections.Function;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public class DelegationAdminServiceImpl implements DelegationAdminService
+{
+ private DelegationRegistry registry;
+ private NodeService nodeService;
+
+ public void setNodeService(NodeService service)
+ {
+ this.nodeService = service;
+ }
+
+ public void setDelegationRegistry(DelegationRegistry registry)
+ {
+ this.registry = registry;
+ }
+
+ @Override public Delegation attachDelegate(NodeRef nodeRef, NodeRef delegateNodeRef, QName assocType)
+ {
+ final Delegation delegation = getDelegationForAssociation(assocType);
+
+ // Prevent the creation of chains of delegation from node A to B to C.
+
+ // If any nodes are already delegating to nodeRef for the specified assoc, then we can't chain.
+ final List existingDelegatorAssocs = nodeService.getSourceAssocs(nodeRef, assocType);
+ if ( !existingDelegatorAssocs.isEmpty())
+ {
+ final List existingDelegators = transform(existingDelegatorAssocs,
+ new Function()
+ {
+ @Override public NodeRef apply(AssociationRef assocRef)
+ {
+ return assocRef.getSourceRef();
+ }
+ });
+ throw new ChainedDelegationUnsupported("Cannot attach delegate", existingDelegators);
+ }
+
+ // Likewise if this delegate node is already itself delegating elsewhere, we cannot chain.
+ final List existingDelegateAssocs = nodeService.getTargetAssocs(delegateNodeRef, assocType);
+ if ( !existingDelegateAssocs.isEmpty())
+ {
+ // If it's not empty, it should only have one value in it, but just in case...
+ final List existingDelegates = transform(existingDelegateAssocs,
+ new Function()
+ {
+ @Override public NodeRef apply(AssociationRef assocRef)
+ {
+ return assocRef.getTargetRef();
+ }
+ });
+ throw new ChainedDelegationUnsupported("Cannot attach delegate", existingDelegates);
+ }
+
+ // OK. We're good to go. We're not making a chain here.
+ nodeService.createAssociation(nodeRef, delegateNodeRef, assocType);
+
+ return delegation;
+ }
+
+ private Delegation getDelegationForAssociation(QName assocType)
+ {
+ final Delegation delegation = registry.getDelegateForAssociation(assocType);
+
+ if (delegation == null)
+ {
+ throw new IllegalArgumentException("No " + Delegation.class.getSimpleName() +
+ " configured for assocType " + assocType);
+ }
+ return delegation;
+ }
+
+ @Override public Delegation detachDelegate(NodeRef nodeRef, QName assocType)
+ {
+ // Is the association there?
+ final List assocs = nodeService.getTargetAssocs(nodeRef, assocType);
+
+ if (assocs == null || assocs.isEmpty())
+ {
+ return null;
+ }
+ else
+ {
+ Delegation result = getDelegationForAssociation(assocType);
+
+ // There should only be one such association... but we'll remove them all just in case
+ for (AssociationRef assocRef : assocs)
+ {
+ nodeService.removeAssociation(nodeRef, assocRef.getTargetRef(), assocType);
+ }
+
+ return result;
+ }
+ }
+
+ @Override public Delegation getDelegationFor(QName aspectName)
+ {
+ Delegation delegation = null;
+
+ for (Delegation d : getDefinedDelegations())
+ {
+ if (d.getAspects().contains(aspectName))
+ {
+ delegation = d;
+ break;
+ }
+ }
+ return delegation;
+ }
+
+ @Override public Set getDelegationsFrom(NodeRef nodeRef)
+ {
+ final Set allDelegations = getDefinedDelegations();
+
+ final Set result = new HashSet<>();
+ for (Delegation d : allDelegations)
+ {
+ final QName assocType = d.getAssocType();
+ if ( !nodeService.getTargetAssocs(nodeRef, assocType).isEmpty())
+ {
+ result.add(d);
+ }
+ }
+
+ return result;
+ }
+
+ @Override public Set getDefinedDelegations()
+ {
+ return registry.getDelegations();
+ }
+}
diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationException.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationException.java
new file mode 100644
index 0000000000..ea978abf7b
--- /dev/null
+++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationException.java
@@ -0,0 +1,107 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import org.alfresco.error.AlfrescoRuntimeException;
+import org.alfresco.service.cmr.repository.NodeRef;
+
+import java.util.List;
+
+/**
+ * Generic class for any runtime exceptions related to metadata delegates.
+ *
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public class DelegationException extends AlfrescoRuntimeException
+{
+ public DelegationException(String msgId) { super(msgId); }
+ public DelegationException(String msgId, Throwable cause) { super(msgId, cause); }
+
+ public static class InvalidDelegation extends DelegationException
+ {
+ public InvalidDelegation(String msgId)
+ {
+ super(msgId);
+ }
+ }
+
+ /** A Metadata Delegation already exists. */
+ public static class DelegationAlreadyExists extends DelegationException
+ {
+ private final Delegation delegation;
+
+ public DelegationAlreadyExists(String msgId, Delegation delegation)
+ {
+ super(msgId);
+ this.delegation = delegation;
+ }
+ }
+
+ /**
+ * A {@link Delegation} has not been found.
+ * Remember that a Delegation is the definition of a type of link.
+ */
+ public static class DelegationNotFound extends DelegationException
+ {
+ public DelegationNotFound(String msgId)
+ {
+ super(msgId);
+ }
+ }
+
+ /**
+ * A Delegate has not been found.
+ * Remember that a Delegate is an instance of a link between two nodes.
+ */
+ public static class DelegateNotFound extends DelegationException
+ {
+ public DelegateNotFound(String msgId)
+ {
+ super(msgId);
+ }
+ }
+
+ /**
+ * Exception to report that we currently do not support chained delegation.
+ */
+ public static class ChainedDelegationUnsupported extends DelegationException
+ {
+ private final List nodesAlreadyDelegating;
+
+ public ChainedDelegationUnsupported(String msgId, List nodesAlreadyDelegating)
+ {
+ super(msgId);
+ this.nodesAlreadyDelegating = nodesAlreadyDelegating;
+ }
+
+ public List getNodesAlreadyDelegating()
+ {
+ return this.nodesAlreadyDelegating;
+ }
+
+ @Override public String toString()
+ {
+ StringBuilder msg = new StringBuilder();
+ msg.append(this.getClass().getSimpleName()).append(" Already delegating from: ")
+ .append(nodesAlreadyDelegating.toString());
+ return msg.toString();
+ }
+ }
+}
diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationRegistry.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationRegistry.java
new file mode 100644
index 0000000000..54133ee40b
--- /dev/null
+++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationRegistry.java
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.DelegationAlreadyExists;
+import org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.InvalidDelegation;
+import org.alfresco.service.namespace.QName;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * This is a registry of {@link Delegation delegations} which have been defined in the system.
+ *
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public class DelegationRegistry
+{
+ private final Set delegations = new HashSet<>();
+
+ public void register(Delegation delegation)
+ {
+ // Various validation steps to do here to ensure we get consistent, sensible Delegations registered.
+ if (delegations.contains(delegation))
+ {
+ throw new DelegationAlreadyExists("Cannot register duplicate delegation", delegation);
+ }
+ for (Delegation existingDelegation : delegations)
+ {
+ if (existingDelegation.getAssocType().equals(delegation.getAssocType()))
+ {
+ throw new InvalidDelegation("Cannot register two delegations with the same assocType. " +
+ "Existing: " + existingDelegation +
+ " New: " + delegation);
+ }
+ // Yes this is a for loop inside a for loop but we're assuming these sets will not be large.
+ for (QName existingAspect : existingDelegation.getAspects())
+ {
+ if (delegation.getAspects().contains(existingAspect))
+ {
+ throw new InvalidDelegation("Cannot register two delegations with the same aspect. " +
+ "Existing: " + existingDelegation +
+ " New: " + delegation);
+ }
+ }
+ }
+
+ this.delegations.add(delegation);
+ }
+
+ public Set getDelegations()
+ {
+ return Collections.unmodifiableSet(delegations);
+ }
+
+ public Delegation getDelegateForAssociation(QName assocType)
+ {
+ for (Delegation d : delegations)
+ {
+ if (d.getAssocType().equals(assocType))
+ {
+ return d;
+ }
+ }
+ return null;
+ }
+}
diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationService.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationService.java
new file mode 100644
index 0000000000..b4048761c0
--- /dev/null
+++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationService.java
@@ -0,0 +1,96 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import static org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.DelegationNotFound;
+
+import org.alfresco.service.cmr.repository.InvalidNodeRefException;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.namespace.QName;
+
+import java.io.Serializable;
+import java.util.Map;
+
+/**
+ * This service provides read-only access to delegated metadata.
+ * TODO complete.
+ *
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public interface DelegationService
+{
+ /**
+ * Checks if the specified nodeRef has an attached {@link Delegation} for the specified aspect.
+ *
+ * @param nodeRef the nodeRef which may or may not have a delegate node for the specified aspect.
+ * @param aspectName the name of the aspect for which the node may or may not have delegation.
+ * @return whether the node is delegating metadata reads for the specified aspect.
+ * @throws InvalidNodeRefException if the supplied nodeRef does not exist.
+ * @throws DelegationNotFound if no delegation for the specified aspect has been attached.
+ */
+ boolean hasDelegateForAspect(NodeRef nodeRef, QName aspectName);
+
+ /**
+ * Gets the delegate node for the specified aspect, if there is one.
+ *
+ * @param nodeRef the node with the delegate.
+ * @param aspectName the aspect name.
+ * @return the nodeRef of the delegate if there is one, else {@code null}.
+ * @throws DelegationNotFound if no delegation for the specified aspect has been attached.
+ */
+ NodeRef getDelegateFor(NodeRef nodeRef, QName aspectName);
+
+ /**
+ * Gets all the property values from the delegate node for the specified aspect.
+ *
+ * @param nodeRef the node with the delegate.
+ * @param aspectName the aspect name which holds the properties we want.
+ * @return the property values as obtained from the delegate node.
+ */
+ Map getDelegateProperties(NodeRef nodeRef, QName aspectName);
+
+ /**
+ * Gets the specified property value from the delegate node.
+ *
+ * @param nodeRef the node with the delegate.
+ * @param propertyName the property name which we want.
+ * @return the property value as obtained from the delegate node.
+ */
+ Serializable getDelegateProperty(NodeRef nodeRef, QName propertyName);
+
+ /**
+ * Determines if a given aspect is present on a node's delegates.
+ *
+ * @param nodeRef the node for which a delegate is sought.
+ * @param aspectName the aspect which is to be checked.
+ * @return Returns true if the aspect has been applied to one of the given node's delegates,
+ * otherwise false
+ */
+ boolean hasAspectOnDelegate(NodeRef nodeRef, QName aspectName);
+
+ /**
+ * Gets all {@link Delegation delegations} currently attached to the specified node.
+ *
+ * @param nodeRef the node whose delegations are sought.
+ * @return Returns a map of all {@link Delegation delegations} by NodeRef for the specified nodeRef.
+ */
+ Map getDelegations(NodeRef nodeRef);
+}
+
diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationServiceImpl.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationServiceImpl.java
new file mode 100644
index 0000000000..3b7190c7d5
--- /dev/null
+++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationServiceImpl.java
@@ -0,0 +1,182 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import static org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.DelegateNotFound;
+import static org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.DelegationNotFound;
+import static org.alfresco.util.collections.CollectionUtils.filterKeys;
+
+import org.alfresco.service.cmr.dictionary.ClassDefinition;
+import org.alfresco.service.cmr.dictionary.DictionaryService;
+import org.alfresco.service.cmr.dictionary.PropertyDefinition;
+import org.alfresco.service.cmr.repository.AssociationRef;
+import org.alfresco.service.cmr.repository.InvalidNodeRefException;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.namespace.QName;
+import org.alfresco.util.collections.Function;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public class DelegationServiceImpl implements DelegationService
+{
+ private DelegationAdminService delegationAdminService;
+ private DictionaryService dictionaryService;
+ private NodeService nodeService;
+
+ public void setDelegationAdminService(DelegationAdminService service)
+ {
+ this.delegationAdminService = service;
+ }
+
+ public void setDictionaryService(DictionaryService service)
+ {
+ this.dictionaryService = service;
+ }
+
+ public void setNodeService(NodeService service)
+ {
+ this.nodeService = service;
+ }
+
+ @Override public boolean hasDelegateForAspect(NodeRef nodeRef, QName aspectName)
+ {
+ final Delegation delegation = delegationAdminService.getDelegationFor(aspectName);
+
+ if ( !nodeService.exists(nodeRef))
+ {
+ throw new InvalidNodeRefException(nodeRef);
+ }
+ else if (delegation == null)
+ {
+ throw new DelegationNotFound("No delegation found for aspect: " + aspectName);
+ }
+ else
+ {
+ final List targetAssocs = nodeService.getTargetAssocs(nodeRef, delegation.getAssocType());
+ return !targetAssocs.isEmpty();
+ }
+ }
+
+ @Override public NodeRef getDelegateFor(NodeRef nodeRef, QName aspectName)
+ {
+ final Delegation d = delegationAdminService.getDelegationFor(aspectName);
+
+ if (d == null)
+ {
+ throw new DelegationNotFound("No delegation found for aspect: " + aspectName);
+ }
+ else
+ {
+ final QName assocType = d.getAssocType();
+ final List assocs = nodeService.getTargetAssocs(nodeRef, assocType);
+
+ return assocs.isEmpty() ? null : assocs.get(0).getTargetRef();
+ }
+ }
+
+ @Override public Map getDelegateProperties(NodeRef nodeRef, final QName aspectName)
+ {
+ final NodeRef delegateNode = getDelegateFor(nodeRef, aspectName);
+
+ if (delegateNode == null)
+ {
+ throw new DelegateNotFound("No delegate node found for " + nodeRef + " " + aspectName);
+ }
+ else
+ {
+ Map allProps = nodeService.getProperties(delegateNode);
+ Map aspectProps = filterKeys(allProps,
+ new Function()
+ {
+ @Override public Boolean apply(QName propName)
+ {
+ final QName containerClassname = dictionaryService.getProperty(propName)
+ .getContainerClass()
+ .getName();
+ return containerClassname.equals(aspectName);
+ }
+ });
+ return aspectProps;
+ }
+ }
+
+ @Override public Serializable getDelegateProperty(NodeRef nodeRef, QName propertyName)
+ {
+ final PropertyDefinition propDefn = dictionaryService.getProperty(propertyName);
+
+ if (propDefn == null)
+ {
+ throw new IllegalArgumentException("Property " + propertyName + " not found.");
+ }
+
+ final ClassDefinition aspectDefn = propDefn.getContainerClass();
+ if (!aspectDefn.isAspect())
+ {
+ StringBuilder msg = new StringBuilder();
+ msg.append("Property '").append(propertyName).append("' is not defined on an aspect: ")
+ .append(aspectDefn.getName());
+
+ throw new IllegalArgumentException(msg.toString());
+ }
+
+ Map allPropValues = getDelegateProperties(nodeRef, aspectDefn.getName());
+ return allPropValues.get(propertyName);
+ }
+
+ @Override public boolean hasAspectOnDelegate(NodeRef nodeRef, QName aspectName)
+ {
+ final NodeRef delegateNode = getDelegateFor(nodeRef, aspectName);
+
+ if (delegateNode == null)
+ {
+ throw new DelegateNotFound("No delegate node found for " + nodeRef + " " + aspectName);
+ }
+ else
+ {
+ return nodeService.hasAspect(delegateNode, aspectName);
+ }
+ }
+
+ @Override public Map getDelegations(NodeRef nodeRef)
+ {
+ Set delegations = delegationAdminService.getDelegationsFrom(nodeRef);
+
+ Map result = new HashMap<>();
+ for (Delegation d : delegations)
+ {
+ // We need only use the first aspect to get the Delegation object
+ if (!d.getAspects().isEmpty())
+ {
+ result.put(d, getDelegateFor(nodeRef, d.getAspects().iterator().next()));
+ }
+ }
+
+ return result;
+ }
+}
+
diff --git a/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/package-info.java b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/package-info.java
new file mode 100644
index 0000000000..b35ca04e74
--- /dev/null
+++ b/rm-server/source/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/package-info.java
@@ -0,0 +1,46 @@
+/*
+ * Copyright (C) 2005-2015 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 .
+ */
+/**
+ * This package contains the types that deliver the Metadata Delegation feature.
+ * Metadata delegation allows read-only aspect metadata for any given Alfresco node to
+ * be sourced from another node, the delegate.
+ *
+ * In this way nodes can 'inherit' some of their metadata from another node which may
+ * have benefits when more than one node is required to share some of the same metadata.
+ *
+ * Multiple nodes may share the same delegate node and one node may be linked to multiple
+ * delegates.
+ *
+ * The linking of nodes to their metadata delegates is done with Alfresco peer associations.
+ * Association types must be declared in an Alfresco content model in the normal way.
+ * Spring configuration is used to assign each association type a set of aspects which will
+ * be available from the delegate via the association.
+ *
+ * See {@link org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationAdminService}
+ * for details on how to create and destroy delegation links between nodes.
+ *
+ * The read-only access to delegated metadat is made available via the
+ * See {@link org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationService}
+ * for details on how the data access is performed.
+ *
+ * See {@link org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationRegistry}
+ * for details on what {@link org.alfresco.module.org_alfresco_module_rm.metadatadelegation.Delegation}s
+ * are defined in the system.
+ */
+package org.alfresco.module.org_alfresco_module_rm.metadatadelegation;
\ No newline at end of file
diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationAdminServiceImplUnitTest.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationAdminServiceImplUnitTest.java
new file mode 100644
index 0000000000..1cd6a81caf
--- /dev/null
+++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationAdminServiceImplUnitTest.java
@@ -0,0 +1,141 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import static java.util.Arrays.asList;
+import static org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.ChainedDelegationUnsupported;
+import static org.alfresco.module.org_alfresco_module_rm.test.util.ExceptionUtils.expectedException;
+import static org.alfresco.module.org_alfresco_module_rm.test.util.FPUtils.asSet;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.when;
+
+import org.alfresco.service.cmr.dictionary.DictionaryService;
+import org.alfresco.service.cmr.repository.AssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.namespace.QName;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+/**
+ * Unit tests for {@link DelegationAdminServiceImpl}.
+ *
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public class DelegationAdminServiceImplUnitTest
+{
+ @InjectMocks private final DelegationAdminServiceImpl delegationAdminService = new DelegationAdminServiceImpl();
+
+ @Mock DictionaryService mockDictionaryService;
+ @Mock NodeService mockNodeService;
+ @Mock DelegationRegistry mockRegistry;
+ @Mock DelegationServiceImpl mockDelegationService;
+
+ private final NodeRef node1 = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "node1");
+ private final NodeRef node2 = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "node2");
+ private final NodeRef node3 = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "node3");
+
+ private final QName assoc1 = QName.createQName("test", "assoc1");
+ private final QName assoc2 = QName.createQName("test", "assoc2");
+
+ private final QName aspect1 = QName.createQName("test", "aspect1");
+ private final QName aspect2 = QName.createQName("test", "aspect2");
+ private final QName aspect3 = QName.createQName("test", "aspect3");
+
+ private final Delegation delegate1 = new Delegation()
+ {{
+ this.setAssocType(assoc1);
+ this.setAspects(asSet(aspect1, aspect2));
+ }};
+ private final Delegation delegate2 = new Delegation()
+ {{
+ this.setAssocType(assoc2);
+ this.setAspects(asSet(aspect3));
+ }};
+
+ @Before public void setUp()
+ {
+ MockitoAnnotations.initMocks(this);
+
+ when(mockRegistry.getDelegations()).thenReturn(asSet(delegate1, delegate2));
+ }
+
+ @Test(expected=IllegalArgumentException.class)
+ public void attachingDelegateWithNoAssociationConfiguredShouldFail()
+ {
+ delegationAdminService.attachDelegate(node1, node2, assoc1);
+ }
+
+ @Test public void attachDetach()
+ {
+ when(mockRegistry.getDelegateForAssociation(assoc1)).thenReturn(delegate1);
+
+ // attach
+ Delegation d = attachDelegate(node1, node2, assoc1);
+
+ // validate
+ assertEquals(assoc1, d.getAssocType());
+ assertEquals(asSet(aspect1, aspect2), d.getAspects());
+ assertTrue(mockDelegationService.hasDelegateForAspect(node1, aspect1));
+ assertFalse(mockDelegationService.hasDelegateForAspect(node1, aspect3));
+
+ // detach
+ assertEquals(d, delegationAdminService.detachDelegate(node1, assoc1));
+ }
+
+ private Delegation attachDelegate(NodeRef from, NodeRef to, QName assocType)
+ {
+ Delegation d = delegationAdminService.attachDelegate(from, to, assocType);
+ when(mockNodeService.getSourceAssocs(to, assocType)).thenReturn(asList(new AssociationRef(from, assocType, to)));
+ when(mockNodeService.getTargetAssocs(from, assocType)).thenReturn(asList(new AssociationRef(from, assocType, to)));
+ for (QName aspect : d.getAspects())
+ {
+ when(mockDelegationService.hasDelegateForAspect(from, aspect)).thenReturn(true);
+ }
+ return d;
+ }
+
+ @Test public void chainsOfDelegationShouldBePrevented()
+ {
+ when(mockRegistry.getDelegateForAssociation(assoc1)).thenReturn(delegate1);
+
+ // The node already has a delegation in place: node1 -> node2. We're trying to add to the
+ // end of the chain: node2 -> node3
+ when(mockNodeService.getSourceAssocs(node2, assoc1)).thenReturn(asList(new AssociationRef(node1, assoc1, node2)));
+ when(mockNodeService.getTargetAssocs(node1, assoc1)).thenReturn(asList(new AssociationRef(node1, assoc1, node2)));
+
+ expectedException(ChainedDelegationUnsupported.class, () -> {
+ delegationAdminService.attachDelegate(node2, node3, assoc1);
+ return null;
+ });
+
+ // Now try to add to the start of the chain: node3 -> node1
+ expectedException(ChainedDelegationUnsupported.class, () -> {
+ delegationAdminService.attachDelegate(node3, node1, assoc1);
+ return null;
+ });
+ }
+}
diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationServiceImplUnitTest.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationServiceImplUnitTest.java
new file mode 100644
index 0000000000..b28fae5de4
--- /dev/null
+++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationServiceImplUnitTest.java
@@ -0,0 +1,194 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import static java.util.Collections.emptyMap;
+import static java.util.Arrays.asList;
+import static org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.DelegateNotFound;
+import static org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.DelegationNotFound;
+import static org.alfresco.module.org_alfresco_module_rm.test.util.ExceptionUtils.expectedException;
+import static org.alfresco.module.org_alfresco_module_rm.test.util.FPUtils.asSet;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+import static org.mockito.Mockito.any;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.eq;
+import static org.mockito.Mockito.when;
+
+import org.alfresco.service.cmr.dictionary.ClassDefinition;
+import org.alfresco.service.cmr.dictionary.DictionaryService;
+import org.alfresco.service.cmr.dictionary.PropertyDefinition;
+import org.alfresco.service.cmr.repository.AssociationRef;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.namespace.QName;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.io.Serializable;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Unit tests for {@link DelegationServiceImpl}.
+ *
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public class DelegationServiceImplUnitTest
+{
+ @InjectMocks private final DelegationServiceImpl delegationService = new DelegationServiceImpl();
+
+ @Mock DictionaryService mockDictionaryService;
+ @Mock NodeService mockNodeService;
+ @Mock DelegationAdminServiceImpl mockDelegationAdminService;
+
+ /** This node has a delegate node. */
+ private final NodeRef nodeWithDelegate = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "nodeWithDelegate");
+ /** This is the delgate for {@link #nodeWithDelegate}. */
+ private final NodeRef delegateNode = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "delegateNode");
+ /** This node has no delegate node. */
+ private final NodeRef nodeWithoutDelegate = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "nodeWithoutDelegate");
+
+ /** The type of the peer association that links the delegate to its source. */
+ private final QName delegateAssocType = QName.createQName("test", "delegateAssocType");
+ /** The instance of the association between {@link #nodeWithDelegate} and {@link #delegateNode}. */
+ private final AssociationRef delegateAssocRef = new AssociationRef(nodeWithDelegate, delegateAssocType, delegateNode);
+
+ /** Name of an aspect that has been delegated. */
+ private final QName delegatedAspect1 = QName.createQName("test", "delegatedAspect1");
+ /** Name of an aspect that has been delegated. */
+ private final QName delegatedAspect2 = QName.createQName("test", "delegatedAspect2");
+ /** Name of a content class (a type in this case) that has not been delegated.
+ * N.B. Types can't be delegated currently. */
+ private final QName undelegatedType = QName.createQName("test", "undelegatedType");
+
+ private final QName delegatedProp = QName.createQName("test", "delegatedProp");
+ private final Serializable delegatedPropValue = "hello";
+ private final QName undelegatedProp = QName.createQName("test", "undelegatedProp");
+
+ private final Delegation delegate = new Delegation()
+ {{
+ this.setAssocType(delegateAssocType);
+ this.setAspects(asSet(delegatedAspect1, delegatedAspect2));
+ }};
+
+ @Before public void setUp()
+ {
+ MockitoAnnotations.initMocks(this);
+
+ final PropertyDefinition aspectProp = mock(PropertyDefinition.class);
+ final ClassDefinition aspectDefn = mock(ClassDefinition.class);
+ when(aspectDefn.getName()).thenReturn(delegatedAspect1);
+ when(aspectProp.getContainerClass()).thenReturn(aspectDefn);
+ when(aspectDefn.isAspect()).thenReturn(true);
+
+ final PropertyDefinition typeProp = mock(PropertyDefinition.class);
+ final ClassDefinition typeDefn = mock(ClassDefinition.class);
+ when(typeDefn.getName()).thenReturn(undelegatedType);
+ when(typeProp.getContainerClass()).thenReturn(typeDefn);
+ when(typeDefn.isAspect()).thenReturn(false);
+
+ when(mockDictionaryService.getProperty(delegatedProp)).thenReturn(aspectProp);
+
+ when(mockDelegationAdminService.getDelegationsFrom(nodeWithDelegate)).thenReturn(asSet(delegate));
+ for (QName delegatedAspect : asSet(delegatedAspect1, delegatedAspect2))
+ {
+ when(mockDelegationAdminService.getDelegationFor(delegatedAspect)).thenReturn(delegate);
+ when(mockNodeService.hasAspect(delegateNode, delegatedAspect)).thenReturn(true);
+ }
+ when(mockNodeService.getSourceAssocs(delegateNode, delegateAssocType)).thenReturn(asList(delegateAssocRef));
+ when(mockNodeService.getTargetAssocs(nodeWithDelegate, delegateAssocType)).thenReturn(asList(delegateAssocRef));
+ when(mockNodeService.exists(any(NodeRef.class))).thenReturn(true);
+ when(mockNodeService.getProperties(delegateNode))
+ .thenReturn(new HashMap()
+ {{
+ this.put(delegatedProp, delegatedPropValue);
+ }});
+ }
+
+ @Test public void hasDelegateForAspect()
+ {
+ assertTrue(delegationService.hasDelegateForAspect(nodeWithDelegate, delegatedAspect1));
+ expectedException(DelegationNotFound.class, () -> delegationService.hasDelegateForAspect(nodeWithoutDelegate, undelegatedType));
+ assertFalse(delegationService.hasDelegateForAspect(nodeWithoutDelegate, delegatedAspect1));
+ }
+
+ @Test public void getDelegateFor()
+ {
+ assertEquals(delegateNode, delegationService.getDelegateFor(nodeWithDelegate, delegatedAspect1));
+ expectedException(DelegationNotFound.class, () ->
+ {
+ delegationService.getDelegateFor(nodeWithDelegate, undelegatedType);
+ return null;
+ });
+ assertNull(delegationService.getDelegateFor(nodeWithoutDelegate, delegatedAspect1));
+ }
+
+ @Test public void getDelegateProperties()
+ {
+ final Map expectedProps = new HashMap<>();
+ expectedProps.put(delegatedProp, delegatedPropValue);
+
+ assertEquals(expectedProps, delegationService.getDelegateProperties(nodeWithDelegate, delegatedAspect1));
+
+ expectedException(DelegationNotFound.class,
+ () -> delegationService.getDelegateProperties(nodeWithDelegate, undelegatedType));
+
+ expectedException(DelegateNotFound.class,
+ () -> delegationService.getDelegateProperties(nodeWithoutDelegate, delegatedAspect1));
+ }
+
+ @Test public void getDelegateProperty()
+ {
+ assertEquals(delegatedPropValue, delegationService.getDelegateProperty(nodeWithDelegate, delegatedProp));
+
+ expectedException(IllegalArgumentException.class,
+ () -> delegationService.getDelegateProperty(nodeWithDelegate, undelegatedProp));
+
+ expectedException(DelegationNotFound.class,
+ () -> delegationService.getDelegateProperties(nodeWithoutDelegate, delegatedProp));
+ }
+
+ @Test public void hasAspectOnDelegate()
+ {
+ assertTrue(delegationService.hasAspectOnDelegate(nodeWithDelegate, delegatedAspect1));
+
+ expectedException(DelegationNotFound.class,
+ () -> delegationService.hasAspectOnDelegate(nodeWithDelegate, undelegatedType));
+
+ expectedException(DelegateNotFound.class,
+ () -> delegationService.hasAspectOnDelegate(nodeWithoutDelegate, delegatedAspect1));
+ }
+
+ @Test public void getDelegations()
+ {
+ final Map expectedDelegations = new HashMap<>();
+ expectedDelegations.put(delegate, delegateNode);
+
+ assertEquals(expectedDelegations, delegationService.getDelegations(nodeWithDelegate));
+ assertEquals(emptyMap(), delegationService.getDelegations(nodeWithoutDelegate));
+ }
+}
diff --git a/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationUnitTest.java b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationUnitTest.java
new file mode 100644
index 0000000000..d63e6409d4
--- /dev/null
+++ b/rm-server/unit-test/java/org/alfresco/module/org_alfresco_module_rm/metadatadelegation/DelegationUnitTest.java
@@ -0,0 +1,128 @@
+/*
+ * Copyright (C) 2005-2015 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.module.org_alfresco_module_rm.metadatadelegation;
+
+import static java.util.Collections.emptySet;
+import static org.alfresco.module.org_alfresco_module_rm.metadatadelegation.DelegationException.InvalidDelegation;
+import static org.alfresco.module.org_alfresco_module_rm.test.util.ExceptionUtils.expectedException;
+import static org.alfresco.module.org_alfresco_module_rm.test.util.FPUtils.asListFrom;
+import static org.alfresco.module.org_alfresco_module_rm.test.util.FPUtils.asSet;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import org.alfresco.service.cmr.dictionary.AspectDefinition;
+import org.alfresco.service.cmr.dictionary.AssociationDefinition;
+import org.alfresco.service.cmr.dictionary.DictionaryService;
+import org.alfresco.service.cmr.repository.NodeRef;
+import org.alfresco.service.cmr.repository.NodeService;
+import org.alfresco.service.cmr.repository.StoreRef;
+import org.alfresco.service.namespace.QName;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+
+import java.util.List;
+
+/**
+ * Unit tests for {@link Delegation}.
+ *
+ * @author Neil Mc Erlean
+ * @since 3.0.a
+ */
+public class DelegationUnitTest
+{
+ @Mock DictionaryService mockDictionaryService;
+ @Mock NodeService mockNodeService;
+
+ private final DelegationAdminServiceImpl metadataDelegationService = new DelegationAdminServiceImpl();
+
+ private final NodeRef node1 = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "node1");
+ private final NodeRef node2 = new NodeRef(StoreRef.STORE_REF_WORKSPACE_SPACESSTORE, "node2");
+ private final QName aspect1 = QName.createQName("test", "aspect1");
+ private final QName aspect2 = QName.createQName("test", "aspect2");
+ private final QName assoc1 = QName.createQName("test", "assoc1");
+
+ @Before public void setUp()
+ {
+ MockitoAnnotations.initMocks(this);
+
+ metadataDelegationService.setNodeService(mockNodeService);
+ }
+
+ @Test public void nullOrEmptyDelegatesAreForbidden()
+ {
+ List invalidDelegations = asListFrom(() -> new Delegation(),
+ () -> {
+ Delegation d = new Delegation();
+ d.setAssocType(assoc1);
+ d.setAspects(null);
+ d.setDictionaryService(mockDictionaryService);
+ return d;
+ },
+ () -> {
+ Delegation d = new Delegation();
+ d.setAssocType(assoc1);
+ d.setAspects(emptySet());
+ d.setDictionaryService(mockDictionaryService);
+ return d;
+ },
+ () -> {
+ Delegation d = new Delegation();
+ d.setAssocType(null);
+ d.setAspects(asSet(aspect1, aspect2));
+ d.setDictionaryService(mockDictionaryService);
+ return d;
+ });
+
+ invalidDelegations.stream()
+ .forEach(d -> expectedException(InvalidDelegation.class, () -> {
+ d.validateAndRegister();
+ return null;
+ }
+ ));
+ }
+
+ @Test(expected=InvalidDelegation.class)
+ public void delegateMustHaveAssocThatExists()
+ {
+ when(mockDictionaryService.getAssociation(assoc1)).thenReturn(null);
+ when(mockDictionaryService.getAspect(aspect1)).thenReturn(mock(AspectDefinition.class));
+
+ Delegation d = new Delegation();
+ d.setAssocType(assoc1);
+ d.setAspects(asSet(aspect1));
+ d.setDictionaryService(mockDictionaryService);
+ d.validateAndRegister();
+ }
+
+ @Test(expected=InvalidDelegation.class)
+ public void delegateMustHaveAspectsAllOfWhichExist()
+ {
+ when(mockDictionaryService.getAssociation(assoc1)).thenReturn(mock(AssociationDefinition.class));
+ when(mockDictionaryService.getAspect(aspect1)).thenReturn(mock(AspectDefinition.class));
+ when(mockDictionaryService.getAspect(aspect2)).thenReturn(null);
+
+ Delegation d = new Delegation();
+ d.setAssocType(assoc1);
+ d.setAspects(asSet(aspect1, aspect2));
+ d.setDictionaryService(mockDictionaryService);
+ d.validateAndRegister();
+ }
+}