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