001/*
002 *  Copyright 2019 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.plugins.repository.jcr;
017
018import java.util.ArrayList;
019import java.util.List;
020
021import javax.jcr.Node;
022import javax.jcr.NodeIterator;
023import javax.jcr.Property;
024import javax.jcr.PropertyIterator;
025import javax.jcr.RepositoryException;
026
027import org.apache.cocoon.util.HashUtil;
028import org.apache.commons.lang3.StringUtils;
029import org.apache.jackrabbit.JcrConstants;
030import org.apache.jackrabbit.core.NodeImpl;
031
032import org.ametys.plugins.repository.AmetysRepositoryException;
033import org.ametys.plugins.repository.collection.AmetysObjectCollectionFactory;
034
035/**
036 * Provides helper methods on nodes.
037 */
038public final class NodeHelper
039{
040    private NodeHelper()
041    {
042        // Hides the default constructor.
043    }
044
045    /**
046     * Rename the given {@link Node} with the given new name
047     * @param node the node to rename
048     * @param newName the new name of the node
049     * @throws AmetysRepositoryException if an error occurs.
050     */
051    public static void rename(Node node, String newName) throws AmetysRepositoryException
052    {
053        try
054        {
055            Node parentNode = node.getParent();
056            boolean order = parentNode.getPrimaryNodeType().hasOrderableChildNodes();
057            Node nextSibling = null;
058            
059            if (order)
060            {
061                // iterate over the siblings to find the following
062                NodeIterator siblings = parentNode.getNodes();
063                boolean iterate = true;
064                
065                while (siblings.hasNext() && iterate)
066                {
067                    Node sibling = siblings.nextNode();
068                    iterate = !sibling.getName().equals(node.getName());
069                }
070                
071                // iterator is currently on the node
072                while (siblings.hasNext() && nextSibling == null)
073                {
074                    Node sibling = siblings.nextNode();
075                    String path = sibling.getPath();
076                    if (node.getSession().itemExists(path))
077                    {
078                        nextSibling = sibling;
079                    }
080                }
081            }
082            
083            node.getSession().move(node.getPath(), node.getParent().getPath() + "/" + newName);
084            
085            if (order)
086            {
087                // nextSibling is either null meaning that the Node must be ordered last or is equals to the following sibling
088                if (nextSibling != null)
089                {
090                    parentNode.orderBefore(newName, nextSibling.getName());
091                }
092                else
093                {
094                    parentNode.orderBefore(newName, null);
095                }
096            }
097        }
098        catch (RepositoryException e)
099        {
100            throw new AmetysRepositoryException(e);
101        }
102    }
103
104    /**
105     * Clone the node to the given parent node keeping the identifiers.
106     * To be able to do that, parent node should be in another workspace than the node to copy.
107     * The destination node should not already contains a node with the same name as the node to copy except if same name siblings is allowed.
108     * This method does not save the parent node.
109     * @param nodeToCopy The node to copy.
110     * @param parentNode The parent node of the destination node.
111     * @return the create node
112     * @throws AmetysRepositoryException if an error occurs
113     */
114    public static Node cloneNode(Node nodeToCopy, Node parentNode) throws AmetysRepositoryException
115    {
116        try
117        {
118            return cloneNode(nodeToCopy, parentNode, nodeToCopy.getName());
119        }
120        catch (RepositoryException e)
121        {
122            throw new AmetysRepositoryException(e);
123        }
124    }
125    
126    /**
127     * Clone the node to the given parent node keeping the identifiers.
128     * To be able to do that, parent node should be in another workspace than the node to copy.
129     * The destination node should not already contains a node with the given node name except if same name siblings is allowed.
130     * This method does not save the parent node.
131     * @param nodeToCopy The node to copy.
132     * @param parentNode The parent node of the destination node.
133     * @param nodeName The destination node name.
134     * @return the create node
135     * @throws AmetysRepositoryException if an error occurs
136     */
137    public static Node cloneNode(Node nodeToCopy, Node parentNode, String nodeName) throws AmetysRepositoryException
138    {
139        try
140        {
141            // Create the base node with the identifier if the node is referenceable
142            Node destNode = nodeToCopy.isNodeType(JcrConstants.MIX_REFERENCEABLE)
143                    ? ((NodeImpl) parentNode).addNodeWithUuid(nodeName, nodeToCopy.getPrimaryNodeType().getName(), nodeToCopy.getIdentifier())
144                    : parentNode.addNode(nodeName, nodeToCopy.getPrimaryNodeType().getName());
145            
146            // Copy properties
147            PropertyIterator properties = nodeToCopy.getProperties();
148            while (properties.hasNext())
149            {
150                Property property = properties.nextProperty();
151                
152                if (!property.getDefinition().isProtected())
153                {
154                    if (property.isMultiple())
155                    {
156                        destNode.setProperty(property.getName(), property.getValues(), property.getType());
157                    }
158                    else
159                    {
160                        destNode.setProperty(property.getName(), property.getValue(), property.getType());
161                    }
162                }
163            }
164            
165            // Copy sub-nodes
166            NodeIterator subNodes = nodeToCopy.getNodes();
167            while (subNodes.hasNext())
168            {
169                Node subNode = subNodes.nextNode();
170                if (subNode.getDefinition().isAutoCreated())
171                {
172                    destNode.getNode(subNode.getName()).remove();
173                }
174                cloneNode(subNode, destNode);
175            }
176            
177            return destNode;
178        }
179        catch (RepositoryException e)
180        {
181            throw new AmetysRepositoryException(e);
182        }
183    }
184    
185    /**
186     * Computes a hashed path in the JCR tree from the name of the child object.<br>
187     * Subclasses may override this method to provide a more suitable hash function.<br>
188     * This implementation relies on the buzhash algorithm.
189     * This method MUST return an array of the same length for each name.
190     * @param name the name of the child object
191     * @return a hashed path of the name.
192     */
193    public static List<String> hashAsList(String name)
194    {
195        long hash = Math.abs(HashUtil.hash(name));
196        String hashStr = Long.toString(hash, 16);
197        hashStr = StringUtils.leftPad(hashStr, 4, '0');
198        return List.of(hashStr.substring(0, 2), hashStr.substring(2, 4));
199    }
200    
201    /**
202     * Get the path with hashed nodes.
203     * @param name the name of the final node
204     * @return the path with hashed nodes
205     */
206    public static String getFullHashPath(String name)
207    {
208        List<String> pathList = new ArrayList<>(hashAsList(name));
209        pathList.add(name);
210        return StringUtils.join(pathList, "/");
211    }
212
213    /**
214     * Get or create hash nodes for the given name, intermediate nodes are of type {@value AmetysObjectCollectionFactory#COLLECTION_ELEMENT_NODETYPE}.
215     * The final node is not created.
216     * The session is not saved.
217     * @param parentNode the parent node of the hashed nodes
218     * @param name the name of the final node.
219     * @return the last level of hashed nodes
220     * @throws AmetysRepositoryException if an error occurs
221     */
222    public static Node getOrCreateFinalHashNode(Node parentNode, String name) throws AmetysRepositoryException
223    {
224        return getOrCreateFinalHashNode(parentNode, name, AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE);
225    }
226    
227    /**
228     * Get or create hash nodes for the given name and intermediate primary type.
229     * The final node is not created.
230     * The session is not saved.
231     * @param parentNode the parent node of the hashed nodes
232     * @param name the name of the final node.
233     * @param hashType the primary type of hashed nodes
234     * @return the last level of hashed nodes
235     * @throws AmetysRepositoryException if an error occurs
236     */
237    public static Node getOrCreateFinalHashNode(Node parentNode, String name, String hashType) throws AmetysRepositoryException
238    {
239        try
240        {
241            Node finalNode = parentNode;
242            
243            for (String hashPart : hashAsList(name))
244            {
245                finalNode = finalNode.hasNode(hashPart)
246                        ? finalNode.getNode(hashPart)
247                        : finalNode.addNode(hashPart, hashType);
248            }
249            
250            return finalNode;
251        }
252        catch (RepositoryException e)
253        {
254            throw new AmetysRepositoryException(e);
255        }
256    }
257}