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