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;
018
019import java.io.IOException;
020import java.io.InputStream;
021import java.util.ArrayList;
022import java.util.Arrays;
023import java.util.Collection;
024import java.util.List;
025
026import javax.jcr.ItemExistsException;
027import javax.jcr.NamespaceRegistry;
028import javax.jcr.Node;
029import javax.jcr.PathNotFoundException;
030import javax.jcr.Repository;
031import javax.jcr.RepositoryException;
032import javax.jcr.Session;
033import javax.jcr.Value;
034import javax.jcr.nodetype.NodeType;
035import javax.jcr.query.Query;
036
037import org.apache.avalon.framework.activity.Initializable;
038import org.apache.avalon.framework.component.Component;
039import org.apache.avalon.framework.service.ServiceException;
040import org.apache.avalon.framework.service.ServiceManager;
041import org.apache.avalon.framework.service.Serviceable;
042import org.apache.excalibur.source.Source;
043import org.apache.excalibur.source.SourceResolver;
044import org.apache.jackrabbit.core.nodetype.InvalidNodeTypeDefException;
045import org.apache.jackrabbit.core.nodetype.NodeTypeDefStore;
046import org.apache.jackrabbit.core.nodetype.NodeTypeManagerImpl;
047import org.apache.jackrabbit.core.nodetype.NodeTypeRegistry;
048import org.apache.jackrabbit.spi.Name;
049import org.apache.jackrabbit.spi.QNodeTypeDefinition;
050import org.slf4j.Logger;
051
052import org.ametys.core.util.LambdaUtils;
053import org.ametys.plugins.repository.jcr.JCRAmetysObject;
054import org.ametys.plugins.repository.jcr.JCRAmetysObjectFactory;
055import org.ametys.plugins.repository.jcr.NodeTypeHelper;
056import org.ametys.plugins.repository.migration.jcr.repository.VersionsFactory;
057import org.ametys.plugins.repository.provider.AbstractRepository;
058import org.ametys.plugins.repository.provider.JackrabbitRepository;
059import org.ametys.plugins.repository.virtual.VirtualAmetysObjectFactory;
060import org.ametys.runtime.plugin.component.AbstractLogEnabled;
061
062/**
063 * Base component for accessing {@link AmetysObject}s.
064 */
065public class AmetysObjectResolver extends AbstractLogEnabled implements Serviceable, Initializable, Component
066{
067    /** Avalon ROLE. */
068    public static final String ROLE = AmetysObjectResolver.class.getName();
069
070    /** JCR Relative Path to root. */
071    public static final String ROOT_REPO = "ametys:root";
072
073    /** JCR type for root node. */
074    public static final String ROOT_TYPE = "ametys:root";
075
076    /** JCR mixin type for objects. */
077    public static final String OBJECT_TYPE = "ametys:object";
078
079    /** JCR property name for virtual objects. */
080    public static final String VIRTUAL_PROPERTY = "ametys-internal:virtual";
081
082    private AmetysObjectFactoryExtensionPoint _ametysFactoryExtensionPoint;
083    private NamespacesExtensionPoint _namespacesExtensionPoint;
084    private NodeTypeDefinitionsExtensionPoint _nodetypeDefsExtensionPoint;
085    private Repository _repository;
086    private SourceResolver _resolver;
087
088    
089    @Override
090    public void service(ServiceManager manager) throws ServiceException
091    {
092        _resolver = (SourceResolver) manager.lookup(SourceResolver.ROLE);
093        _repository = (Repository) manager.lookup(AbstractRepository.ROLE);
094        _ametysFactoryExtensionPoint = (AmetysObjectFactoryExtensionPoint) manager.lookup(AmetysObjectFactoryExtensionPoint.ROLE);
095        _namespacesExtensionPoint = (NamespacesExtensionPoint) manager.lookup(NamespacesExtensionPoint.ROLE);
096        _nodetypeDefsExtensionPoint = (NodeTypeDefinitionsExtensionPoint) manager.lookup(NodeTypeDefinitionsExtensionPoint.ROLE);
097    }
098    
099    @Override
100    public void initialize() throws Exception
101    {
102        // On vérifie que le root soit bien créé
103        Session session = _repository.login();
104
105        _initNamespaces(session);
106        _initNodetypes(session);
107
108        if (!session.getRootNode().hasNode(ROOT_REPO))
109        {
110            getLogger().info("Creating ametys root Node");
111            
112            session.getRootNode().addNode(ROOT_REPO, ROOT_TYPE);
113            
114            initRepoNodes(session, getLogger());
115        }
116        
117        if (session.hasPendingChanges())
118        {
119            session.save();
120        }
121        
122        session.logout();
123        if (_repository instanceof JackrabbitRepository)
124        {
125            // Now that custom_nodetypes.xml have been (re)created, compare it with the previous version and delete the backup if they are the same
126            ((JackrabbitRepository) _repository).compareCustomNodetypes();
127        }
128    }
129    
130    /**
131     * Init the repository nodes directly inside ROOT node
132     * @param session the session to use
133     * @param logger the logger to log actions
134     * @return true if the sesion needs changes
135     * @throws RepositoryException something went wrong
136     */
137    public static boolean initRepoNodes(Session session, Logger logger) throws RepositoryException
138    {
139        // Create Ametys migration versions node if we just created the repository.
140        logger.info("Creating ametys migration versions root Node");
141        session.getRootNode().getNode(ROOT_REPO)
142                    .addNode(VersionsFactory.VERSIONS_NODENAME, VersionsFactory.VERSIONS_NODETYPE);
143        
144        return session.hasPendingChanges();
145    }
146    
147    private void _initNamespaces(Session session) throws RepositoryException
148    {
149        NamespaceRegistry registry = session.getWorkspace().getNamespaceRegistry();
150        Collection prefixes = Arrays.asList(registry.getPrefixes());
151        
152        _namespacesExtensionPoint.getExtensionsIds().stream()
153                                                    .filter(prefix -> !prefixes.contains(prefix))
154                                                    .forEach(LambdaUtils.wrapConsumer(prefix -> 
155                                                    {
156                                                        String namespace = _namespacesExtensionPoint.getNamespace(prefix);
157                                                        getLogger().debug("Adding {} namespace", prefix);
158                                                        registry.registerNamespace(prefix, namespace);
159                                                    }));
160    }
161
162    private void _initNodetypes(Session session) throws RepositoryException, InvalidNodeTypeDefException, IOException
163    {
164        NodeTypeDefStore store = new NodeTypeDefStore();
165
166        // Hard-coded first nodetypes file
167        Source fsource = _resolver.resolveURI("plugin:repository://nodetypes/ametys_nodetypes.xml");
168        try (InputStream is = fsource.getInputStream())
169        {
170            store.load(is);
171        }
172        finally
173        {
174            _resolver.release(fsource);
175        }
176        
177        // Load all declared nodetype definitions in the store.
178        for (String nodetypeDef : _nodetypeDefsExtensionPoint.getNodeTypeDefinitions())
179        {
180            Source source = _resolver.resolveURI(nodetypeDef);
181            try (InputStream is = source.getInputStream())
182            {
183                store.load(is);
184            }
185            finally
186            {
187                _resolver.release(source);
188            }
189        }
190        
191        NodeTypeManagerImpl ntManager = (NodeTypeManagerImpl) session.getWorkspace().getNodeTypeManager();
192        NodeTypeRegistry registry = ntManager.getNodeTypeRegistry();
193        
194        // Remove all already registered nodetypes from the store.
195        for (Name name : registry.getRegisteredNodeTypes())
196        {
197            store.remove(name);
198        }
199        
200        // Register the "new" nodetype definitions.
201        Collection<QNodeTypeDefinition> ntDefs = store.all();
202        if (!ntDefs.isEmpty())
203        {
204            registry.registerNodeTypes(ntDefs);
205        }
206    }
207
208    /**
209     * Retrieves an {@link AmetysObject} from an absolute path.
210     * The given path is absolute in the Ametys tree.<br>
211     * The path may omit the leading <code>'/'</code>, but the path
212     * is always considered absolute, <code>null</code> path is forbidden.<br>
213     * @param <A> the actual type of {@link AmetysObject}.
214     * @param absolutePath the path to use.
215     * @return the corresponding AmetysObject.
216     * @throws AmetysRepositoryException if an error occurs.
217     * @throws UnknownAmetysObjectException if no such object exists for the given path.
218     * @deprecated Use resolveByPath instead
219     */
220    @Deprecated
221    public <A extends AmetysObject> A resolve(String absolutePath) throws AmetysRepositoryException, UnknownAmetysObjectException
222    {
223        return resolveByPath(absolutePath);
224    }
225    
226    /**
227     * Retrieves an {@link AmetysObject} from an absolute path.
228     * The given path is absolute in the Ametys tree.<br>
229     * The path may omit the leading <code>'/'</code>, but the path
230     * is always considered absolute, <code>null</code> path is forbidden.<br>
231     * @param <A> the actual type of {@link AmetysObject}.
232     * @param absolutePath the path to use.
233     * @return the corresponding AmetysObject.
234     * @throws AmetysRepositoryException if an error occurs.
235     * @throws UnknownAmetysObjectException if no such object exists for the given path.
236     */
237    public <A extends AmetysObject> A resolveByPath(String absolutePath) throws AmetysRepositoryException, UnknownAmetysObjectException
238    {
239        return resolveByPath(absolutePath, null);
240    }
241    
242    /**
243     * Retrieves an {@link AmetysObject} from an absolute path.
244     * The given path is absolute in the Ametys tree.<br>
245     * The path may omit the leading <code>'/'</code>, but the path
246     * is always considered absolute, <code>null</code> path is forbidden.<br>
247     * @param <A> the actual type of {@link AmetysObject}.
248     * @param absolutePath the path to use.
249     * @param session the JCR Session to use to retrieve the {@link AmetysObject}.
250     * @return the corresponding AmetysObject.
251     * @throws AmetysRepositoryException if an error occurs.
252     * @throws UnknownAmetysObjectException if no such object exists for the given path.
253     */
254    public <A extends AmetysObject> A resolveByPath(String absolutePath, Session session) throws AmetysRepositoryException, UnknownAmetysObjectException
255    {
256        if (getLogger().isDebugEnabled())
257        {
258            getLogger().debug("Resolving " + absolutePath);
259        }
260        
261        if (absolutePath == null)
262        {
263            throw new AmetysRepositoryException("Absolute path cannot be null");
264        }
265        
266        Node rootNode;
267        Session jcrSession = null;
268        try
269        {
270            jcrSession = session != null ? session : _repository.login();
271            rootNode = jcrSession.getRootNode().getNode(ROOT_REPO);
272        }
273        catch (PathNotFoundException e)
274        {
275            if (session == null && jcrSession != null)
276            {
277                // logout only if the session was created here
278                jcrSession.logout();
279            }
280
281            throw new AmetysRepositoryException("Unable to get ametys:root Node", e);
282        }
283        catch (RepositoryException e)
284        {
285            if (session == null && jcrSession != null)
286            {
287                // logout only if the session was created here
288                jcrSession.logout();
289            }
290
291            throw new AmetysRepositoryException("An error occured while getting ametys:root node", e);
292        }
293        
294        try
295        {
296            return this.<A>_resolve(null, rootNode, absolutePath, false);
297        }
298        catch (RepositoryException e)
299        {
300            if (session == null)
301            {
302                // logout only if the session was created here
303                jcrSession.logout();
304            }
305
306            throw new AmetysRepositoryException("An error occured while resolving " + absolutePath, e);
307        }
308    }
309
310    /**
311     * Retrieves an {@link AmetysObject} by its unique id.
312     * @param <A> the actual type of {@link AmetysObject}.
313     * @param id the identifier representing the wanted {@link AmetysObject} is the Ametys repository.
314     * @return the corresponding {@link AmetysObject}.
315     * @throws AmetysRepositoryException if an error occurs.
316     * @throws UnknownAmetysObjectException if no such object exists for the given id.
317     */
318    public <A extends AmetysObject> A resolveById(String id) throws AmetysRepositoryException, UnknownAmetysObjectException
319    {
320        if (getLogger().isDebugEnabled())
321        {
322            getLogger().debug("Resolving " + id);
323        }
324        
325        int index = id.indexOf("://");
326        if (index == -1)
327        {
328            throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id);
329        }
330        
331        String scheme = id.substring(0, index);
332        
333        AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme);
334        
335        if (factory == null)
336        {
337            throw new UnknownAmetysObjectException("There's no object for id " + id);
338        }
339        
340        return factory.getAmetysObjectById(id);
341    }
342    
343    /**
344     * <b>Expert</b>. Retrieves an {@link AmetysObject} by its unique id and the provided JCR Session.<br>
345     * It only works with id corresponding to a {@link JCRAmetysObjectFactory}.<br>
346     * This method should be uses to avoid useless Session creation.
347     * @param <A> the actual type of {@link AmetysObject}.
348     * @param id the identifier representing the wanted {@link AmetysObject} is the Ametys repository.
349     * @param session the JCR Session to use to retrieve the {@link AmetysObject}.
350     * @return the corresponding {@link AmetysObject}.
351     * @throws AmetysRepositoryException if an error occurs.
352     * @throws UnknownAmetysObjectException if no such object exists for the given id.
353     * @throws RepositoryException if a JCR error occurs.
354     */
355    public <A extends AmetysObject> A resolveById(String id, Session session) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException
356    {
357        if (getLogger().isDebugEnabled())
358        {
359            getLogger().debug("Resolving " + id);
360        }
361        
362        int index = id.indexOf("://");
363        if (index == -1)
364        {
365            throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id);
366        }
367        
368        String scheme = id.substring(0, index);
369        
370        AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme);
371        
372        if (factory == null)
373        {
374            throw new UnknownAmetysObjectException("There's no object for id " + id);
375        }
376        
377        if (!(factory instanceof JCRAmetysObjectFactory))
378        {
379            throw new IllegalArgumentException("The expert method resolveById(String, Session) should only be called for id corresponding to a JCRAmetysObjectFactory");
380        }
381        
382        return ((JCRAmetysObjectFactory<A>) factory).getAmetysObjectById(id, session);
383    }
384
385    /**
386     * Return true if the specified id correspond to an existing {@link AmetysObject}.
387     * @param id the identifier.
388     * @return true if the specified id correspond to an existing {@link AmetysObject}.
389     * @throws AmetysRepositoryException if an error occurs.
390     */
391    public boolean hasAmetysObjectForId(String id) throws AmetysRepositoryException
392    {
393        int index = id.indexOf("://");
394        if (index == -1)
395        {
396            throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id);
397        }
398        
399        String scheme = id.substring(0, index);
400        
401        AmetysObjectFactory factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme);
402        
403        if (factory == null)
404        {
405            return false;
406        }
407        
408        return factory.hasAmetysObjectForId(id);
409    }
410    
411    /**
412     * <b>Expert</b>. Returns the {@link AmetysObject} corresponding to a given JCR Node.<br>
413     * It is strictly equivalent to call <code>resolve(null, node, null, allowUnknownNode)</code>
414     * @param <A> the actual type of {@link AmetysObject}s
415     * @param node an existing node in the underlying JCR repository.
416     * @param allowUnknownNode if <code>true</code>, returns <code>null</code> if the node type
417     *                         does not correspond to a factory. If <code>false</code> and no factory
418     *                         corresponds, an {@link AmetysRepositoryException} is thrown.
419     * @return the {@link AmetysObject} corresponding to a given JCR node.
420     * @throws AmetysRepositoryException if an error occurs.
421     * @throws RepositoryException if a JCR error occurs.
422     */
423    public <A extends AmetysObject> A resolve(Node node, boolean allowUnknownNode) throws AmetysRepositoryException, RepositoryException
424    {
425        return this.<A>_resolve(null, node, null, allowUnknownNode);
426    }
427    
428    /**
429     * <b>Expert</b>. Retrieves an {@link AmetysObject}, given a JCR Node, a relative path
430     * and the parentPath in the Ametys hierarchy.<br>
431     * The path is always relative, even if it begins with a <code>'/'</code>,
432     * <code>null</code> path or empty path are equivalent.<br>
433     * May return null if ignoreUnknownNodes is true.
434     * @param <A> the actual type of {@link AmetysObject}.
435     * @param parentPath the parentPath of the returned AmetysObject, in the Ametys hierarchy.
436     * @param node the context JCR node.
437     * @param childPath the path relative to the JCR node.
438     * @param allowUnknownNode if <code>true</code>, returns <code>null</code> if the node type
439     *                         does not correspond to a factory. If <code>false</code> and no factory
440     *                         corresponds, an {@link AmetysRepositoryException} is thrown.
441     * @return the corresponding AmetysObject.
442     * @throws AmetysRepositoryException if an error occurs.
443     * @throws UnknownAmetysObjectException if no such object exists for the given path.
444     * @throws RepositoryException if a JCR error occurs.
445     */
446    public <A extends AmetysObject> A resolve(String parentPath, Node node, String childPath, boolean allowUnknownNode) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException
447    {
448        return this.<A>_resolve(parentPath, node, childPath, allowUnknownNode);
449    }
450    
451    @SuppressWarnings("unchecked")
452    private <T extends AmetysObject> T _resolve(String parentPath, Node node, String childPath, boolean allowUnknownNode) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException
453    {
454        if (getLogger().isDebugEnabled())
455        {
456            getLogger().debug("Entering _resolve with parentPath=" + parentPath + ", node=" + node.getPath() + ", childPath=" + childPath + ", ignoreUnknownNodes=" + allowUnknownNode);
457        }
458        
459        String path = childPath == null ? "" : childPath;
460        path = path.length() == 0 || path.charAt(0) != '/' ? path : path.substring(1);
461
462        if (path.length() != 0 && (Character.isSpaceChar(path.charAt(0)) || Character.isSpaceChar(path.charAt(path.length() - 1))))
463        {
464            throw new AmetysRepositoryException("Path cannot begin or end with a space character");
465        }
466        
467        String nodeType = NodeTypeHelper.getNodeTypeName(node);
468        
469        JCRAmetysObjectFactory jcrFactory = _getJCRFactory(nodeType, allowUnknownNode, parentPath, childPath);
470        
471        if (jcrFactory == null)
472        {
473            return null;
474        }
475        
476        AmetysObject rootObject = jcrFactory.getAmetysObject(node, parentPath);
477
478        if (path.length() != 0)
479        {
480            if (!(rootObject instanceof TraversableAmetysObject))
481            {
482                throw new AmetysRepositoryException("The node of type '" + nodeType + "' at path '" + node.getPath() + "' does not corresponds to a TraversableAmetysObject");
483            }
484
485            return (T) ((TraversableAmetysObject) rootObject).getChild(path);
486        }
487        else
488        {
489            return (T) rootObject;
490        }
491    }
492    
493    
494    private JCRAmetysObjectFactory _getJCRFactory(String nodeType, boolean allowUnknownNode, String parentPath, String childPath)
495    {
496        if (getLogger().isDebugEnabled())
497        {
498            getLogger().debug("Nodetype is " + nodeType);
499        }
500
501        AmetysObjectFactory<?> factory = _ametysFactoryExtensionPoint.getFactoryForNodetype(nodeType);
502        
503        if (factory == null)
504        {
505            if (allowUnknownNode)
506            {
507                if (getLogger().isDebugEnabled())
508                {
509                    getLogger().debug("No factory for nodetype " + nodeType + ". Unknown node is allowed, returning null.");
510                }
511
512                return null;
513            }
514            
515            throw new UnknownAmetysObjectException("Cannot get factory for node '" + childPath +  "' under '" + parentPath + "': There's no factory for nodetype: " + nodeType);
516        }
517
518        if (getLogger().isDebugEnabled())
519        {
520            getLogger().debug("Factory is " + factory.getClass().getName());
521        }
522
523        if (!(factory instanceof JCRAmetysObjectFactory))
524        {
525            throw new AmetysRepositoryException("A factory resolving JCR nodes must implements JCRAmetysObjectFactory");
526        }
527
528        JCRAmetysObjectFactory jcrFactory = (JCRAmetysObjectFactory) factory;
529        
530        return jcrFactory;
531    }
532    
533    /**
534     * <b>Expert</b>. Retrieves the virtual children of a concrete JCR Node.<br>
535     * @param <A> the actual type of {@link AmetysObject}s.
536     * @param parent the {@link JCRAmetysObject} "hosting" the {@link VirtualAmetysObjectFactory} reference.
537     * @return all virtual children under the given JCR Node in the Ametys hierarchy.
538     * @throws AmetysRepositoryException if an error occurs.
539     * @throws RepositoryException if a JCR error occurs.
540     */
541    public <A extends AmetysObject> AmetysObjectIterable<A> resolveVirtualChildren(JCRAmetysObject parent) throws AmetysRepositoryException, RepositoryException
542    {
543        Node contextNode = parent.getNode();
544        
545        if (getLogger().isDebugEnabled())
546        {
547            getLogger().debug("Entering resolveVirtualChildren with parent=" + parent);
548        }
549
550        if (!contextNode.hasProperty(VIRTUAL_PROPERTY))
551        {
552            return null;
553        }
554        
555        Value[] values = contextNode.getProperty(VIRTUAL_PROPERTY).getValues();
556        List<AmetysObjectIterable<A>> children = new ArrayList<>(values.length);
557        for (Value value : values)
558        {
559            String id = value.getString();
560            
561            if (getLogger().isDebugEnabled())
562            {
563                getLogger().debug("Found virtual factory id: " + id);
564            }
565
566            AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getExtension(id);
567            
568            if (factory == null)
569            {
570                throw new AmetysRepositoryException("There's no virtual factory for id " + id);
571            }
572            
573            if (getLogger().isDebugEnabled())
574            {
575                getLogger().debug("Found factory: " + factory.getClass().getName());
576            }
577
578            if (!(factory instanceof VirtualAmetysObjectFactory))
579            {
580                throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory");
581            }
582            
583            VirtualAmetysObjectFactory<A> virtualFactory = (VirtualAmetysObjectFactory<A>) factory;
584            children.add(virtualFactory.getChildren(parent));
585        }
586        
587        return new ChainedAmetysObjectIterable<>(children);
588    }
589    
590    /**
591     * <b>Expert</b>. Retrieves the virtual child of a concrete JCR Node.<br>
592     * @param parent the {@link JCRAmetysObject} "hosting" the {@link VirtualAmetysObjectFactory} reference.
593     * @param childPath  the name of the virtual child.
594     * @return a named child under the given JCR Node in the Ametys hierarchy.
595     * @throws AmetysRepositoryException if an error occurs.
596     * @throws RepositoryException if a JCR error occurs.
597     * @throws UnknownAmetysObjectException if the named child does not exist
598     */
599    public AmetysObject resolveVirtualChild(JCRAmetysObject parent, String childPath) throws AmetysRepositoryException, RepositoryException, UnknownAmetysObjectException
600    {
601        Node contextNode = parent.getNode();
602        
603        if (getLogger().isDebugEnabled())
604        {
605            getLogger().debug("Entering resolveVirtualChild with parent=" + parent);
606        }
607
608        if (!contextNode.hasProperty(VIRTUAL_PROPERTY))
609        {
610            throw new UnknownAmetysObjectException("There's no virtual child at Ametys path " + parent.getPath());
611        }
612        
613        String path = childPath == null ? "" : childPath;
614        path = path.length() == 0 || path.charAt(0) != '/' ? path : path.substring(1);
615        int index = path.indexOf('/');
616        String childName = index == -1 ? path : path.substring(0, index);
617        String subPath = index == -1 ? null : path.substring(index + 1);
618
619        if (childName.length() == 0)
620        {
621            throw new AmetysRepositoryException("A path element cannot be empty in " + childPath);
622        }
623        else if (Character.isSpaceChar(path.charAt(0)) || Character.isSpaceChar(path.charAt(path.length() - 1)))
624        {
625            throw new AmetysRepositoryException("Path element cannot begin or end with a space character: " + childName);
626        }
627        
628        Value[] values = contextNode.getProperty(VIRTUAL_PROPERTY).getValues();
629        AmetysObject object = _getVirtualChild(parent, childName, values);
630        
631        if (object == null)
632        {
633            throw new UnknownAmetysObjectException("There's no virtual object named " + childName + " at Ametys path " + parent.getPath());
634        }
635        
636        if (subPath != null)
637        {
638            if (!(object instanceof TraversableAmetysObject))
639            {
640                throw new AmetysRepositoryException("The virtual object " + childName + "at path '" + childPath + "' does not corresponds to a TraversableAmetysObject");
641            }
642
643            return ((TraversableAmetysObject) object).getChild(subPath);
644        }
645        else
646        {
647            return object;
648        }
649    }
650
651    /**
652     * Executes the given JCR XPath query and resolves results as
653     * {@link AmetysObject}s.<br>
654     * The resulting {@link AmetysObjectIterable} supports lazy loading, but
655     * will also fail lazily if one if the result nodes does not correspond to
656     * an {@link AmetysObject}.
657     * @param <A> the actual type of the results.
658     * @param jcrQuery a JCR XPath query.
659     * @return an Iterator over the resulting {@link AmetysObject}.
660     */
661    public <A extends AmetysObject> AmetysObjectIterable<A> query(String jcrQuery)
662    {
663        Session session = null;
664        try
665        {
666            session = _repository.login();
667            return query(jcrQuery, session);
668        }
669        catch (RepositoryException ex)
670        {
671            if (session != null)
672            {
673                session.logout();
674            }
675
676            throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex);
677        }
678    }
679    
680    /**
681     * <b>Expert</b>. Executes the given JCR XPath query with the provided JCR Session and resolves results as
682     * {@link AmetysObject}s.<br>
683     * The resulting {@link AmetysObjectIterable} supports lazy loading, but
684     * will also fail lazily if one if the result nodes does not correspond to
685     * an {@link AmetysObject}.
686     * @param <A> the actual type of the results.
687     * @param jcrQuery a JCR XPath query.
688     * @param session the JCR Session to use to execute the request.
689     * @return an Iterator over the resulting {@link AmetysObject}.
690     * @throws RepositoryException if a JCR error occurs.
691     */
692    @SuppressWarnings("deprecation")
693    public <A extends AmetysObject> AmetysObjectIterable<A> query(String jcrQuery, Session session) throws RepositoryException
694    {
695        if (getLogger().isDebugEnabled())
696        {
697            getLogger().debug("Executing XPath query: '" + jcrQuery + "'");
698        }
699        
700        Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH);
701
702        long t1 = System.currentTimeMillis();
703        AmetysObjectIterable<A> it = new NodeIteratorIterable<>(this, query.execute().getNodes(), null, session);
704        
705        if (getLogger().isInfoEnabled())
706        {
707            getLogger().info("JCR query '" + jcrQuery + "' executed in " + (System.currentTimeMillis() - t1) + " ms");
708        }
709        
710        return it;
711    }
712
713    private AmetysObject _getVirtualChild(JCRAmetysObject parent, String childName, Value[] values) throws RepositoryException
714    {
715        int i = 0;
716        AmetysObject object = null;
717        
718        while (object == null && i < values.length)
719        {
720            Value value = values[i];
721            String id = value.getString();
722
723            if (getLogger().isDebugEnabled())
724            {
725                getLogger().debug("Found virtual factory id: " + id);
726            }
727
728            AmetysObjectFactory factory = _ametysFactoryExtensionPoint.getExtension(id);
729            
730            if (factory == null)
731            {
732                throw new AmetysRepositoryException("There's no virtual factory for id " + id);
733            }
734            
735            if (getLogger().isDebugEnabled())
736            {
737                getLogger().debug("Found factory: " + factory.getClass().getName());
738            }
739
740            if (!(factory instanceof VirtualAmetysObjectFactory))
741            {
742                throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory: " + id);
743            }
744            
745            VirtualAmetysObjectFactory virtualFactory = (VirtualAmetysObjectFactory) factory;
746            
747            try
748            {
749                object = virtualFactory.getChild(parent, childName);
750            }
751            catch (UnknownAmetysObjectException e)
752            {
753                // Not an error
754                if (getLogger().isDebugEnabled())
755                {
756                    getLogger().debug("The factory: " + factory.getClass().getName() + " has no child named" + childName, e);
757                }
758
759                i++;
760            }
761        }
762        
763        return object;
764    }
765    
766    /**
767     * <b>Expert</b>. Creates a child object in the JCR tree and resolve it to an {@link AmetysObject}.
768     * @param <A> the actual type of {@link AmetysObject}s
769     * @param parentPath the parentPath of the new object.
770     * @param parentNode the parent JCR Node of the new object.
771     * @param childName the name of the new object.
772     * @param nodetype the type of the Node backing the new object.
773     * @return the newly created {@link AmetysObject}.
774     * @throws AmetysRepositoryException if an error occurs.
775     * @throws RepositoryIntegrityViolationException if an object with the same name already
776     *         exists and same name siblings is not allowed.
777     * @throws RepositoryException if a JCR error occurs.
778     */
779    public <A extends AmetysObject> A createAndResolve(String parentPath, Node parentNode, String childName, String nodetype) throws AmetysRepositoryException, RepositoryIntegrityViolationException, RepositoryException
780    {
781        if (getLogger().isDebugEnabled())
782        {
783            getLogger().debug("Entering createAndResolve with parentPath=" + parentPath + ", parentNode=" + parentNode.getPath() + ", childName=" + childName + ", nodetype=" + nodetype);
784        }
785
786        if (_ametysFactoryExtensionPoint.getFactoryForNodetype(nodetype) == null)
787        {
788            throw new AmetysRepositoryException("Cannot create a node '" + childName +  "' under '" + parentPath + "': There's no factory for nodetype: " + nodetype);
789        }
790        
791        try
792        {
793            Node node = parentNode.addNode(childName, nodetype);
794            NodeType[] mixinNodeTypes = node.getMixinNodeTypes();
795            boolean foundMixin = false;
796            
797            int i = 0;
798            while (!foundMixin && i < mixinNodeTypes.length)
799            {
800                if (OBJECT_TYPE.equals(mixinNodeTypes[i].getName()))
801                {
802                    foundMixin = true;
803                }
804                
805                i++;
806            }
807            
808            if (!foundMixin)
809            {
810                node.addMixin(OBJECT_TYPE);
811            }
812            
813            return this.<A>resolve(parentPath, node, null, false);
814        }
815        catch (ItemExistsException e)
816        {
817            throw new RepositoryIntegrityViolationException("The object " + childName + " already exist at path " + parentPath, e);
818        }
819        catch (RepositoryException e)
820        {
821            throw new AmetysRepositoryException("Unable to add child node for the underlying node for object at path " + parentPath, e);
822        }
823    }
824}