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.data.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        if (getLogger().isDebugEnabled())
240        {
241            getLogger().debug("Resolving " + absolutePath);
242        }
243        
244        if (absolutePath == null)
245        {
246            throw new AmetysRepositoryException("Absolute path cannot be null");
247        }
248        
249        Node rootNode;
250        Session session = null;
251        try
252        {
253            session = _repository.login();
254            rootNode = session.getRootNode().getNode(ROOT_REPO);
255        }
256        catch (PathNotFoundException e)
257        {
258            if (session != null)
259            {
260                session.logout();
261            }
262
263            throw new AmetysRepositoryException("Unable to get ametys:root Node", e);
264        }
265        catch (RepositoryException e)
266        {
267            if (session != null)
268            {
269                session.logout();
270            }
271
272            throw new AmetysRepositoryException("An error occured while getting ametys:root node", e);
273        }
274        
275        try
276        {
277            return this.<A>_resolve(null, rootNode, absolutePath, false);
278        }
279        catch (RepositoryException e)
280        {
281            session.logout();
282
283            throw new AmetysRepositoryException("An error occured while resolving " + absolutePath, e);
284        }
285    }
286
287    /**
288     * Retrieves an {@link AmetysObject} by its unique id.
289     * @param <A> the actual type of {@link AmetysObject}.
290     * @param id the identifier representing the wanted {@link AmetysObject} is the Ametys repository.
291     * @return the corresponding {@link AmetysObject}.
292     * @throws AmetysRepositoryException if an error occurs.
293     * @throws UnknownAmetysObjectException if no such object exists for the given id.
294     */
295    public <A extends AmetysObject> A resolveById(String id) throws AmetysRepositoryException, UnknownAmetysObjectException
296    {
297        if (getLogger().isDebugEnabled())
298        {
299            getLogger().debug("Resolving " + id);
300        }
301        
302        int index = id.indexOf("://");
303        if (index == -1)
304        {
305            throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id);
306        }
307        
308        String scheme = id.substring(0, index);
309        
310        AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme);
311        
312        if (factory == null)
313        {
314            throw new UnknownAmetysObjectException("There's no object for id " + id);
315        }
316        
317        return factory.getAmetysObjectById(id);
318    }
319    
320    /**
321     * <b>Expert</b>. Retrieves an {@link AmetysObject} by its unique id and the provided JCR Session.<br>
322     * It only works with id corresponding to a {@link JCRAmetysObjectFactory}.<br>
323     * This method should be uses to avoid useless Session creation.
324     * @param <A> the actual type of {@link AmetysObject}.
325     * @param id the identifier representing the wanted {@link AmetysObject} is the Ametys repository.
326     * @param session the JCR Session to use to retrieve the {@link AmetysObject}.
327     * @return the corresponding {@link AmetysObject}.
328     * @throws AmetysRepositoryException if an error occurs.
329     * @throws UnknownAmetysObjectException if no such object exists for the given id.
330     * @throws RepositoryException if a JCR error occurs.
331     */
332    public <A extends AmetysObject> A resolveById(String id, Session session) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException
333    {
334        if (getLogger().isDebugEnabled())
335        {
336            getLogger().debug("Resolving " + id);
337        }
338        
339        int index = id.indexOf("://");
340        if (index == -1)
341        {
342            throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id);
343        }
344        
345        String scheme = id.substring(0, index);
346        
347        AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme);
348        
349        if (factory == null)
350        {
351            throw new UnknownAmetysObjectException("There's no object for id " + id);
352        }
353        
354        if (!(factory instanceof JCRAmetysObjectFactory))
355        {
356            throw new IllegalArgumentException("The expert method resolveById(String, Session) should only be called for id corresponding to a JCRAmetysObjectFactory");
357        }
358        
359        return ((JCRAmetysObjectFactory<A>) factory).getAmetysObjectById(id, session);
360    }
361
362    /**
363     * Return true if the specified id correspond to an existing {@link AmetysObject}.
364     * @param id the identifier.
365     * @return true if the specified id correspond to an existing {@link AmetysObject}.
366     * @throws AmetysRepositoryException if an error occurs.
367     */
368    public boolean hasAmetysObjectForId(String id) throws AmetysRepositoryException
369    {
370        int index = id.indexOf("://");
371        if (index == -1)
372        {
373            throw new AmetysRepositoryException("An object id must conform to the <protocol>://<protocol-specific-part> syntax: " + id);
374        }
375        
376        String scheme = id.substring(0, index);
377        
378        AmetysObjectFactory factory = _ametysFactoryExtensionPoint.getFactoryForScheme(scheme);
379        
380        if (factory == null)
381        {
382            return false;
383        }
384        
385        return factory.hasAmetysObjectForId(id);
386    }
387    
388    /**
389     * <b>Expert</b>. Returns the {@link AmetysObject} corresponding to a given JCR Node.<br>
390     * It is strictly equivalent to call <code>resolve(null, node, null, allowUnknownNode)</code>
391     * @param <A> the actual type of {@link AmetysObject}s
392     * @param node an existing node in the underlying JCR repository.
393     * @param allowUnknownNode if <code>true</code>, returns <code>null</code> if the node type
394     *                         does not correspond to a factory. If <code>false</code> and no factory
395     *                         corresponds, an {@link AmetysRepositoryException} is thrown.
396     * @return the {@link AmetysObject} corresponding to a given JCR node.
397     * @throws AmetysRepositoryException if an error occurs.
398     * @throws RepositoryException if a JCR error occurs.
399     */
400    public <A extends AmetysObject> A resolve(Node node, boolean allowUnknownNode) throws AmetysRepositoryException, RepositoryException
401    {
402        return this.<A>_resolve(null, node, null, allowUnknownNode);
403    }
404    
405    /**
406     * <b>Expert</b>. Retrieves an {@link AmetysObject}, given a JCR Node, a relative path
407     * and the parentPath in the Ametys hierarchy.<br>
408     * The path is always relative, even if it begins with a <code>'/'</code>,
409     * <code>null</code> path or empty path are equivalent.<br>
410     * May return null if ignoreUnknownNodes is true.
411     * @param <A> the actual type of {@link AmetysObject}.
412     * @param parentPath the parentPath of the returned AmetysObject, in the Ametys hierarchy.
413     * @param node the context JCR node.
414     * @param childPath the path relative to the JCR node.
415     * @param allowUnknownNode if <code>true</code>, returns <code>null</code> if the node type
416     *                         does not correspond to a factory. If <code>false</code> and no factory
417     *                         corresponds, an {@link AmetysRepositoryException} is thrown.
418     * @return the corresponding AmetysObject.
419     * @throws AmetysRepositoryException if an error occurs.
420     * @throws UnknownAmetysObjectException if no such object exists for the given path.
421     * @throws RepositoryException if a JCR error occurs.
422     */
423    public <A extends AmetysObject> A resolve(String parentPath, Node node, String childPath, boolean allowUnknownNode) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException
424    {
425        return this.<A>_resolve(parentPath, node, childPath, allowUnknownNode);
426    }
427    
428    @SuppressWarnings("unchecked")
429    private <T extends AmetysObject> T _resolve(String parentPath, Node node, String childPath, boolean allowUnknownNode) throws AmetysRepositoryException, UnknownAmetysObjectException, RepositoryException
430    {
431        if (getLogger().isDebugEnabled())
432        {
433            getLogger().debug("Entering _resolve with parentPath=" + parentPath + ", node=" + node.getPath() + ", childPath=" + childPath + ", ignoreUnknownNodes=" + allowUnknownNode);
434        }
435        
436        String path = childPath == null ? "" : childPath;
437        path = path.length() == 0 || path.charAt(0) != '/' ? path : path.substring(1);
438
439        if (path.length() != 0 && (Character.isSpaceChar(path.charAt(0)) || Character.isSpaceChar(path.charAt(path.length() - 1))))
440        {
441            throw new AmetysRepositoryException("Path cannot begin or end with a space character");
442        }
443        
444        String nodeType = NodeTypeHelper.getNodeTypeName(node);
445        
446        JCRAmetysObjectFactory jcrFactory = _getJCRFactory(nodeType, allowUnknownNode, parentPath, childPath);
447        
448        if (jcrFactory == null)
449        {
450            return null;
451        }
452        
453        AmetysObject rootObject = jcrFactory.getAmetysObject(node, parentPath);
454
455        if (path.length() != 0)
456        {
457            if (!(rootObject instanceof TraversableAmetysObject))
458            {
459                throw new AmetysRepositoryException("The node of type '" + nodeType + "' at path '" + node.getPath() + "' does not corresponds to a TraversableAmetysObject");
460            }
461
462            return (T) ((TraversableAmetysObject) rootObject).getChild(path);
463        }
464        else
465        {
466            return (T) rootObject;
467        }
468    }
469    
470    
471    private JCRAmetysObjectFactory _getJCRFactory(String nodeType, boolean allowUnknownNode, String parentPath, String childPath)
472    {
473        if (getLogger().isDebugEnabled())
474        {
475            getLogger().debug("Nodetype is " + nodeType);
476        }
477
478        AmetysObjectFactory<?> factory = _ametysFactoryExtensionPoint.getFactoryForNodetype(nodeType);
479        
480        if (factory == null)
481        {
482            if (allowUnknownNode)
483            {
484                if (getLogger().isDebugEnabled())
485                {
486                    getLogger().debug("No factory for nodetype " + nodeType + ". Unknown node is allowed, returning null.");
487                }
488
489                return null;
490            }
491            
492            throw new UnknownAmetysObjectException("Cannot get factory for node '" + childPath +  "' under '" + parentPath + "': There's no factory for nodetype: " + nodeType);
493        }
494
495        if (getLogger().isDebugEnabled())
496        {
497            getLogger().debug("Factory is " + factory.getClass().getName());
498        }
499
500        if (!(factory instanceof JCRAmetysObjectFactory))
501        {
502            throw new AmetysRepositoryException("A factory resolving JCR nodes must implements JCRAmetysObjectFactory");
503        }
504
505        JCRAmetysObjectFactory jcrFactory = (JCRAmetysObjectFactory) factory;
506        
507        return jcrFactory;
508    }
509    
510    /**
511     * <b>Expert</b>. Retrieves the virtual children of a concrete JCR Node.<br>
512     * @param <A> the actual type of {@link AmetysObject}s.
513     * @param parent the {@link JCRAmetysObject} "hosting" the {@link VirtualAmetysObjectFactory} reference.
514     * @return all virtual children under the given JCR Node in the Ametys hierarchy.
515     * @throws AmetysRepositoryException if an error occurs.
516     * @throws RepositoryException if a JCR error occurs.
517     */
518    public <A extends AmetysObject> AmetysObjectIterable<A> resolveVirtualChildren(JCRAmetysObject parent) throws AmetysRepositoryException, RepositoryException
519    {
520        Node contextNode = parent.getNode();
521        
522        if (getLogger().isDebugEnabled())
523        {
524            getLogger().debug("Entering resolveVirtualChildren with parent=" + parent);
525        }
526
527        if (!contextNode.hasProperty(VIRTUAL_PROPERTY))
528        {
529            return null;
530        }
531        
532        Value[] values = contextNode.getProperty(VIRTUAL_PROPERTY).getValues();
533        List<AmetysObjectIterable<A>> children = new ArrayList<>(values.length);
534        for (Value value : values)
535        {
536            String id = value.getString();
537            
538            if (getLogger().isDebugEnabled())
539            {
540                getLogger().debug("Found virtual factory id: " + id);
541            }
542
543            AmetysObjectFactory<A> factory = _ametysFactoryExtensionPoint.getExtension(id);
544            
545            if (factory == null)
546            {
547                throw new AmetysRepositoryException("There's no virtual factory for id " + id);
548            }
549            
550            if (getLogger().isDebugEnabled())
551            {
552                getLogger().debug("Found factory: " + factory.getClass().getName());
553            }
554
555            if (!(factory instanceof VirtualAmetysObjectFactory))
556            {
557                throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory");
558            }
559            
560            VirtualAmetysObjectFactory<A> virtualFactory = (VirtualAmetysObjectFactory<A>) factory;
561            children.add(virtualFactory.getChildren(parent));
562        }
563        
564        return new ChainedAmetysObjectIterable<>(children);
565    }
566    
567    /**
568     * <b>Expert</b>. Retrieves the virtual child of a concrete JCR Node.<br>
569     * @param parent the {@link JCRAmetysObject} "hosting" the {@link VirtualAmetysObjectFactory} reference.
570     * @param childPath  the name of the virtual child.
571     * @return a named child under the given JCR Node in the Ametys hierarchy.
572     * @throws AmetysRepositoryException if an error occurs.
573     * @throws RepositoryException if a JCR error occurs.
574     * @throws UnknownAmetysObjectException if the named child does not exist
575     */
576    public AmetysObject resolveVirtualChild(JCRAmetysObject parent, String childPath) throws AmetysRepositoryException, RepositoryException, UnknownAmetysObjectException
577    {
578        Node contextNode = parent.getNode();
579        
580        if (getLogger().isDebugEnabled())
581        {
582            getLogger().debug("Entering resolveVirtualChild with parent=" + parent);
583        }
584
585        if (!contextNode.hasProperty(VIRTUAL_PROPERTY))
586        {
587            throw new UnknownAmetysObjectException("There's no virtual child at Ametys path " + parent.getPath());
588        }
589        
590        String path = childPath == null ? "" : childPath;
591        path = path.length() == 0 || path.charAt(0) != '/' ? path : path.substring(1);
592        int index = path.indexOf('/');
593        String childName = index == -1 ? path : path.substring(0, index);
594        String subPath = index == -1 ? null : path.substring(index + 1);
595
596        if (childName.length() == 0)
597        {
598            throw new AmetysRepositoryException("A path element cannot be empty in " + childPath);
599        }
600        else if (Character.isSpaceChar(path.charAt(0)) || Character.isSpaceChar(path.charAt(path.length() - 1)))
601        {
602            throw new AmetysRepositoryException("Path element cannot begin or end with a space character: " + childName);
603        }
604        
605        Value[] values = contextNode.getProperty(VIRTUAL_PROPERTY).getValues();
606        AmetysObject object = _getVirtualChild(parent, childName, values);
607        
608        if (object == null)
609        {
610            throw new UnknownAmetysObjectException("There's no virtual object named " + childName + " at Ametys path " + parent.getPath());
611        }
612        
613        if (subPath != null)
614        {
615            if (!(object instanceof TraversableAmetysObject))
616            {
617                throw new AmetysRepositoryException("The virtual object " + childName + "at path '" + childPath + "' does not corresponds to a TraversableAmetysObject");
618            }
619
620            return ((TraversableAmetysObject) object).getChild(subPath);
621        }
622        else
623        {
624            return object;
625        }
626    }
627
628    /**
629     * Executes the given JCR XPath query and resolves results as
630     * {@link AmetysObject}s.<br>
631     * The resulting {@link AmetysObjectIterable} supports lazy loading, but
632     * will also fail lazily if one if the result nodes does not correspond to
633     * an {@link AmetysObject}.
634     * @param <A> the actual type of the results.
635     * @param jcrQuery a JCR XPath query.
636     * @return an Iterator over the resulting {@link AmetysObject}.
637     */
638    public <A extends AmetysObject> AmetysObjectIterable<A> query(String jcrQuery)
639    {
640        Session session = null;
641        try
642        {
643            session = _repository.login();
644            return query(jcrQuery, session);
645        }
646        catch (RepositoryException ex)
647        {
648            if (session != null)
649            {
650                session.logout();
651            }
652
653            throw new AmetysRepositoryException("An error occured executing the JCR query : " + jcrQuery, ex);
654        }
655    }
656    
657    /**
658     * <b>Expert</b>. Executes the given JCR XPath query with the provided JCR Session and resolves results as
659     * {@link AmetysObject}s.<br>
660     * The resulting {@link AmetysObjectIterable} supports lazy loading, but
661     * will also fail lazily if one if the result nodes does not correspond to
662     * an {@link AmetysObject}.
663     * @param <A> the actual type of the results.
664     * @param jcrQuery a JCR XPath query.
665     * @param session the JCR Session to use to execute the request.
666     * @return an Iterator over the resulting {@link AmetysObject}.
667     * @throws RepositoryException if a JCR error occurs.
668     */
669    @SuppressWarnings("deprecation")
670    public <A extends AmetysObject> AmetysObjectIterable<A> query(String jcrQuery, Session session) throws RepositoryException
671    {
672        if (getLogger().isDebugEnabled())
673        {
674            getLogger().debug("Executing XPath query: '" + jcrQuery + "'");
675        }
676        
677        Query query = session.getWorkspace().getQueryManager().createQuery(jcrQuery, Query.XPATH);
678
679        long t1 = System.currentTimeMillis();
680        AmetysObjectIterable<A> it = new NodeIteratorIterable<>(this, query.execute().getNodes(), null, session);
681        
682        if (getLogger().isInfoEnabled())
683        {
684            getLogger().info("JCR query '" + jcrQuery + "' executed in " + (System.currentTimeMillis() - t1) + " ms");
685        }
686        
687        return it;
688    }
689
690    private AmetysObject _getVirtualChild(JCRAmetysObject parent, String childName, Value[] values) throws RepositoryException
691    {
692        int i = 0;
693        AmetysObject object = null;
694        
695        while (object == null && i < values.length)
696        {
697            Value value = values[i];
698            String id = value.getString();
699
700            if (getLogger().isDebugEnabled())
701            {
702                getLogger().debug("Found virtual factory id: " + id);
703            }
704
705            AmetysObjectFactory factory = _ametysFactoryExtensionPoint.getExtension(id);
706            
707            if (factory == null)
708            {
709                throw new AmetysRepositoryException("There's no virtual factory for id " + id);
710            }
711            
712            if (getLogger().isDebugEnabled())
713            {
714                getLogger().debug("Found factory: " + factory.getClass().getName());
715            }
716
717            if (!(factory instanceof VirtualAmetysObjectFactory))
718            {
719                throw new AmetysRepositoryException("A factory handling virtual objects must implement VirtualAmetysObjectFactory: " + id);
720            }
721            
722            VirtualAmetysObjectFactory virtualFactory = (VirtualAmetysObjectFactory) factory;
723            
724            try
725            {
726                object = virtualFactory.getChild(parent, childName);
727            }
728            catch (UnknownAmetysObjectException e)
729            {
730                // Not an error
731                if (getLogger().isDebugEnabled())
732                {
733                    getLogger().debug("The factory: " + factory.getClass().getName() + " has no child named" + childName, e);
734                }
735
736                i++;
737            }
738        }
739        
740        return object;
741    }
742    
743    /**
744     * <b>Expert</b>. Creates a child object in the JCR tree and resolve it to an {@link AmetysObject}.
745     * @param <A> the actual type of {@link AmetysObject}s
746     * @param parentPath the parentPath of the new object.
747     * @param parentNode the parent JCR Node of the new object.
748     * @param childName the name of the new object.
749     * @param nodetype the type of the Node backing the new object.
750     * @return the newly created {@link AmetysObject}.
751     * @throws AmetysRepositoryException if an error occurs.
752     * @throws RepositoryIntegrityViolationException if an object with the same name already
753     *         exists and same name siblings is not allowed.
754     * @throws RepositoryException if a JCR error occurs.
755     */
756    public <A extends AmetysObject> A createAndResolve(String parentPath, Node parentNode, String childName, String nodetype) throws AmetysRepositoryException, RepositoryIntegrityViolationException, RepositoryException
757    {
758        if (getLogger().isDebugEnabled())
759        {
760            getLogger().debug("Entering createAndResolve with parentPath=" + parentPath + ", parentNode=" + parentNode.getPath() + ", childName=" + childName + ", nodetype=" + nodetype);
761        }
762
763        if (_ametysFactoryExtensionPoint.getFactoryForNodetype(nodetype) == null)
764        {
765            throw new AmetysRepositoryException("Cannot create a node '" + childName +  "' under '" + parentPath + "': There's no factory for nodetype: " + nodetype);
766        }
767        
768        try
769        {
770            Node node = parentNode.addNode(childName, nodetype);
771            NodeType[] mixinNodeTypes = node.getMixinNodeTypes();
772            boolean foundMixin = false;
773            
774            int i = 0;
775            while (!foundMixin && i < mixinNodeTypes.length)
776            {
777                if (OBJECT_TYPE.equals(mixinNodeTypes[i].getName()))
778                {
779                    foundMixin = true;
780                }
781                
782                i++;
783            }
784            
785            if (!foundMixin)
786            {
787                node.addMixin(OBJECT_TYPE);
788            }
789            
790            return this.<A>resolve(parentPath, node, null, false);
791        }
792        catch (ItemExistsException e)
793        {
794            throw new RepositoryIntegrityViolationException("The object " + childName + " already exist at path " + parentPath, e);
795        }
796        catch (RepositoryException e)
797        {
798            throw new AmetysRepositoryException("Unable to add child node for the underlying node for object at path " + parentPath, e);
799        }
800    }
801}