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