001/*
002 *  Copyright 2010 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 */
016
017package org.ametys.plugins.repository.jcr;
018
019import java.util.ArrayList;
020import java.util.List;
021
022import javax.jcr.ItemExistsException;
023import javax.jcr.Node;
024import javax.jcr.NodeIterator;
025import javax.jcr.RepositoryException;
026import javax.jcr.Value;
027import javax.jcr.lock.Lock;
028import javax.jcr.lock.LockManager;
029import javax.jcr.nodetype.NodeType;
030
031import org.apache.avalon.framework.logger.Logger;
032import org.apache.jackrabbit.util.Text;
033
034import org.ametys.plugins.repository.AmetysObject;
035import org.ametys.plugins.repository.AmetysObjectFactory;
036import org.ametys.plugins.repository.AmetysObjectFactoryExtensionPoint;
037import org.ametys.plugins.repository.AmetysObjectIterable;
038import org.ametys.plugins.repository.AmetysObjectResolver;
039import org.ametys.plugins.repository.AmetysRepositoryException;
040import org.ametys.plugins.repository.ChainedAmetysObjectIterable;
041import org.ametys.plugins.repository.CollectionIterable;
042import org.ametys.plugins.repository.RepositoryConstants;
043import org.ametys.plugins.repository.RepositoryIntegrityViolationException;
044import org.ametys.plugins.repository.TraversableAmetysObject;
045import org.ametys.plugins.repository.UnknownAmetysObjectException;
046import org.ametys.plugins.repository.virtual.VirtualAmetysObjectFactory;
047
048/**
049 * Helper for implementing {@link TraversableAmetysObject} stored in JCR.
050 */
051public final class TraversableAmetysObjectHelper
052{
053    private TraversableAmetysObjectHelper()
054    {
055        // empty
056    }
057    
058    /**
059     * Returns the {@link AmetysObject} at the given subPath,
060     * relative to the given {@link DefaultTraversableAmetysObject}.
061     * @param <A> the actual type of {@link AmetysObject}.
062     * @param object the context {@link DefaultTraversableAmetysObject}.
063     * @param factory the corresponding {@link JCRAmetysObjectFactory}.
064     * @param path the sub path. Cannot be <code>null</code>, empty or absolute.
065     * @param resolver the {@link AmetysObjectResolver}.
066     * @param logger a {@link Logger} for traces.
067     * @return the {@link AmetysObject} at the given subPath,
068     *         relative to the given {@link DefaultTraversableAmetysObject}.
069     * @throws AmetysRepositoryException if an error occurs.
070     * @throws UnknownAmetysObjectException if no such object exists.
071     */
072    @SuppressWarnings("unchecked")
073    public static <A extends AmetysObject> A getChild(JCRTraversableAmetysObject object, JCRAmetysObjectFactory factory, String path, AmetysObjectResolver resolver, Logger logger) throws AmetysRepositoryException, UnknownAmetysObjectException
074    {
075        if (logger.isDebugEnabled())
076        {
077            logger.debug("Entering DefaultTraversableAmetysObjectFactory.getChild with path=" + path + ", object=" + object);
078        }
079
080        if (path == null || "".equals(path) || path.charAt(0) == '/')
081        {
082            throw new AmetysRepositoryException("Child path cannot be null, empty or absolute");
083        }
084        
085        Node node = object.getNode();
086        
087        try
088        {
089            // instead of going through resolver for each path segment, we first test the nodetype of the sub Node. 
090            // If it's same than this factory, there's no need to resolve anything.   
091            String[] pathElements = path.split("/");
092            
093            Node contextNode = node;
094            String contextPath = object.getPath();
095            String contextParentPath = null;
096            
097            int i = 0;
098            while (i < pathElements.length)
099            {
100                if (logger.isDebugEnabled())
101                {
102                    logger.debug("contextPath=" + contextPath + ", pathElement=" + pathElements[i]);
103                }
104
105                if (".".equals(pathElements[i]) || "..".equals(pathElements[i]))
106                {
107                    throw new AmetysRepositoryException("Path cannot contain segment with . or ..");
108                }
109                
110                // handle special characters for local part
111                String jcrName = _escape(pathElements[i]);
112
113                if (contextNode.hasNode(jcrName))
114                {
115                    // the path element corresponds to a JCR Node
116                    Node subNode = contextNode.getNode(jcrName);
117                    String type = NodeTypeHelper.getNodeTypeName(subNode);
118                    
119                    if (factory.getNodetypes().contains(type))
120                    {
121                        if (logger.isDebugEnabled())
122                        {
123                            logger.debug("The nodetype is the same as the current factory, no need to go through resolver: " + type);
124                        }
125
126                        contextParentPath = contextPath;
127                        contextPath += "/" + subNode.getName();
128                        contextNode = subNode;
129                        i++;
130                    }
131                    else
132                    {
133                        return (A) resolver.resolve(contextPath, subNode, _computeSubPath(pathElements, i + 1), false);
134                    }
135                }
136                else if (contextNode.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY))
137                {
138                    // the sub node does not exist, but there may be virtual children
139                    JCRAmetysObject contextObject = resolver.resolve(contextParentPath, contextNode, null, true);
140                    
141                    if (contextObject == null)
142                    {
143                        throw new UnknownAmetysObjectException("There's no object at path " + jcrName + " from path " + contextPath);
144                    }
145                    
146                    return (A) resolver.resolveVirtualChild(contextObject, _computeSubPath(pathElements, i));
147                }
148                else
149                {
150                    // there's no children
151                    throw new UnknownAmetysObjectException("There's no object at path " + jcrName + " from path " + contextPath);
152                }
153            }
154            
155            // on est arrivés au bout du subPath, ce qui signifie que tous les descendants sont connus par cette factory 
156            return (A) factory.getAmetysObject(contextNode, contextParentPath);
157        }
158        catch (RepositoryException e)
159        {
160            throw new AmetysRepositoryException("Unable to resolve AmetysObject at path " + path + " relative to AmetysObject at path " + object.getPath(), e);
161        }
162    }
163    
164    private static String _computeSubPath(String[] pathElements, int beginIndex)
165    {
166        String subPath = null;
167        
168        for (int j = beginIndex; j < pathElements.length; j++)
169        {
170            subPath = subPath == null ? pathElements[j] : subPath + "/" + pathElements[j];
171        }
172        
173        return subPath;
174    }
175
176    /**
177     * Returns all children of the given {@link DefaultTraversableAmetysObject}.
178     * @param <A> the actual type of {@link AmetysObject}s
179     * @param object a {@link DefaultTraversableAmetysObject}.
180     * @param factory the corresponding {@link JCRAmetysObjectFactory}.
181     * @param resolver the {@link AmetysObjectResolver}.
182     * @param logger a {@link Logger} for traces.
183     * @return a List containing all children object in the Ametys hierarchy.
184     * @throws AmetysRepositoryException if an error occurs.
185     */
186    @SuppressWarnings("unchecked")
187    public static <A extends AmetysObject> AmetysObjectIterable<A> getChildren(JCRTraversableAmetysObject object, JCRAmetysObjectFactory factory, AmetysObjectResolver resolver, Logger logger) throws AmetysRepositoryException
188    {
189        if (logger.isDebugEnabled())
190        {
191            logger.debug("Entering DefaultTraversableAmetysObjectFactory.getChildren with object=" + object);
192        }
193
194        try
195        {
196            Node node = object.getNode();
197            NodeIterator it = node.getNodes();
198            List<A> children = new ArrayList<>((int) it.getSize());
199            
200            while (it.hasNext())
201            {
202                Node child = it.nextNode();
203                String type = NodeTypeHelper.getNodeTypeName(child);
204                
205                if (factory.getNodetypes().contains(type))
206                {
207                    // if the node type correspond to the factory, do not go trough resolver
208                    children.add((A) factory.getAmetysObject(child, object.getPath()));
209                }
210                else
211                {
212                    A obj = resolver.<A>resolve(object.getPath(), child, null, true);
213                    
214                    if (obj != null)
215                    {
216                        children.add(obj);
217                    }
218                }
219            }
220            
221            AmetysObjectIterable<A> childrenIt = new CollectionIterable<>(children);
222            
223            // on regarde les virtuels
224            if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY))
225            {
226                AmetysObjectIterable<A> virtualIt = resolver.resolveVirtualChildren(object);
227                
228                List<AmetysObjectIterable<A>> chainedList = new ArrayList<>();
229                chainedList.add(childrenIt);
230                chainedList.add(virtualIt);
231                
232                return new ChainedAmetysObjectIterable<>(chainedList);
233            }
234            else
235            {
236                return childrenIt;
237            }
238        }
239        catch (RepositoryException e)
240        {
241            throw new AmetysRepositoryException("Unable to retrieve children", e);
242        }
243    }
244    
245    /**
246     * Tests if a given object has a child with a given name.
247     * @param object the context object.
248     * @param name the name to test.
249     * @param ametysFactoryExtensionPoint the {@link AmetysObjectFactoryExtensionPoint}.
250     * @param logger a {@link Logger} for traces.
251     * @return <code>true</code> is the given object has a child with the given name,
252     *         <code>false</code> otherwise.
253     * @throws AmetysRepositoryException if an error occurs.
254     */
255    public static boolean hasChild(JCRTraversableAmetysObject object, String name, AmetysObjectFactoryExtensionPoint ametysFactoryExtensionPoint, Logger logger) throws AmetysRepositoryException
256    {
257        if (logger.isDebugEnabled())
258        {
259            logger.debug("Entering DefaultTraversableAmetysObjectFactory.hasChild with object=" + object + ", name=" + name);
260        }
261
262        if (name == null || "".equals(name) || name.charAt(0) == '/')
263        {
264            throw new AmetysRepositoryException("Child name cannot be null, empty or absolute");
265        }
266        
267        if (".".equals(name) || "..".equals(name))
268        {
269            throw new AmetysRepositoryException("Child name cannot be . or ..");
270        }
271        
272        Node node = object.getNode();
273        
274        try
275        {
276            String jcrName = _escape(name);
277            if (node.hasNode(jcrName))
278            {
279                if (logger.isDebugEnabled())
280                {
281                    logger.debug("Child node exists: " + jcrName);
282                }
283
284                // if a physical node exists, its an Ametys child if and only if its nodetype is known 
285                Node childNode = node.getNode(jcrName);
286                String nodetype = NodeTypeHelper.getNodeTypeName(childNode);
287                return ametysFactoryExtensionPoint.getFactoryForNodetype(nodetype) != null;
288            }
289            else if (node.hasProperty(AmetysObjectResolver.VIRTUAL_PROPERTY))
290            {
291                if (logger.isDebugEnabled())
292                {
293                    logger.debug("Looking for virtuals...");
294                }
295
296                // looking at virtuals...
297                Value[] values = node.getProperty(AmetysObjectResolver.VIRTUAL_PROPERTY).getValues();
298                for (Value value : values)
299                {
300                    String virtual = value.getString();
301
302                    VirtualAmetysObjectFactory virtualFactory = _getVirtualFactory(virtual, ametysFactoryExtensionPoint, logger);
303                    
304                    if (virtualFactory.hasChild(object, name))
305                    {
306                        return true;
307                    }
308                }
309            }
310            
311            return false;
312        }
313        catch (RepositoryException e)
314        {
315            throw new AmetysRepositoryException("Unable to test if the underlying Node for object " + object.getId() + " has a child named " + name, e);
316        }
317    }
318    
319    private static VirtualAmetysObjectFactory _getVirtualFactory(String id, AmetysObjectFactoryExtensionPoint ametysFactoryExtensionPoint, Logger logger)
320    {
321        if (logger.isDebugEnabled())
322        {
323            logger.debug("Found virtual id: " + id);
324        }
325        
326        AmetysObjectFactory factory = ametysFactoryExtensionPoint.getExtension(id);
327        
328        if (factory == null)
329        {
330            throw new AmetysRepositoryException("There's no virtual factory for id " + id);
331        }
332        
333        if (!(factory instanceof VirtualAmetysObjectFactory))
334        {
335            throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory: " + id);
336        }
337        
338        VirtualAmetysObjectFactory virtualFactory = (VirtualAmetysObjectFactory) factory;
339        
340        return virtualFactory;
341    }
342
343    /**
344     * Creates a child to the given object.
345     * @param <A> the actual type of {@link AmetysObject}.
346     * @param object the parent {@link AmetysObject}.
347     * @param factory the corresponding {@link JCRAmetysObjectFactory}.
348     * @param name the new object's name.
349     * @param type the new object's type.
350     * @param ametysFactoryExtensionPoint the {@link AmetysObjectFactoryExtensionPoint}.
351     * @param resolver the {@link AmetysObjectResolver}.
352     * @param logger a {@link Logger} for traces.
353     * @return the newly created {@link AmetysObject}.
354     * @throws AmetysRepositoryException if an error occurs.
355     */
356    @SuppressWarnings("unchecked")
357    public static <A extends AmetysObject> A createChild(JCRTraversableAmetysObject object, JCRAmetysObjectFactory factory, String name, String type, AmetysObjectFactoryExtensionPoint ametysFactoryExtensionPoint, AmetysObjectResolver resolver, Logger logger) throws AmetysRepositoryException
358    {
359        if (logger.isDebugEnabled())
360        {
361            logger.debug("Entering DefaultTraversableAmetysObjectFactory.createChild with object=" + object + ", name=" + name + ", type=" + type);
362        }
363
364        // the code of this method is mainly duplicated from the AmetysObjectResolver.createAndResolve method, 
365        // with the optimization that there's no need to go through resolver when the nodetype id the same than this factory
366
367        if (ametysFactoryExtensionPoint.getFactoryForNodetype(type) == null)
368        {
369            throw new AmetysRepositoryException("Cannot create a node '" + name +  "' under '" + object.getPath() + " (" + object.getId() + ")': There's no factory for nodetype: " + type);
370        }
371
372        Node contextNode = object.getNode();
373        
374        try
375        {
376            _checkLock(contextNode);
377            
378            String legalName = _escape(name);
379            Node node = contextNode.addNode(legalName, type);
380            NodeType[] mixinNodeTypes = node.getMixinNodeTypes();
381            boolean foundMixin = false;
382            
383            int i = 0;
384            while (!foundMixin && i < mixinNodeTypes.length)
385            {
386                if (AmetysObjectResolver.OBJECT_TYPE.equals(mixinNodeTypes[i].getName()))
387                {
388                    foundMixin = true;
389                }
390                
391                i++;
392            }
393            
394            if (!foundMixin)
395            {
396                node.addMixin(AmetysObjectResolver.OBJECT_TYPE);
397            }
398            
399            if (factory.getNodetypes().contains(type))
400            {
401                // pas besoin de repasser par le resolver si le type est le même que cette factory
402                return (A) factory.getAmetysObject(node, object.getPath());
403            }
404            
405            return (A) resolver.resolve(object.getPath(), node, null, false);
406        }
407        catch (ItemExistsException e)
408        {
409            throw new RepositoryIntegrityViolationException("The object " + name + " already exist at path " + object.getParentPath(), e);
410        }
411        catch (RepositoryException e)
412        {
413            throw new AmetysRepositoryException("Unable to add child node for the underlying node for object " + object.getId(), e);
414        }
415    }
416    
417    private static String _escape(String qName)
418    {
419        int index = qName.indexOf(':');
420        
421        if (index == -1)
422        {
423            return Text.escapeIllegalJcrChars(qName);
424        }
425        else
426        {
427            return qName.substring(0, index) + ':' + Text.escapeIllegalJcrChars(qName.substring(index + 1, qName.length()));
428        }
429    }
430    
431    private static void _checkLock(Node node) throws RepositoryException
432    {
433        if (node.isLocked())
434        {
435            LockManager lockManager = node.getSession().getWorkspace().getLockManager();
436            Lock lock = lockManager.getLock(node.getPath());
437            Node lockHolder = lock.getNode();
438            
439            lockManager.addLockToken(lockHolder.getProperty(RepositoryConstants.METADATA_LOCKTOKEN).getString());
440        }
441    }
442}