/** * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.brixcms.jcr; import org.apache.wicket.util.string.Strings; import org.brixcms.jcr.api.JcrNamespaceRegistry; import org.brixcms.jcr.api.JcrNode; import org.brixcms.jcr.api.JcrNodeIterator; import org.brixcms.jcr.api.JcrProperty; import org.brixcms.jcr.api.JcrPropertyIterator; import org.brixcms.jcr.api.JcrSession; import org.brixcms.jcr.api.JcrValue; import org.brixcms.jcr.api.JcrValueFactory; import org.brixcms.jcr.api.JcrWorkspace; import org.brixcms.jcr.exception.JcrException; import org.brixcms.jcr.wrapper.BrixNode; import javax.jcr.ImportUUIDBehavior; import javax.jcr.ItemNotFoundException; import javax.jcr.PropertyType; import javax.jcr.nodetype.NodeType; import java.io.ByteArrayInputStream; import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * @author Matej Knopp */ public class JcrUtil { /** * Clones the given list of nodes. The clones will be located relative to targetRootNode. * <p/> * If a node being cloned is referenceable and there is already node with same UUID in the target workspace, the * location of the node in target workspace determines the result. If node being cloned would become child of the * same parent as the existing node in target workspace, the existing node will be replaced. Otherwise the node * being cloned will get a new UUID. * * @param nodes list of nodes to clone * @param targetRootNode parent for clones */ public static void cloneNodes(List<JcrNode> nodes, JcrNode targetRootNode) { cloneNodes(nodes, targetRootNode, null); } /** * Clones the given list of nodes. The clones will be located relative to targetRootNode. * <p/> * If a node being cloned is referenceable and there is already node with same UUID in the target workspace, the * location of the node in target workspace determines the result. If node being cloned would become child of the * same parent as the existing node in target workspace, the existing node will be replaced. Otherwise the node * being cloned will get a new UUID. * * @param nodes list of nodes to clone * @param targetRootNodeProvider provider for parents for clones * @param parentLimiter (non mandatory) allows to skip certain nodes when creating parent hierarchy for * cloned nodes */ public static void cloneNodes(List<JcrNode> nodes, TargetRootNodeProvider targetRootNodeProvider, ParentLimiter parentLimiter) { if (nodes != null && !nodes.isEmpty()) { JcrNode firstTargetRoot = targetRootNodeProvider.getTargetRootNode(nodes.iterator().next()); String xmlns = createXMLNS(firstTargetRoot.getSession()); Map<String, String> uuidMap = new HashMap<String, String>(); nodes = filterRedundantNodes(nodes); List<NodePair> processedNodes = new ArrayList<NodePair>(); for (JcrNode node : nodes) { JcrNode targetRoot = targetRootNodeProvider.getTargetRootNode(node); createNode(node, targetRoot, xmlns, uuidMap, processedNodes, parentLimiter); } assignProperties(processedNodes, uuidMap); } } /** * Creates the XML snippet containing namespace definitions. * * @param session * @return */ private static String createXMLNS(JcrSession session) { JcrNamespaceRegistry registry = session.getWorkspace().getNamespaceRegistry(); String prefixes[] = registry.getPrefixes(); StringBuilder result = new StringBuilder(); for (String prefix : prefixes) { if (prefix != null && prefix.length() > 0) { if (result.length() > 0) { result.append("\n"); } result.append("xmlns:"); result.append(prefix); result.append("=\""); result.append(registry.getURI(prefix)); result.append("\""); } } return result.toString(); } /** * Filters out nodes that are redundantly in the list because their parent nodes are also in the list. * * @param nodes * @return */ private static List<JcrNode> filterRedundantNodes(List<JcrNode> nodes) { List<JcrNode> result = new ArrayList<JcrNode>(nodes); for (JcrNode n : nodes) { String pathCurrent = n.getPath(); for (JcrNode n2 : result) { String pathExisting = n2.getPath(); if (pathCurrent.startsWith(pathExisting) && n != n2) { result.remove(n); break; } } } return result; } /** * Creates a copy of originalNode (without setting the properties). The node position will be concatenation of * targetRootNode path and originalNode path. If the targetRootNode path doesn't contains the appropriate child * nodes, they will be created. * * @param originalNode * @param targetRootNode * @param xmlns * @param uuidMap * @param nodes * @param parentLimiter */ private static void createNode(JcrNode originalNode, JcrNode targetRootNode, String xmlns, Map<String, String> uuidMap, List<NodePair> nodes, ParentLimiter parentLimiter) { JcrNode targetParent = ensureParentExists(originalNode, targetRootNode, parentLimiter, xmlns, uuidMap); createNodeAndChildren(originalNode, targetParent, xmlns, uuidMap, nodes); } /** * Ensures all nodes from originalNode parent hierarchy exist as children of targetRootNode. Missing nodes will be * created having same primary node type as original node. * <p/> * Example: If originalNode is /foo/bar/baz/node and targetRootNode is /x/y having child /x/y/foo , this method will * create nodes x/y/foo/bar and x/y/foo/bar/baz . * * @param originalNode * @param targetRootNode * @return */ private static JcrNode ensureParentExists(JcrNode originalNode, JcrNode targetRootNode, ParentLimiter parentLimiter, String xmlns, Map<String, String> uuidMap) { List<JcrNode> originalParents = getParents(originalNode, parentLimiter); for (JcrNode node : originalParents) { String name = node.getName(); if (targetRootNode.hasNode(name)) { targetRootNode = targetRootNode.getNode(name); } else { targetRootNode = cloneNode(node, targetRootNode, xmlns, uuidMap); } } return targetRootNode; } /** * Returns list of parent nodes for given node. The list is sorted ascending by node depth. * * @param node * @return */ private static List<JcrNode> getParents(JcrNode node, ParentLimiter parentLimiter) { List<JcrNode> result = new ArrayList<JcrNode>(); JcrNode p = node.getParent(); while (p.getDepth() > 0 && (parentLimiter == null || !parentLimiter.isFinalParent(node, p))) { result.add(0, p); p = p.getParent(); } return result; } private static JcrNode cloneNode(JcrNode originalNode, JcrNode targetParent, String xmlns, Map<String, String> uuidMap) { // create node JcrNode targetNode; if (originalNode.isNodeType("mix:referenceable")) { targetNode = createNodeWithUUID(originalNode, targetParent, xmlns, uuidMap); } else { targetNode = targetParent.addNode(originalNode.getName(), originalNode.getPrimaryNodeType().getName()); } // set mixin types NodeType[] mixins = originalNode.getMixinNodeTypes(); for (NodeType type : mixins) { if (type.getName().equals("mix:referenceable") == false) { targetNode.addMixin(type.getName()); } } return targetNode; } /** * Creates a node that is child of parentNode having same name (and possibly uuid) as originalNode. * <p/> * What happens if there is node with same UUID in target workspace depends on the location of the node. If the node * with same uuid is a direct child of {@link parentNode}, it gets replaced. Otherwise node with new UUID is created * and the original one is preserved. * * @param originalNode * @param parentNode * @param xmlns * @param uuidMap * @return * @see ImportUUIDBehavior */ private static JcrNode createNodeWithUUID(JcrNode originalNode, JcrNode parentNode, String xmlns, Map<String, String> uuidMap) { // construct the import xml snippet String uuid = originalNode.getIdentifier(); String name = originalNode.getName(); JcrSession session = parentNode.getSession(); StringBuilder xml = new StringBuilder(); xml.append("<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"); xml.append("<sv:node "); xml.append(xmlns); xml.append(" sv:name=\""); xml.append(escapeMarkup(name)); xml.append("\">"); xml.append("<sv:property sv:name=\"jcr:primaryType\" sv:type=\"Name\"><sv:value>"); xml.append(escapeMarkup(originalNode.getPrimaryNodeType().getName())); xml.append("</sv:value></sv:property>"); xml.append("<sv:property sv:name=\"jcr:mixinTypes\" sv:type=\"Name\">"); xml.append("<sv:value>mix:referenceable</sv:value></sv:property>"); xml.append("<sv:property sv:name=\"jcr:uuid\" sv:type=\"Name\"><sv:value>"); xml.append(uuid); xml.append("</sv:value></sv:property></sv:node>"); InputStream stream = null; try { stream = new ByteArrayInputStream(xml.toString().getBytes("utf-8")); } catch (UnsupportedEncodingException e) { // retarded } JcrNode existing = null; try { // there doesn't seem to be a way in JCR to check if there is // such node in workspace // except trying to get it and then catching the exception existing = session.getNodeByIdentifier(uuid); } catch (JcrException e) { } int uuidBehavior; if (existing != null && existing.getParent().equals(parentNode)) { uuidBehavior = ImportUUIDBehavior.IMPORT_UUID_COLLISION_REPLACE_EXISTING; } else { uuidBehavior = ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW; } if (uuidBehavior != ImportUUIDBehavior.IMPORT_UUID_CREATE_NEW) { // simpler alternative - if we replace node or throw error on UUID // clash session.importXML(parentNode.getPath(), stream, uuidBehavior); return session.getNodeByIdentifier(uuid); } else { // more complicated alternative - on uuid clash node gets new uuid // and all // cloned references should use the new one boolean exists = existing != null; session.importXML(parentNode.getPath(), stream, uuidBehavior); if (exists == false) { // if there was no node with such uuid in target workspace return session.getNodeByIdentifier(uuid); } else { // otherwise get the latest child with such name JcrNodeIterator iterator = parentNode.getNodes(name); iterator.skip(iterator.getSize() - 1); JcrNode newNode = iterator.nextNode(); String newUuid = newNode.getIdentifier(); // and if it has uuid other than the existing one (should always // be the case) if (uuid.equals(newUuid) == false) { uuidMap.put(uuid, newUuid); } return newNode; } } } private static CharSequence escapeMarkup(String s) { return Strings.escapeMarkup(s, false, false); } /** * Creates copy (without setting the properties) of originalNode and it's children. * * @param originalNode node being cloned * @param targetParent parent of the clone * @param xmlns string containing the xmlns attributes of sv:node element * @param uuidMap map that is used to track mapping from old uuid to new one (in case new UUIDs have been * created. * @param nodes list of pair <originalNode, targetNode). used to track added nodes so that the properties can * be set after all nodes are created. */ private static void createNodeAndChildren(JcrNode originalNode, JcrNode targetParent, String xmlns, Map<String, String> uuidMap, List<NodePair> nodes) { JcrNode targetNode = cloneNode(originalNode, targetParent, xmlns, uuidMap); // add to nodes list so that we can set properties later NodePair pair = new NodePair(); pair.originalNode = originalNode; pair.targetNode = targetNode; nodes.add(pair); // go over nodes and call the method recursively JcrNodeIterator nodeIterator = originalNode.getNodes(); while (nodeIterator.hasNext()) { createNodeAndChildren(nodeIterator.nextNode(), targetNode, xmlns, uuidMap, nodes); } } /** * Goes through each pair of the node list and copies the properties from originalNode to targetNode * * @param nodes * @param uuidMap */ private static void assignProperties(List<NodePair> nodes, Map<String, String> uuidMap) { for (NodePair current : nodes) { JcrNode originalNode = current.originalNode; JcrNode targetNode = current.targetNode; JcrValueFactory vf = targetNode.getSession().getValueFactory(); JcrPropertyIterator propertyIterator = originalNode.getProperties(); while (propertyIterator.hasNext()) { JcrProperty property = propertyIterator.nextProperty(); String name = property.getName(); if (!property.getDefinition().isProtected()) { if (!property.getDefinition().isMultiple()) { JcrValue value = property.getValue(); targetNode.setProperty(name, remapReference(value, uuidMap, vf)); } else { JcrValue values[] = property.getValues(); for (int i = 0; i < values.length; ++i) { values[i] = remapReference(values[i], uuidMap, vf); } targetNode.setProperty(name, values); } } } } } /** * Method checks if given value is of type reference and references node with UUID that has been remapped (can * happen with {@link ImportUUIDBehavior#IMPORT_UUID_CREATE_NEW} being set. * * @param value * @param uuidMap * @param valueFactory * @return */ private static JcrValue remapReference(JcrValue value, Map<String, String> uuidMap, JcrValueFactory valueFactory) { if (value.getType() == PropertyType.REFERENCE) { String uuid = value.getString(); String newUuid = uuidMap.get(uuid); if (newUuid != null) { JcrValue newValue = valueFactory.createValue(newUuid, PropertyType.REFERENCE); return newValue; } } return value; } /** * Clones the given list of nodes. The clones will be located relative to targetRootNode. * <p/> * If a node being cloned is referenceable and there is already node with same UUID in the target workspace, the * location of the node in target workspace determines the result. If node being cloned would become child of the * same parent as the existing node in target workspace, the existing node will be replaced. Otherwise the node * being cloned will get a new UUID. * * @param nodes list of nodes to clone * @param targetRootNode parent for clones * @param parentLimiter (non mandatory) allows to skip certain nodes when creating parent hierarchy for cloned * nodes */ public static void cloneNodes(List<JcrNode> nodes, final JcrNode targetRootNode, ParentLimiter parentLimiter) { TargetRootNodeProvider provider = new TargetRootNodeProvider() { public JcrNode getTargetRootNode(JcrNode arg0) { return targetRootNode; } }; cloneNodes(nodes, provider, parentLimiter); } /** * Scans the given list of nodes and their children for references that target nodes outside subtrees of the nodes * in the list. Alternatively, if the referenced node is not part of any subtree and targetWorkspace is not null, * the targetWorkspace is checked for node with same uuid as the referenced node. * * @param nodes * @param targetWorkspace * @return Map of Node->List of Referenced Nodes */ public static Map<JcrNode, List<JcrNode>> getUnsatisfiedDependencies(List<JcrNode> nodes, JcrWorkspace targetWorkspace) { List<String> paths = new ArrayList<String>(); for (JcrNode node : nodes) { paths.add(node.getPath()); } Map<JcrNode, List<JcrNode>> result = new HashMap<JcrNode, List<JcrNode>>(); for (JcrNode node : nodes) { checkDependencies(node, paths, targetWorkspace, result); } return result; } /** * Checks for the dependencies of the given node and it's children * * @param node * @param paths * @param targetWorkspace * @param result */ private static void checkDependencies(JcrNode node, List<String> paths, JcrWorkspace targetWorkspace, Map<JcrNode, List<JcrNode>> result) { // go through all properties JcrPropertyIterator iterator = node.getProperties(); while (iterator.hasNext()) { JcrProperty property = iterator.nextProperty(); // if it is a reference property if (property.getType() == PropertyType.REFERENCE) { // if the property has multiple values if (property.getDefinition().isMultiple()) { JcrValue values[] = property.getValues(); for (JcrValue value : values) { checkReferenceValue(value, node, paths, targetWorkspace, result); } } else { JcrValue value = property.getValue(); checkReferenceValue(value, node, paths, targetWorkspace, result); } } } // go through children and do a recursive check for dependencies JcrNodeIterator nodes = node.getNodes(); while (nodes.hasNext()) { JcrNode child = nodes.nextNode(); checkDependencies(child, paths, targetWorkspace, result); } } /** * Checks if the node referenced by the value is either a child node of a node from paths or the node exists in * targetWorkspace (if targetWorkspace is not null). * * @param value * @param node * @param paths * @param targetWorkspace * @param result */ private static void checkReferenceValue(JcrValue value, JcrNode node, List<String> paths, JcrWorkspace targetWorkspace, Map<JcrNode, List<JcrNode>> result) { // get the referenced node and it's path JcrNode target = node.getSession().getNodeByIdentifier(value.getString()); String path = target.getPath(); // check if the node is child of node from paths boolean found = false; for (String p : paths) { if (path.startsWith(p)) { found = true; break; } } // in case it is not check if node with same uuid exists in target // workspace if (found == false && targetWorkspace != null) { try { targetWorkspace.getSession().getNodeByIdentifier(value.getString()); found = true; } catch (JcrException ignore) { } } // if the node wasn't found add it to result if (found == false) { List<JcrNode> list = result.get(node); if (list == null) { list = new ArrayList<JcrNode>(); result.put(node, list); } if (!list.contains(target)) { list.add(target); } } } /** * Returns node with given UUID from the session or <code>null</code> if there is no node with such UUID. * * @param session * @param uuid * @return */ public static BrixNode getNodeByUUID(JcrSession session, String uuid) { try { BrixNode node = (BrixNode) session.getNodeByIdentifier(uuid); return node; } catch (JcrException e) { if (e.getCause() instanceof ItemNotFoundException) { return null; } throw e; } } private static class NodePair { JcrNode originalNode; JcrNode targetNode; } /** * Interface that allows to limit copy of parent hierarchy for cloned nodes. * * @author Matej Knopp */ public static interface ParentLimiter { public boolean isFinalParent(JcrNode node, JcrNode parent); } /** * Interface for dynamically providing target root nodes for individual cloned nodes. * * @author Matej Knopp */ public static interface TargetRootNodeProvider { public JcrNode getTargetRootNode(JcrNode node); } }