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