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.collection;
018
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.List;
023
024import javax.jcr.Node;
025import javax.jcr.NodeIterator;
026import javax.jcr.RepositoryException;
027import javax.jcr.Session;
028
029import org.apache.avalon.framework.configuration.Configuration;
030import org.apache.avalon.framework.configuration.ConfigurationException;
031import org.apache.cocoon.util.HashUtil;
032import org.apache.commons.lang.StringUtils;
033
034import org.ametys.plugins.repository.AmetysObject;
035import org.ametys.plugins.repository.AmetysObjectIterable;
036import org.ametys.plugins.repository.AmetysRepositoryException;
037import org.ametys.plugins.repository.ChainedAmetysObjectIterable;
038import org.ametys.plugins.repository.NodeIteratorIterable;
039import org.ametys.plugins.repository.jcr.SimpleAmetysObjectFactory;
040
041/**
042 * Factory for {@link AmetysObjectCollection}.
043 */
044public class AmetysObjectCollectionFactory extends SimpleAmetysObjectFactory
045{
046    /** JCR nodetype for the collection itself */
047    public static final String COLLECTION_NODETYPE = "ametys:collection";
048    
049    /** JCR nodetype for collection elements (ie. nodes "between" the collection and contained contents) */
050    public static final String COLLECTION_ELEMENT_NODETYPE = "ametys:collectionElement";
051    
052    @Override
053    public void configure(Configuration configuration) throws ConfigurationException
054    {
055        // does nothing, scheme and nodetypes are not obtained through configuration
056    }
057
058    @Override
059    public String getScheme()
060    {
061        return "collection";
062    }
063    
064    @Override
065    public Collection<String> getNodetypes()
066    {
067        ArrayList<String> nodetypes = new ArrayList<>();
068        
069        nodetypes.add(COLLECTION_NODETYPE);
070        nodetypes.add(COLLECTION_ELEMENT_NODETYPE);
071        
072        return Collections.unmodifiableCollection(nodetypes);
073    }
074    
075    @Override
076    @SuppressWarnings("unchecked")
077    public AmetysObjectCollection getAmetysObject(Node node, String parentPath) throws AmetysRepositoryException, RepositoryException
078    {
079        String nodeType = node.getPrimaryNodeType().getName();
080        
081        if (nodeType.equals(COLLECTION_NODETYPE))
082        {
083            // c'est directement une collection
084            return new AmetysObjectCollection(node, parentPath, this);
085        }
086        
087        // sinon, c'est un élément de collection, on remonte jusqu'à la collection
088        Node contextNode = node;
089        while (!COLLECTION_NODETYPE.equals(contextNode.getPrimaryNodeType().getName()))
090        {
091            contextNode = contextNode.getParent();
092        }
093        
094        return new AmetysObjectCollection(contextNode, parentPath, this);
095    }
096    
097    /**
098     * Computes a hashed path in the JCR tree from the name of the child object.<br>
099     * Subclasses may override this method to provide a more suitable hash function.<br>
100     * This implementation relies on the buzhash algorithm.
101     * This method MUST return an array of the same length for each name.
102     * @param name the name of the child object
103     * @return a hashed path of the name.
104     */
105    protected String[] getHashedPath(String name)
106    {
107        long hash = Math.abs(HashUtil.hash(name));
108        String hashStr = Long.toString(hash, 16);
109        hashStr = StringUtils.leftPad(hashStr, 4, '0');
110        
111        return new String[]{hashStr.substring(0, 2), hashStr.substring(2, 4)};
112    }
113    
114    /**
115     * Returns the parent of the given {@link AmetysObjectCollection} 
116     * @param object an {@link AmetysObjectCollection}
117     * @return the parent of the given {@link AmetysObjectCollection} 
118     * @throws AmetysRepositoryException if an error occurs
119     */
120    public AmetysObject getParent(AmetysObjectCollection object) throws AmetysRepositoryException
121    {
122        Node node = object.getNode();
123        try
124        {
125            Node parentNode = node.getParent();
126            return _resolver.resolve(parentNode, false);
127        }
128        catch (RepositoryException ex)
129        {
130            throw new AmetysRepositoryException("An error occured during resolving parent object of object " + object.getName(), ex);
131        }
132    }
133
134    /**
135     * Returns a single {@link AmetysObject} given its path and JCR Node.<br>
136     * This method should never been called by clients.
137     * @param <A> the type of the composite {@link AmetysObject}.
138     * @param parentPath the path of the collection
139     * @param node the node of the child
140     * @param subPath the subpath in the Ametys hierarchy
141     * @return an {@link AmetysObject}
142     * @throws AmetysRepositoryException if an error occurs.
143     */
144    @SuppressWarnings("unchecked")
145    public <A extends AmetysObject> A getObject(String parentPath, Node node, String subPath) throws AmetysRepositoryException
146    {
147        try
148        {
149            return (A) _resolver.resolve(parentPath, node, subPath, false);
150        }
151        catch (RepositoryException e)
152        {
153            throw new AmetysRepositoryException("An error occured while resolving Node", e);
154        }
155    }
156    
157    /**
158     * Creates a child object in the collection and resolve it to an {@link AmetysObject}.
159     * @param <A> the type of the composite {@link AmetysObject}.
160     * @param parentPath the parentPath of the new object.
161     * @param parentNode the parent JCR Node of the new object, corresponding to a collection element.
162     * @param name the name of the new object.
163     * @param type the type of the Node backing the new object.
164     * @return the newly created {@link AmetysObject}.
165     * @throws AmetysRepositoryException if an error occurs.
166     */
167    @SuppressWarnings("unchecked")
168    public <A extends AmetysObject> A createChild(String parentPath, Node parentNode, String name, String type) throws AmetysRepositoryException
169    {
170        try
171        {
172            return (A) _resolver.createAndResolve(parentPath, parentNode, name, type);
173        }
174        catch (RepositoryException e)
175        {
176            throw new AmetysRepositoryException("An error occured while creating Node", e);
177        }
178    }
179    
180    /**
181     * Returns the {@link AmetysObject}s children of the JCR Node backing an {@link AmetysObjectCollection}.<br>
182     * This method should never been called by clients.
183     * @param parentPath the parent path in the Ametys hierarchy of all {@link AmetysObject} being returned.
184     * @param collectionNode the JCR Node backing the {@link AmetysObjectCollection}.
185     * @return the {@link AmetysObject}s children.
186     */
187    @SuppressWarnings("unchecked")
188    public AmetysObjectIterable getChildren(String parentPath, Node collectionNode)
189    {
190        List<AmetysObjectIterable> iterators = new ArrayList<>();
191            
192        try
193        {
194            NodeIterator it = collectionNode.getNodes();
195            _addFirstLevelChildren(parentPath, iterators, it, collectionNode.getSession());
196        }
197        catch (RepositoryException ex)
198        {
199            throw new AmetysRepositoryException("An error occured while iterating inside the collection", ex);
200        }
201        
202        return new ChainedAmetysObjectIterable(iterators);
203    }
204    
205    private void _addFirstLevelChildren(String parentPath, List<AmetysObjectIterable> iterators, NodeIterator it, Session session) throws RepositoryException
206    {
207        // the collection itself could have some other child nodes which should be ignored here
208        while (it.hasNext())
209        {
210            Node nextChild = it.nextNode();
211            if (nextChild.getPrimaryNodeType().getName().equals(AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE))
212            {
213                _addNextLevelChildren(parentPath, iterators, nextChild.getNodes(), session);
214            }
215        }
216    }
217    
218    private void _addNextLevelChildren(String parentPath, List<AmetysObjectIterable> iterators, NodeIterator it, Session session) throws RepositoryException
219    {
220        if (it.hasNext())
221        {
222            Node firstNode = it.nextNode();
223            if (firstNode.getPrimaryNodeType().getName().equals(AmetysObjectCollectionFactory.COLLECTION_ELEMENT_NODETYPE))
224            {
225                NodeIterator firstChildIt = firstNode.getNodes();
226                _addNextLevelChildren(parentPath, iterators, firstChildIt, session);
227                
228                while (it.hasNext())
229                {
230                    NodeIterator childIt = it.nextNode().getNodes();
231                    _addNextLevelChildren(parentPath, iterators, childIt, session);
232                }
233            }
234            else
235            {
236                iterators.add(new NodeIteratorIterable(_resolver, new WrapperNodeIterator(firstNode, it), parentPath, session));
237            }
238        }
239    }
240    
241    private class WrapperNodeIterator implements NodeIterator
242    {
243        private Node _firstElement;
244        private NodeIterator _it;
245        
246        private boolean _firstElementUsed;
247        
248        public WrapperNodeIterator(Node firstElement, NodeIterator it)
249        {
250            _firstElement = firstElement;
251            _it = it;
252            _firstElementUsed = false;
253        }
254        
255        public Node nextNode()
256        {
257            if (!_firstElementUsed)
258            {
259                _firstElementUsed = true;
260                return _firstElement;
261            }
262            
263            return _it.nextNode();
264        }
265
266        public long getPosition()
267        {
268            if (!_firstElementUsed)
269            {
270                return 0;
271            }
272            
273            return _it.getPosition();
274        }
275
276        public long getSize()
277        {
278            return _it.getSize();
279        }
280
281        public void skip(long skipNum)
282        {
283            if (skipNum < 0) 
284            {
285                throw new IllegalArgumentException("skipNum must not be negative");
286            }
287
288            if (skipNum == 0)
289            {
290                return;
291            }
292            
293            if (!_firstElementUsed)
294            {
295                _firstElementUsed = true;
296                if (skipNum > 1)
297                {
298                    _it.skip(skipNum - 1);
299                }
300            }
301            else
302            {
303                _it.skip(skipNum);
304            }
305        }
306
307        public boolean hasNext()
308        {
309            if (!_firstElementUsed)
310            {
311                return true;
312            }
313            
314            return _it.hasNext();
315        }
316
317        public Object next()
318        {
319            return nextNode();
320        }
321
322        public void remove()
323        {
324            throw new UnsupportedOperationException("remove is unsupported");
325        }        
326    }
327}