001/*
002 *  Copyright 2011 Anyware Services
003 *
004 *  Licensed under the Apache License, Version 2.0 (the "License");
005 *  you may not use this file except in compliance with the License.
006 *  You may obtain a copy of the License at
007 *
008 *      http://www.apache.org/licenses/LICENSE-2.0
009 *
010 *  Unless required by applicable law or agreed to in writing, software
011 *  distributed under the License is distributed on an "AS IS" BASIS,
012 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
013 *  See the License for the specific language governing permissions and
014 *  limitations under the License.
015 */
016package org.ametys.cms.repository;
017
018import javax.jcr.ItemExistsException;
019import javax.jcr.ItemNotFoundException;
020import javax.jcr.Node;
021import javax.jcr.NodeIterator;
022import javax.jcr.PathNotFoundException;
023import javax.jcr.Property;
024import javax.jcr.PropertyIterator;
025import javax.jcr.PropertyType;
026import javax.jcr.RepositoryException;
027import javax.jcr.Session;
028import javax.jcr.Value;
029import javax.jcr.version.VersionHistory;
030
031import org.apache.avalon.framework.component.Component;
032import org.apache.avalon.framework.logger.AbstractLogEnabled;
033import org.apache.avalon.framework.thread.ThreadSafe;
034import org.apache.cocoon.util.HashUtil;
035import org.apache.commons.collections.Predicate;
036import org.apache.commons.collections.PredicateUtils;
037import org.apache.commons.lang.StringUtils;
038import org.apache.jackrabbit.core.NodeImpl;
039
040import org.ametys.cms.support.AmetysPredicateUtils;
041import org.ametys.plugins.repository.RepositoryConstants;
042import org.ametys.plugins.repository.collection.AmetysObjectCollectionFactory;
043import org.ametys.plugins.repository.jcr.NameHelper;
044
045/**
046 * Helper for common processing used while synchronizing.
047 */
048public class CloneComponent extends AbstractLogEnabled implements Component, ThreadSafe
049{
050    /** Avalon Role */
051    public static final String ROLE = CloneComponent.class.getName();
052    
053    /**
054     * Adds a node to a parent node using a source node for name, type
055     * and potential UUID.
056     * @param srcNode the source node.
057     * @param parentNode the parent node for the newly created node.
058     * @param nodeName the node name to use.
059     * @return the created node.
060     * @throws RepositoryException if an error occurs.
061     */
062    public Node addNodeWithUUID(Node srcNode, Node parentNode, String nodeName) throws RepositoryException
063    {
064        if (srcNode.isNodeType("nt:frozenNode"))
065        {
066            String nodeTypeName = srcNode.getProperty("jcr:frozenPrimaryType").getString();
067            String uuid = null;
068            
069            if (srcNode.hasProperty("jcr:frozenUuid"))
070            {
071                uuid = srcNode.getProperty("jcr:frozenUuid").getString();
072            }
073            
074            if (uuid == null)
075            {
076                return parentNode.addNode(nodeName, nodeTypeName);
077            }
078            else
079            {
080                return ((NodeImpl) parentNode).addNodeWithUuid(nodeName, nodeTypeName, uuid);
081            }
082        }
083        else
084        {
085            if (!srcNode.isNodeType("mix:referenceable"))
086            {
087                return parentNode.addNode(nodeName, srcNode.getPrimaryNodeType().getName());
088            }
089            else
090            {
091                return ((NodeImpl) parentNode).addNodeWithUuid(nodeName, srcNode.getPrimaryNodeType().getName(), srcNode.getIdentifier());
092            }
093        }
094    }
095    
096    /**
097     * Clones all the properties of a node.
098     * @param srcNode the source node.
099     * @param clonedNode the cloned node.
100     * @param propertyPredicate the property selector.
101     * @throws RepositoryException if an error occurs.
102     */
103    public void cloneAllProperties(Node srcNode, Node clonedNode, Predicate propertyPredicate) throws RepositoryException
104    {
105        // First remove existing matching properties from cloned Node
106        PropertyIterator clonedProperties = clonedNode.getProperties();
107        while (clonedProperties.hasNext())
108        {
109            Property property = clonedProperties.nextProperty();
110            if (AmetysPredicateUtils.ignoreProtectedProperties(propertyPredicate).evaluate(property))
111            {
112                property.remove();
113            }
114        }
115        
116        // Then copy properties
117        PropertyIterator itProperties = srcNode.getProperties();
118        
119        while (itProperties.hasNext())
120        {
121            Property property =  itProperties.nextProperty();
122            
123            // Ignore protected properties
124            if (AmetysPredicateUtils.ignoreProtectedProperties(propertyPredicate).evaluate(property))
125            {
126                cloneProperty(clonedNode, property);
127            }
128        }
129    }
130    
131    /**
132     * Clone a property.
133     * @param clonedNode the node to copy the property to.
134     * @param property the property to clone.
135     * @throws RepositoryException if an error occurs.
136     */
137    protected void cloneProperty(Node clonedNode, Property property) throws RepositoryException
138    {
139        Session session = clonedNode.getSession();
140        
141        if (property.getType() == PropertyType.REFERENCE)
142        {
143            try
144            {
145                // Clone the referenced nodes.
146                if (property.isMultiple())
147                {
148                    Value[] sourceValues = property.getValues();
149                    Value[] clonedValues = new Value[sourceValues.length];
150                    
151                    for (int i = 0; i < sourceValues.length; i++)
152                    {
153                        Node referencedNode = session.getNodeByIdentifier(sourceValues[i].getString());
154                        
155                        Node clonedReferencedNode = null;
156                        try
157                        {
158                            clonedReferencedNode = clonedNode.getSession().getNodeByIdentifier(referencedNode.getIdentifier());
159                        }
160                        catch (ItemNotFoundException e)
161                        {
162                            clonedReferencedNode = getOrCloneNode(clonedNode.getSession(), referencedNode);
163                        }
164                        
165                        clonedValues[i] = session.getValueFactory().createValue(clonedReferencedNode);
166                    }
167                    
168                    clonedNode.setProperty(property.getName(), clonedValues);
169                }
170                else
171                {
172                    Node referencedNode = property.getNode();
173                    
174                    Node clonedReferencedNode = null;
175                    try
176                    {
177                        clonedReferencedNode = clonedNode.getSession().getNodeByIdentifier(referencedNode.getIdentifier());
178                    }
179                    catch (ItemNotFoundException e)
180                    {
181                        clonedReferencedNode = getOrCloneNode(clonedNode.getSession(), referencedNode);
182                    }
183                    
184                    clonedNode.setProperty(property.getName(), clonedReferencedNode);
185                }
186            }
187            catch (ItemNotFoundException e)
188            {
189                // the target node does not exist anymore, this could be due to workflow having been deleted
190            }
191        }
192        else
193        {
194            if (property.getDefinition().isMultiple())
195            {
196                clonedNode.setProperty(property.getName(), property.getValues(), property.getType());
197            }
198            else
199            {
200                clonedNode.setProperty(property.getName(), property.getValue(), property.getType());
201            }
202        }
203    }
204    
205    /**
206     * Clones a node by preserving the source node UUID.
207     * @param srcNode the source node.
208     * @param clonedNode the cloned node.
209     * @param propertyPredicate the property selector.
210     * @param nodePredicate the node selector.
211     * @throws RepositoryException if an error occurs.
212     */
213    public void cloneNodeAndPreserveUUID(Node srcNode, Node clonedNode, Predicate propertyPredicate, Predicate nodePredicate) throws RepositoryException
214    {
215        // Clone properties
216        cloneAllProperties(srcNode, clonedNode, propertyPredicate);
217        
218        // Remove all matching subNodes before cloning, for better handling of same name siblings
219        NodeIterator subNodes = clonedNode.getNodes();
220        
221        while (subNodes.hasNext())
222        {
223            Node subNode = subNodes.nextNode();
224            
225            if (nodePredicate.evaluate(subNode))
226            {
227                subNode.remove();
228            }
229        }
230        
231        // Then copy sub nodes
232        NodeIterator itNodes = srcNode.getNodes();
233        
234        while (itNodes.hasNext())
235        {
236            Node subNode = itNodes.nextNode();
237            
238            if (nodePredicate.evaluate(subNode))
239            {
240                Node clonedSubNode = addNodeWithUUID(subNode, clonedNode, subNode.getName());
241                cloneNodeAndPreserveUUID(subNode, clonedSubNode, propertyPredicate, nodePredicate);
242            }
243        }
244    }
245    
246    /**
247     * Get or clone a node.
248     * @param destSession the destination session.
249     * @param node the node in the source workspace.
250     * @return the cloned Node in the destination session.
251     * @throws RepositoryException if an error occurs.
252     */
253    public Node getOrCloneNode(Session destSession, Node node) throws RepositoryException
254    {
255        return getOrCloneNode(destSession, node, null);
256    }
257    
258    /**
259     * Get or clone a node in a specified version.
260     * @param destSession the destination session.
261     * @param node the node in the source workspace.
262     * @param version the node version to clone, null to get the current version.
263     * @return the cloned Node in the destination session.
264     * @throws RepositoryException if an error occurs.
265     */
266    public Node getOrCloneNode(Session destSession, Node node, String version) throws RepositoryException
267    {
268        Node clonedParentNode = cloneAncestorsAndPreserveUUID(node, destSession);
269        String nodeName = node.getName();
270        
271        if (clonedParentNode.hasNode(nodeName))
272        {
273            return clonedParentNode.getNode(nodeName);
274        }
275        else
276        {
277            Node clonedNode = null;
278            
279            if (StringUtils.isNotEmpty(version))
280            {
281                VersionHistory versionHistory = node.getSession().getWorkspace().getVersionManager().getVersionHistory(node.getPath());
282                Node validatedNode = versionHistory.getVersionByLabel(version).getFrozenNode();
283                clonedNode = addNodeWithUUID(validatedNode, clonedParentNode, nodeName);
284                
285                cloneNodeAndPreserveUUID(validatedNode, clonedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate());
286            }
287            else
288            {
289                clonedNode = addNodeWithUUID(node, clonedParentNode, nodeName);
290                
291                cloneNodeAndPreserveUUID(node, clonedNode, PredicateUtils.truePredicate(), PredicateUtils.truePredicate());
292            }
293            
294            return clonedNode;
295        }
296    }
297    
298    /**
299     * Clones ancestors of a node by preserving the source node UUID.
300     * @param srcNode the source node.
301     * @param destSession the destination session.
302     * @return the parent node the destination workspace.
303     * @throws RepositoryException if an error occurs.
304     */
305    public Node cloneAncestorsAndPreserveUUID(Node srcNode, Session destSession) throws RepositoryException
306    {
307        if (srcNode.getName().length() == 0)
308        {
309            // We are on the root node which already exists
310            return destSession.getRootNode();
311        }
312        else
313        {
314            Node destRootNode = destSession.getRootNode();
315            Node parentNode = srcNode.getParent();
316            String parentNodePath = parentNode.getPath().substring(1);
317            
318            if (parentNodePath.length() == 0)
319            {
320                return destSession.getRootNode();
321            }
322            else if (destRootNode.hasNode(parentNodePath))
323            {
324                // Found existing parent
325                return destRootNode.getNode(parentNodePath);
326            }
327            else
328            {
329                Node clonedAncestorNode = cloneAncestorsAndPreserveUUID(parentNode, destSession);
330                Node clonedParentNode = null;
331                
332                if (clonedAncestorNode.hasNode(parentNode.getName()))
333                {
334                    // Possible with autocreated children
335                    clonedParentNode = clonedAncestorNode.getNode(parentNode.getName());
336                }
337                else
338                {
339                    clonedParentNode = addNodeWithUUID(parentNode, clonedAncestorNode, parentNode.getName());
340                }
341                
342                // Copy only properties
343                cloneNodeAndPreserveUUID(parentNode, clonedParentNode, PredicateUtils.truePredicate(), PredicateUtils.falsePredicate());
344                
345                return clonedParentNode;
346            }
347        }
348    }
349    
350    /**
351     * Clone a content node, cloning its workflow along.
352     * @param destSession the destination session.
353     * @param node the node to clone.
354     * @return the cloned node.
355     * @throws RepositoryException if an error occurs.
356     */
357    public Node cloneContentNodeWithWorkflow(Session destSession, Node node) throws RepositoryException
358    {
359        return cloneContentNodeWithWorkflow(destSession, node, PredicateUtils.truePredicate(), PredicateUtils.truePredicate(), null);
360    }
361    
362    /**
363     * Clone a content node, cloning its workflow along.
364     * @param destSession the destination session.
365     * @param node the node to clone.
366     * @param propertyPredicate a test on the properties.
367     * @param nodePredicate a test on the nodes.
368     * @return the cloned node.
369     * @throws RepositoryException if an error occurs.
370     */
371    public Node cloneContentNodeWithWorkflow(Session destSession, Node node, Predicate propertyPredicate, Predicate nodePredicate) throws RepositoryException
372    {
373        return cloneContentNodeWithWorkflow(destSession, node, propertyPredicate, nodePredicate, null);
374    }
375    
376    /**
377     * Clone a content node, cloning its workflow along.
378     * @param destSession the destination session.
379     * @param node the node to clone.
380     * @param propertyPredicate a test on the properties.
381     * @param nodePredicate a test on the nodes.
382     * @param version the version of the node to clone.
383     * @return the cloned node.
384     * @throws RepositoryException if an error occurs.
385     */
386    public Node cloneContentNodeWithWorkflow(Session destSession, Node node, Predicate propertyPredicate, Predicate nodePredicate, String version) throws RepositoryException
387    {
388        Node clonedContentParentNode = null;
389        
390        String nodeName = node.getName();
391        
392        if (_inCollection(node))
393        {
394            // Clone the ancestors until the collection (hashed nodes will be processed separatel).
395            Node destCollectionNode = cloneAncestorsAndPreserveUUID(node.getParent().getParent(), destSession);
396            // Search for an available node name.
397            nodeName = _getAvailableNodeName(destCollectionNode, node);
398            String[] path = _getHashedPath(nodeName);
399            
400            // Create the two hashed nodes, the content will be created in the second one.
401            Node destHash1 = _getOrAddNode(destCollectionNode, path[0], AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE);
402            clonedContentParentNode = _getOrAddNode(destHash1, path[1], AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE);
403        }
404        else
405        {
406            clonedContentParentNode = cloneAncestorsAndPreserveUUID(node, destSession);
407        }
408        
409        Node clonedNode = null;
410        try
411        {
412            clonedNode = destSession.getNodeByIdentifier(node.getIdentifier());
413        }
414        catch (ItemNotFoundException e)
415        {
416            clonedNode = createNodeClone(node, clonedContentParentNode, nodeName);
417        }
418        
419        cloneNodeAndPreserveUUID(node, clonedNode, propertyPredicate, nodePredicate);
420        
421        // Clone unversioned metadata
422        String unversionedNodeName = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":unversioned";
423        Node unversionedNode = null;
424        
425        if (clonedNode.hasNode(unversionedNodeName))
426        {
427            unversionedNode = clonedNode.getNode(unversionedNodeName);
428        }
429        else
430        {
431            unversionedNode = clonedNode.addNode(unversionedNodeName, "ametys:compositeMetadata");
432        }
433        
434        cloneNodeAndPreserveUUID(node.getNode(unversionedNodeName), unversionedNode, propertyPredicate, nodePredicate);
435        
436        return clonedNode;
437    }
438    
439    private boolean _inCollection(Node node) throws RepositoryException
440    {
441        boolean inCollection = false;
442        
443        try
444        {
445            inCollection = node.getParent().isNodeType(AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE)
446                && node.getParent().getParent().isNodeType(AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE)
447                && node.getParent().getParent().getParent().isNodeType(AmetysObjectCollectionFactory.COLLECTION_NODETYPE);
448        }
449        catch (ItemNotFoundException e)
450        {
451            // Ignore, one parent does not exist, just return false.
452        }
453        
454        return inCollection;
455    }
456    
457    private String _getAvailableNodeName(Node destCollectionNode, Node srcNode) throws RepositoryException
458    {
459        String baseName = srcNode.getName();
460        String nodeName = NameHelper.filterName(baseName);
461        
462        int index = 2;
463        while (_nodeExistsInCollection(destCollectionNode, nodeName))
464        {
465            baseName = srcNode.getName() + "-" + index;
466            nodeName = NameHelper.filterName(baseName);
467            index++;
468        }
469        
470        return nodeName;
471    }
472    
473    private boolean _nodeExistsInCollection(Node collectionNode, String nodeName) throws RepositoryException
474    {
475        try
476        {
477            String[] path = _getHashedPath(nodeName);
478            return collectionNode.getNode(path[0]).getNode(path[1]).hasNode(nodeName);
479        }
480        catch (PathNotFoundException e)
481        {
482            // If a path element is not found, the node doesn't exist.
483            return false;
484        }
485    }
486    
487    private String[] _getHashedPath(String name)
488    {
489        long hash = Math.abs(HashUtil.hash(name));
490        String hashStr = Long.toString(hash, 16);
491        hashStr = StringUtils.leftPad(hashStr, 4, '0');
492        
493        return new String[]{hashStr.substring(0, 2), hashStr.substring(2, 4)};
494    }
495    
496    private static Node _getOrAddNode(Node parent, String name, String type) throws RepositoryException
497    {
498        if (parent.hasNode(name))
499        {
500            return parent.getNode(name);
501        }
502        else
503        {
504            return parent.addNode(name, type);
505        }
506    }
507    
508    /**
509     * Create a clone of the specified node.
510     * @param srcNode the node to clone.
511     * @param parentNode the node under which to create the clonde.
512     * @param desiredNodeName the wanted node name.
513     * @return the cloned node.
514     * @throws RepositoryException if an error occurs.
515     */
516    protected Node createNodeClone(Node srcNode, Node parentNode, String desiredNodeName) throws RepositoryException
517    {
518        Node clonedNode = null;
519        
520        String nodeName = NameHelper.filterName(desiredNodeName);
521        
522        int errorCount = 0;
523        do
524        {
525            try
526            {
527                clonedNode = addNodeWithUUID(srcNode, parentNode, nodeName);
528            }
529            catch (ItemExistsException e)
530            {
531                // Node name is already used.
532                errorCount++;
533                
534                nodeName = NameHelper.filterName(desiredNodeName + " " + (errorCount + 1));
535            }
536        }
537        while (clonedNode == null);
538        
539        return clonedNode;
540    }
541    
542    /**
543     * Reorder a node, mirroring the order in the default workspace.
544     * @param parentNode the parent of the source Node in the default workspace.
545     * @param nodeName the node name.
546     * @param node the node in the destination workspace to be reordered.
547     * @throws RepositoryException if an error occurs.
548     */
549    public void orderNode(Node parentNode, String nodeName, Node node) throws RepositoryException
550    {
551        Session destSession = node.getSession();
552        
553        // iterate over the siblings to find the following
554        NodeIterator siblings = parentNode.getNodes();
555        boolean iterate = true;
556        
557        while (siblings.hasNext() && iterate)
558        {
559            Node sibling = siblings.nextNode();
560            iterate = !sibling.getName().equals(nodeName);
561        }
562        
563        // iterator is currently on the pageNode
564        
565        Node nextSibling = null;
566        while (siblings.hasNext() && nextSibling == null)
567        {
568            Node sibling = siblings.nextNode();
569            String name = sibling.getName();
570            String path = sibling.getPath();
571            if (!name.startsWith(RepositoryConstants.NAMESPACE_PREFIX + ":") && !name.startsWith(RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":") && destSession.itemExists(path))
572            {
573                nextSibling = sibling;
574            }
575        }
576        
577        // nextSibling is either null meaning that the Node must be ordered last or is equals to the following sibling
578        if (nextSibling != null)
579        {
580            node.getParent().orderBefore(nodeName, nextSibling.getName());
581        }
582        else
583        {
584            node.getParent().orderBefore(nodeName, null);
585        }
586    }
587    
588}