diff --git a/source/java/org/alfresco/repo/admin/registry/RegistryKey.java b/source/java/org/alfresco/repo/admin/registry/RegistryKey.java index 8505d91f3c..edfbc4ed02 100644 --- a/source/java/org/alfresco/repo/admin/registry/RegistryKey.java +++ b/source/java/org/alfresco/repo/admin/registry/RegistryKey.java @@ -87,6 +87,29 @@ public class RegistryKey implements Serializable this.path = new String[length - 1]; System.arraycopy(key, 0, path, 0, length - 1); } + + /** + * A constructor to specifically declare the path and property portions of the key. + * + * @param namespaceUri the key namespace to use. If left null then the + * {@link #REGISTRY_1_0_URI default} will be used. + * @param path the path part of the key + * @param property the property name for the key. This may be null. + */ + public RegistryKey(String namespaceUri, String[] path, String property) + { + if (namespaceUri == null) + { + namespaceUri = REGISTRY_1_0_URI; + } + this.namespaceUri = namespaceUri; + if ((path == null || path.length == 0) && property == null) + { + throw new IllegalArgumentException("No path or property supplied for the RegistryKey"); + } + this.property = property; + this.path = path; + } @Override public String toString() diff --git a/source/java/org/alfresco/repo/admin/registry/RegistryService.java b/source/java/org/alfresco/repo/admin/registry/RegistryService.java index 9b95a73bf2..a3d843fd38 100644 --- a/source/java/org/alfresco/repo/admin/registry/RegistryService.java +++ b/source/java/org/alfresco/repo/admin/registry/RegistryService.java @@ -41,26 +41,60 @@ public interface RegistryService * @param key the registry key. * @param value any value that can be stored in the repository. */ - void addValue(RegistryKey key, Serializable value); + void addProperty(RegistryKey key, Serializable value); /** * @param key the registry key. - * @return Returns the value stored in the key. + * @return Returns the value stored in the key or null if + * no value exists at the path and name provided * - * @see #addValue(String, Serializable) + * @see #addProperty(String, Serializable) */ - Serializable getValue(RegistryKey key); + Serializable getProperty(RegistryKey key); /** - * Fetches all child elements for the given path. So for a registry key key=/a/b/ignored, the - * results of getChildElements(key) would be b. The key's last path - * element represents the key's property name, and can therefore be any value without affecting - * the outcome of the call. + * Fetches all child elements for the given path. The key's property should be + * null as it is completely ignored. + *
+     *    ...
+     *    registryService.addValue(KEY_A_B_C_1, VALUE_ONE);
+     *    registryService.addValue(KEY_A_B_C_2, VALUE_TWO);
+     *    ...
+     *    assertTrue(registryService.getChildElements(KEY_A_B_null).contains("C"));
+     *    ...
+     * 
* * @param key the registry key with the path. The last element in the path - * will be ignored, and can be any acceptable property localname. + * will be ignored, and can be any acceptable value localname or null. * @return Returns all child elements (not values) for the given key, ignoring * the last element in the key. + * + * @see RegistryKey#getPath() */ Collection getChildElements(RegistryKey key); + + /** + * Copies the path or value from the source to the target location. The source and target + * keys must be both either path-specific or property-specific. If the source doesn't + * exist, then nothing will be done; there is no guarantee that the target will exist after + * the call. + *

+ * This is essentially a merge operation. Use {@link #delete(RegistryKey) delete} first + * if the target must be cleaned. + * + * @param sourceKey the source registry key to take values from + * @param targetKey the target registyr key to move the path or value to + */ + void copy(RegistryKey sourceKey, RegistryKey targetKey); + + /** + * Delete the path element or value described by the key. If the key points to nothing, + * then nothing is done. + * delete(/a/b/c) will remove value c from path /a/b.
+ * delete(/a/b/null) will remove node /a/b along with all values and child + * elements. + * + * @param key the path or value to delete + */ + void delete(RegistryKey key); } diff --git a/source/java/org/alfresco/repo/admin/registry/RegistryServiceImpl.java b/source/java/org/alfresco/repo/admin/registry/RegistryServiceImpl.java index 770d000589..f43428534b 100644 --- a/source/java/org/alfresco/repo/admin/registry/RegistryServiceImpl.java +++ b/source/java/org/alfresco/repo/admin/registry/RegistryServiceImpl.java @@ -28,7 +28,10 @@ import java.io.Serializable; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.HashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.alfresco.error.AlfrescoRuntimeException; import org.alfresco.model.ContentModel; @@ -43,6 +46,7 @@ import org.alfresco.service.namespace.NamespaceException; import org.alfresco.service.namespace.NamespaceService; import org.alfresco.service.namespace.QName; import org.alfresco.service.namespace.RegexQNamePattern; +import org.alfresco.util.EqualsHelper; import org.alfresco.util.Pair; import org.alfresco.util.PropertyCheck; import org.alfresco.util.PropertyMap; @@ -168,8 +172,11 @@ public class RegistryServiceImpl implements RegistryService } /** + * Get the node-qname pair for the key. If the key doesn't have a value element, + * i.e. if it is purely path-based, then the QName will be null. + * * @return Returns the node and property name represented by the key or null - * if it doesn't exist and was not allowed to be created + * if it doesn't exist and was not allowed to be created. */ private Pair getPath(RegistryKey key, boolean create) { @@ -234,10 +241,15 @@ public class RegistryServiceImpl implements RegistryService currentNodeRef = childAssocRefs.get(0).getChildRef(); } } + // Cater for null properties, i.e. path-based keys + QName propertyQName = null; + if (property != null) + { + propertyQName = QName.createQName( + namespaceUri, + QName.createValidLocalName(property)); + } // Create the result - QName propertyQName = QName.createQName( - namespaceUri, - QName.createValidLocalName(property)); Pair resultPair = new Pair(currentNodeRef, propertyQName); // done if (logger.isDebugEnabled()) @@ -259,8 +271,12 @@ public class RegistryServiceImpl implements RegistryService /** * @inheritDoc */ - public void addValue(RegistryKey key, Serializable value) + public void addProperty(RegistryKey key, Serializable value) { + if (key.getProperty() == null) + { + throw new IllegalArgumentException("Registry values must be added using paths that contain property names: " + key); + } // Check the namespace being used in the key String namespaceUri = key.getNamespaceUri(); if (!namespaceService.getURIs().contains(namespaceUri)) @@ -281,23 +297,27 @@ public class RegistryServiceImpl implements RegistryService } } - public Serializable getValue(RegistryKey key) + public Serializable getProperty(RegistryKey key) { + if (key.getProperty() == null) + { + throw new IllegalArgumentException("Registry values must be fetched using paths that contain property names: " + key); + } // Get the path, without creating Pair keyPair = getPath(key, false); - Serializable value = null; + Serializable property = null; if (keyPair != null) { - value = nodeService.getProperty(keyPair.getFirst(), keyPair.getSecond()); + property = nodeService.getProperty(keyPair.getFirst(), keyPair.getSecond()); } // Done if (logger.isDebugEnabled()) { - logger.debug("Retrieved value from registry: \n" + + logger.debug("Retrieved property from registry: \n" + " Key: " + key + "\n" + - " Value: " + value); + " Value: " + property); } - return value; + return property; } public Collection getChildElements(RegistryKey key) @@ -330,4 +350,175 @@ public class RegistryServiceImpl implements RegistryService } return results; } + + public void copy(RegistryKey sourceKey, RegistryKey targetKey) + { + if ((sourceKey.getProperty() == null) && !(targetKey.getProperty() == null)) + { + throw new AlfrescoRuntimeException( + "Registry keys must both be path specific for a copy: \n" + + " Source: " + sourceKey + "\n" + + " Target: " + targetKey); + } + else if ((sourceKey.getProperty() != null) && (targetKey.getProperty() == null)) + { + throw new AlfrescoRuntimeException( + "Registry keys must both be value specific for a copy: \n" + + " Source: " + sourceKey + "\n" + + " Target: " + targetKey); + } + // If the source is missing, then do nothing + Pair sourceKeyPair = getPath(sourceKey, false); + if (sourceKeyPair == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Nothing copied from non-existent registry source key: \n" + + " Source: " + sourceKey + "\n" + + " Target: " + targetKey); + } + return; + } + // Move based on the path or property + Pair targetKeyPair = getPath(targetKey, true); + if (sourceKeyPair.getSecond() != null) + { + // It is property-based so we just need to copy the value + Serializable value = nodeService.getProperty(sourceKeyPair.getFirst(), sourceKeyPair.getSecond()); + nodeService.setProperty(targetKeyPair.getFirst(), targetKeyPair.getSecond(), value); + } + else + { + // It is path based so we need to copy all registry entries + // We have an existing target, but we need to recurse + Set processedNodeRefs = new HashSet(20); + copyRecursive(sourceKey, targetKey, processedNodeRefs); + } + // Done + if (logger.isDebugEnabled()) + { + logger.debug("Copied registry keys: \n" + + " Source: " + sourceKey + "\n" + + " Target: " + targetKey); + } + } + + /** + * @param sourceKey the source path that must exist + * @param targetKey the target path that will be created + * @param processedNodeRefs a set to help avoid infinite loops + */ + private void copyRecursive(RegistryKey sourceKey, RegistryKey targetKey, Set processedNodeRefs) + { + String sourceNamespaceUri = sourceKey.getNamespaceUri(); + String targetNamespaceUri = targetKey.getNamespaceUri(); + // The source just exist + Pair sourceKeyPair = getPath(sourceKey, false); + if (sourceKeyPair == null) + { + // It has disappeared + return; + } + NodeRef sourceNodeRef = sourceKeyPair.getFirst(); + // Check that we don't have a circular reference + if (processedNodeRefs.contains(sourceNodeRef)) + { + // This is very serious, but it can be worked around + logger.error("Circular paths detected in registry entries: \n" + + " Current Source Key: " + sourceKey + "\n" + + " Current Target Key: " + targetKey + "\n" + + " Source Node: " + sourceNodeRef); + logger.error("Bypassing circular registry entry"); + return; + } + + // Make sure that the target exists + Pair targetKeyPair = getPath(targetKey, true); + NodeRef targetNodeRef = targetKeyPair.getFirst(); + + // Copy properties of the source namespace + Map sourceProperties = nodeService.getProperties(sourceNodeRef); + Map targetProperties = nodeService.getProperties(targetNodeRef); + boolean changed = false; + for (Map.Entry entry : sourceProperties.entrySet()) + { + QName sourcePropertyQName = entry.getKey(); + if (!EqualsHelper.nullSafeEquals(sourcePropertyQName.getNamespaceURI(), sourceNamespaceUri)) + { + // Wrong namespace + continue; + } + // Copy the value over + Serializable value = entry.getValue(); + QName targetPropertyQName = QName.createQName(targetNamespaceUri, sourcePropertyQName.getLocalName()); + targetProperties.put(targetPropertyQName, value); + changed = true; + } + if (changed) + { + nodeService.setProperties(targetNodeRef, targetProperties); + } + // We have processed the source node + processedNodeRefs.add(sourceNodeRef); + + // Now get the child elements of the source + Collection sourceChildElements = getChildElements(sourceKey); + String[] sourcePath = sourceKey.getPath(); + String[] childSourcePath = new String[sourcePath.length + 1]; // + System.arraycopy(sourcePath, 0, childSourcePath, 0, sourcePath.length); + String[] targetPath = targetKey.getPath(); + String[] childTargetPath = new String[targetPath.length + 1]; // + System.arraycopy(targetPath, 0, childTargetPath, 0, targetPath.length); + for (String sourceChildElement : sourceChildElements) + { + // Make the source child key using the current source namespace + childSourcePath[sourcePath.length] = sourceChildElement; + RegistryKey sourceChildKey = new RegistryKey(sourceNamespaceUri, childSourcePath, null); + // Make the target child key using the current target namespace + childTargetPath[targetPath.length] = sourceChildElement; + RegistryKey targetChildKey = new RegistryKey(targetNamespaceUri, childTargetPath, null); + // Recurse + copyRecursive(sourceChildKey, targetChildKey, processedNodeRefs); + } + } + + public void delete(RegistryKey key) + { + Pair keyPair = getPath(key, false); + if (keyPair == null) + { + if (logger.isDebugEnabled()) + { + logger.debug("Nothing to delete for registry key: \n" + + " Key: " + key); + } + return; + } + NodeRef pathNodeRef = keyPair.getFirst(); + QName propertyQName = keyPair.getSecond(); + if (propertyQName == null) + { + // This is a path-based deletion + nodeService.deleteNode(pathNodeRef); + if (logger.isDebugEnabled()) + { + logger.debug("Performed path-based delete: \n" + + " Key: " + key + "\n" + + " Node: " + pathNodeRef); + } + } + else + { + // This is a value-based deletion + nodeService.removeProperty(pathNodeRef, propertyQName); + if (logger.isDebugEnabled()) + { + logger.debug("Performed value-based delete: \n" + + " Key: " + key + "\n" + + " Node: " + pathNodeRef + "\n" + + " Property: " + propertyQName); + } + } + // Done + } } diff --git a/source/java/org/alfresco/repo/admin/registry/RegistryServiceImplTest.java b/source/java/org/alfresco/repo/admin/registry/RegistryServiceImplTest.java index 5180858bba..310b5de226 100644 --- a/source/java/org/alfresco/repo/admin/registry/RegistryServiceImplTest.java +++ b/source/java/org/alfresco/repo/admin/registry/RegistryServiceImplTest.java @@ -75,8 +75,11 @@ public class RegistryServiceImplTest extends TestCase private static final Long VALUE_ONE = 1L; private static final Long VALUE_TWO = 2L; private static final Long VALUE_THREE = 3L; + private static final RegistryKey KEY_A = new RegistryKey(null, "a", null); + private static final RegistryKey KEY_A_B = new RegistryKey(null, "a", "b", null); private static final RegistryKey KEY_A_B_0 = new RegistryKey(null, "a", "b", "0"); private static final RegistryKey KEY_A_B_1 = new RegistryKey(null, "a", "b", "1"); + private static final RegistryKey KEY_A_B_C = new RegistryKey(null, "a", "b", "c", null); private static final RegistryKey KEY_A_B_C_0 = new RegistryKey(null, "a", "b", "c", "0"); private static final RegistryKey KEY_A_B_C_1 = new RegistryKey(null, "a", "b", "c", "1"); private static final RegistryKey KEY_A_B_C_2 = new RegistryKey(null, "a", "b", "c", "2"); @@ -85,7 +88,12 @@ public class RegistryServiceImplTest extends TestCase private static final RegistryKey KEY_A_B_C_D_1 = new RegistryKey(null, "a", "b", "c", "d", "1"); private static final RegistryKey KEY_A_B_C_D_2 = new RegistryKey(null, "a", "b", "c", "d", "2"); private static final RegistryKey KEY_A_B_C_D_3 = new RegistryKey(null, "a", "b", "c", "d", "3"); + private static final RegistryKey KEY_X_Y_Z = new RegistryKey(null, "x", "y", "z", null); private static final RegistryKey KEY_X_Y_Z_0 = new RegistryKey(null, "x", "y", "z", "0"); + private static final RegistryKey KEY_X_Y_Z_1 = new RegistryKey(null, "x", "y", "z", "1"); + private static final RegistryKey KEY_X_Y_Z_2 = new RegistryKey(null, "x", "y", "z", "2"); + private static final RegistryKey KEY_X_Y_Z_D_1 = new RegistryKey(null, "x", "y", "z", "d", "1"); + private static final RegistryKey KEY_X_Y_Z_D_2 = new RegistryKey(null, "x", "y", "z", "d", "2"); private static final RegistryKey KEY_SPECIAL = new RegistryKey(null, "me & you", "whatever"); private static final RegistryKey KEY_DOES_NOT_EXIST = new RegistryKey(null, "does", "not", "exist"); /** @@ -93,29 +101,29 @@ public class RegistryServiceImplTest extends TestCase */ public void testProperUsage() throws Exception { - registryService.addValue(KEY_A_B_C_1, VALUE_ONE); - registryService.addValue(KEY_A_B_C_2, VALUE_TWO); - registryService.addValue(KEY_A_B_C_3, VALUE_THREE); - registryService.addValue(KEY_A_B_C_D_1, VALUE_ONE); - registryService.addValue(KEY_A_B_C_D_2, VALUE_TWO); - registryService.addValue(KEY_A_B_C_D_3, VALUE_THREE); + registryService.addProperty(KEY_A_B_C_1, VALUE_ONE); + registryService.addProperty(KEY_A_B_C_2, VALUE_TWO); + registryService.addProperty(KEY_A_B_C_3, VALUE_THREE); + registryService.addProperty(KEY_A_B_C_D_1, VALUE_ONE); + registryService.addProperty(KEY_A_B_C_D_2, VALUE_TWO); + registryService.addProperty(KEY_A_B_C_D_3, VALUE_THREE); - assertEquals("Incorrect value from service registry", VALUE_ONE, registryService.getValue(KEY_A_B_C_1)); - assertEquals("Incorrect value from service registry", VALUE_TWO, registryService.getValue(KEY_A_B_C_2)); - assertEquals("Incorrect value from service registry", VALUE_THREE, registryService.getValue(KEY_A_B_C_3)); - assertEquals("Incorrect value from service registry", VALUE_ONE, registryService.getValue(KEY_A_B_C_D_1)); - assertEquals("Incorrect value from service registry", VALUE_TWO, registryService.getValue(KEY_A_B_C_D_2)); - assertEquals("Incorrect value from service registry", VALUE_THREE, registryService.getValue(KEY_A_B_C_D_3)); + assertEquals("Incorrect value from service registry", VALUE_ONE, registryService.getProperty(KEY_A_B_C_1)); + assertEquals("Incorrect value from service registry", VALUE_TWO, registryService.getProperty(KEY_A_B_C_2)); + assertEquals("Incorrect value from service registry", VALUE_THREE, registryService.getProperty(KEY_A_B_C_3)); + assertEquals("Incorrect value from service registry", VALUE_ONE, registryService.getProperty(KEY_A_B_C_D_1)); + assertEquals("Incorrect value from service registry", VALUE_TWO, registryService.getProperty(KEY_A_B_C_D_2)); + assertEquals("Incorrect value from service registry", VALUE_THREE, registryService.getProperty(KEY_A_B_C_D_3)); - assertNull("Missing key should return null value", registryService.getValue(KEY_A_B_C_0)); - assertNull("Missing key should return null value", registryService.getValue(KEY_A_B_C_D_0)); - assertNull("Missing key should return null value", registryService.getValue(KEY_X_Y_Z_0)); + assertNull("Missing key should return null value", registryService.getProperty(KEY_A_B_C_0)); + assertNull("Missing key should return null value", registryService.getProperty(KEY_A_B_C_D_0)); + assertNull("Missing key should return null value", registryService.getProperty(KEY_X_Y_Z_0)); } public void testGetElements() throws Exception { - registryService.addValue(KEY_A_B_C_1, VALUE_ONE); - registryService.addValue(KEY_A_B_C_2, VALUE_TWO); + registryService.addProperty(KEY_A_B_C_1, VALUE_ONE); + registryService.addProperty(KEY_A_B_C_2, VALUE_TWO); // Check that we get an empty list for a random query assertEquals("Excpected empty collection for random query", 0, registryService.getChildElements(KEY_DOES_NOT_EXIST).size()); @@ -130,7 +138,67 @@ public class RegistryServiceImplTest extends TestCase public void testSpecialCharacters() { - registryService.addValue(KEY_SPECIAL, VALUE_THREE); - assertEquals("Incorrect value for special key", VALUE_THREE, registryService.getValue(KEY_SPECIAL)); + registryService.addProperty(KEY_SPECIAL, VALUE_THREE); + assertEquals("Incorrect value for special key", VALUE_THREE, registryService.getProperty(KEY_SPECIAL)); + } + + public void testDelete() + { + registryService.addProperty(KEY_A_B_C_1, VALUE_ONE); + registryService.addProperty(KEY_A_B_C_2, VALUE_TWO); + // Safety check + assertEquals("Incorrect value from service registry", VALUE_ONE, registryService.getProperty(KEY_A_B_C_1)); + assertEquals("Incorrect value from service registry", VALUE_TWO, registryService.getProperty(KEY_A_B_C_2)); + + // Property-based delete + registryService.delete(KEY_A_B_C_1); + assertNull("Expected deleted value to be null", registryService.getProperty(KEY_A_B_C_1)); + + // Path-based delete + registryService.delete(KEY_A_B); + assertNull("Expected deleted value to be null", registryService.getProperty(KEY_A_B_C_1)); + assertNull("Expected deleted value to be null", registryService.getProperty(KEY_A_B_C_2)); + + Collection childElements = registryService.getChildElements(KEY_A); + assertEquals("There should be no more elements within A", 0, childElements.size()); + } + + public void testCopy() + { + registryService.addProperty(KEY_A_B_C_1, VALUE_ONE); + registryService.addProperty(KEY_A_B_C_2, VALUE_TWO); + registryService.addProperty(KEY_A_B_C_D_1, VALUE_ONE); + registryService.addProperty(KEY_A_B_C_D_2, VALUE_TWO); + + // Check illegal copy + try + { + registryService.copy(KEY_A_B_C_1, KEY_X_Y_Z); + fail("Failed to detect copy from property to path"); + } + catch (Throwable e) + { + // Expected + } + try + { + registryService.copy(KEY_A_B_C, KEY_X_Y_Z_0); + fail("Failed to detect copy from path to property"); + } + catch (Throwable e) + { + // Expected + } + + // Property-based copy + registryService.copy(KEY_A_B_C_1, KEY_X_Y_Z_0); + assertEquals("Incorrect value after property copy", VALUE_ONE, registryService.getProperty(KEY_X_Y_Z_0)); + + // Path-based copy + registryService.copy(KEY_A_B_C, KEY_X_Y_Z); + assertEquals("Value not recursively copied during path copy", VALUE_ONE, registryService.getProperty(KEY_X_Y_Z_1)); + assertEquals("Value not recursively copied during path copy", VALUE_TWO, registryService.getProperty(KEY_X_Y_Z_2)); + assertEquals("Path not recursively copied during path copy", VALUE_ONE, registryService.getProperty(KEY_X_Y_Z_D_1)); + assertEquals("Path not recursively copied during path copy", VALUE_TWO, registryService.getProperty(KEY_X_Y_Z_D_2)); } }