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 */
016package org.ametys.workspaces.repository.jcr;
017
018import java.io.IOException;
019import java.util.ArrayList;
020import java.util.Collection;
021import java.util.Collections;
022import java.util.Comparator;
023import java.util.List;
024import java.util.Map;
025import java.util.TreeMap;
026
027import javax.jcr.Node;
028import javax.jcr.NodeIterator;
029import javax.jcr.Property;
030import javax.jcr.PropertyIterator;
031import javax.jcr.PropertyType;
032import javax.jcr.RepositoryException;
033import javax.jcr.Session;
034import javax.jcr.Value;
035
036import org.apache.cocoon.ProcessingException;
037import org.apache.cocoon.environment.ObjectModelHelper;
038import org.apache.cocoon.environment.Request;
039import org.apache.cocoon.xml.AttributesImpl;
040import org.apache.cocoon.xml.XMLUtils;
041import org.apache.commons.lang.StringUtils;
042import org.xml.sax.Attributes;
043import org.xml.sax.SAXException;
044
045import org.ametys.core.util.DateUtils;
046import org.ametys.runtime.config.Config;
047
048/**
049 * Generate the content of a node:
050 * <ul>
051 *  <li>properties
052 *  <li>children
053 *  <li>referers
054 * </ul>
055 */
056public class RepositoryNodeGenerator extends AbstractRepositoryGenerator
057{
058    private static final Comparator<Node> __ALPHA_NODE_COMPARATOR = new NodeNameComparator();
059    
060    private static final Comparator<Node> __REVERSE_ALPHA_NODE_COMPARATOR = new NodeNameComparator(false);
061    
062    public void generate() throws IOException, SAXException, ProcessingException
063    {
064        Request request = ObjectModelHelper.getRequest(objectModel);
065        
066        String workspaceName = parameters.getParameter("workspace", "default");
067        // Can be "document", "alpha" or "reverseAlpha".
068        String defaultOrder = Config.getInstance() != null ? Config.getInstance().getValue("repository.default.sort") : "document";
069        String order = parameters.getParameter("order", defaultOrder);
070        if (StringUtils.isBlank(order))
071        {
072            order = defaultOrder;
073        }
074            
075        _getRepository(request, workspaceName);
076
077        if (getLogger().isDebugEnabled())
078        {
079            getLogger().debug("Trying to generate content for a node");
080        }
081        
082        contentHandler.startDocument();
083        
084        
085        if (workspaceName != null)
086        {
087            AttributesImpl repositoryAttributes = new AttributesImpl();
088            repositoryAttributes.addCDATAAttribute("workspace", workspaceName);
089            XMLUtils.startElement(contentHandler, "repository", repositoryAttributes);
090        }
091        else
092        {
093            XMLUtils.startElement(contentHandler, "repository");
094        }
095
096        try
097        {
098            _saxNode(_session, order);
099        }
100        catch (RepositoryException e)
101        {
102            throw new ProcessingException("Unable to retrieve node for path: '" + source + "'", e);
103        }
104        
105        XMLUtils.endElement(contentHandler, "repository");
106        
107        contentHandler.endDocument();
108    }
109
110    private void _saxNode(Session session, String order) throws SAXException, RepositoryException
111    {
112        Node node = null;
113        Node rootNode = session.getRootNode();
114        String path = parameters.getParameter("path", "");
115        long start = parameters.getParameterAsLong("start", -1);
116        long end = parameters.getParameterAsLong("end", -1);
117        
118        if (StringUtils.isEmpty(path))
119        {
120            node = rootNode;
121        }
122        else
123        {
124            node = rootNode.getNode(path);
125        }
126        
127        AttributesImpl nodeAttributes = new AttributesImpl();
128        
129        if (rootNode != node)
130        {
131            nodeAttributes.addCDATAAttribute("parentPath", node.getParent().getPath());
132        }
133        else
134        {
135            // Parent path is empty if this is the JCR root node
136            nodeAttributes.addCDATAAttribute("parentPath", "");
137        }
138        
139        if (start > -1 && end > -1)
140        {
141            nodeAttributes.addCDATAAttribute("start", Long.toString(start));
142            nodeAttributes.addCDATAAttribute("end", Long.toString(end));
143        }
144        
145        boolean hasOrderableChildNodes = node.getPrimaryNodeType().hasOrderableChildNodes();
146        
147        nodeAttributes.addCDATAAttribute("name", node.getName());
148        nodeAttributes.addCDATAAttribute("path", node.getPath());
149        nodeAttributes.addCDATAAttribute("pathWithGroups", NodeGroupHelper.getPathWithGroups(node));
150        nodeAttributes.addCDATAAttribute("index", String.valueOf(node.getIndex()));
151        nodeAttributes.addCDATAAttribute("hasOrderableChildNodes", Boolean.toString(hasOrderableChildNodes));
152        nodeAttributes.addCDATAAttribute("isNew", Boolean.toString(node.isNew()));
153        nodeAttributes.addCDATAAttribute("isModified", Boolean.toString(node.isModified()));
154        nodeAttributes.addCDATAAttribute("noChild", Boolean.toString(!node.hasNodes()));
155        nodeAttributes.addCDATAAttribute("type", "node");
156        
157//        String realOrder = hasOrderableChildNodes ? "document" : order;
158        
159        XMLUtils.startElement(contentHandler, "node", nodeAttributes);
160        
161        // Properties
162        _saxProperties(node);
163
164        // Children
165        _saxChildren(node, start, end, order);
166        
167        // Referers
168        _saxReferers(node);
169        
170        XMLUtils.endElement(contentHandler, "node");
171    }
172
173    private void _saxProperties(Node node) throws SAXException, RepositoryException
174    {
175        Map<String, Property> jcrProperties = new TreeMap<>();
176        Map<String, Property> otherProperties = new TreeMap<>();
177        
178        PropertyIterator itProperty = node.getProperties();
179        while (itProperty.hasNext())
180        {
181            Property property = itProperty.nextProperty();
182            String propertyName = property.getName();
183            
184            if (propertyName.startsWith("jcr:"))
185            {
186                jcrProperties.put(propertyName, property);
187            }
188            else
189            {
190                otherProperties.put(propertyName, property);
191            }
192        }
193        
194        for (Property property : jcrProperties.values())
195        {
196            _saxProperty(property, node);
197        }
198        
199        for (Property property : otherProperties.values())
200        {
201            _saxProperty(property, node);
202        }
203        
204        XMLUtils.createElement(contentHandler, "isLocked", String.valueOf(node.isLocked()));
205        XMLUtils.createElement(contentHandler, "isCheckedOut", String.valueOf(node.isCheckedOut()));
206        XMLUtils.createElement(contentHandler, "isNew", Boolean.toString(node.isNew()));
207    }
208    
209    private void _saxProperty(Property property, Node parentNode) throws SAXException, RepositoryException
210    {
211        boolean isMultiple = property.getDefinition().isMultiple();
212        AttributesImpl propertyAttributes = new AttributesImpl();
213        boolean editable = !property.getDefinition().isProtected() && !parentNode.isLocked();
214
215        propertyAttributes.addCDATAAttribute("name", property.getName());
216        propertyAttributes.addCDATAAttribute("multiple", String.valueOf(isMultiple));
217        propertyAttributes.addCDATAAttribute("type", PropertyType.nameFromValue(property.getType()));
218        propertyAttributes.addCDATAAttribute("editable", String.valueOf(editable));
219        propertyAttributes.addCDATAAttribute("new", Boolean.toString(property.isNew()));
220        propertyAttributes.addCDATAAttribute("modified", Boolean.toString(property.isModified()));
221        
222        XMLUtils.startElement(contentHandler, "property", propertyAttributes);
223        
224        if (isMultiple)
225        {
226            Value[] values = property.getValues();
227            
228            for (int i = 0; i < values.length; i++)
229            {
230                _saxValue(values[i]);
231            }
232        }
233        else
234        {
235            _saxValue(property.getValue());
236        }
237        
238        XMLUtils.endElement(contentHandler, "property");
239    }
240    
241    private void _saxChildren(Node node, long start, long end, String order) throws SAXException, RepositoryException
242    {
243        NodeIterator itNode = node.getNodes();
244        
245        long startIndex = Math.max(0, start);
246        long endIndex = end >= 0 ? end : (itNode.getSize() - 1);
247        
248        long childCount = endIndex - startIndex + 1;
249        
250        if (childCount > NodeGroupHelper.MAX_CHILDREN_PER_NODE)
251        {
252            _saxGroups(node, startIndex, childCount);
253        }
254        else
255        {
256            if ("alpha".equals(order) || "reverseAlpha".equals(order))
257            {
258                Collection<Node> children = _extractNodes(itNode, startIndex, endIndex, "alpha".equals(order));
259                for (Node child : children)
260                {
261                    _saxSubnode(child);
262                }
263            }
264            else
265            {
266                itNode.skip(startIndex);
267                for (long i = startIndex; i <= endIndex && itNode.hasNext(); i++)
268                {
269                    _saxSubnode(itNode.nextNode());
270                }
271            }
272        }
273    }
274    
275    private Collection<Node> _extractNodes(NodeIterator itNodes, long start, long end, boolean ascending)
276    {
277        List<Node> nodes = new ArrayList<>((int) itNodes.getSize());
278        
279        // Fill in the list with all elements.
280        while (itNodes.hasNext())
281        {
282            nodes.add(itNodes.nextNode());
283        }
284        
285        // Sort the collection.
286        Collections.sort(nodes, ascending ? __ALPHA_NODE_COMPARATOR : __REVERSE_ALPHA_NODE_COMPARATOR);
287        
288        return nodes.subList((int) start, (int) end + 1);
289    }
290    
291    /**
292     * Generate node groups.
293     * @param node The node to consider
294     * @param startIndex The index to start the group
295     * @param childCount The group size
296     * @throws RepositoryException if an error occurred
297     * @throws SAXException if an error occurred
298     */
299    private void _saxGroups(Node node, long startIndex, long childCount) throws RepositoryException, SAXException
300    {
301        // Compute the number of levels.
302        long level = (long) Math.ceil(Math.log(childCount) / Math.log(NodeGroupHelper.MAX_CHILDREN_PER_NODE)) - 1;
303        long childrenPerGroup = (long) Math.pow(NodeGroupHelper.MAX_CHILDREN_PER_NODE, level);
304        long groupCount = (long) Math.ceil((double) childCount / (double) childrenPerGroup);
305        
306        // SAX virtual nodes (groups).
307        for (long i = 0; i < groupCount; i++)
308        {
309            long groupStartIndex = startIndex + i * childrenPerGroup;
310            long groupEndIndex = startIndex + Math.min((i + 1) * childrenPerGroup - 1 , childCount - 1);
311            String name = String.format("%d...%d", groupStartIndex + 1, groupEndIndex + 1);
312            
313            AttributesImpl atts = new AttributesImpl();
314            atts.addCDATAAttribute("level", Long.toString(level));
315            atts.addCDATAAttribute("index", Long.toString(i));
316            atts.addCDATAAttribute("start", Long.toString(groupStartIndex));
317            atts.addCDATAAttribute("end", Long.toString(groupEndIndex));
318            atts.addCDATAAttribute("path", node.getPath());
319            atts.addCDATAAttribute("name", name);
320            atts.addCDATAAttribute("type", "group");
321            XMLUtils.createElement(contentHandler, "group", atts);
322        }
323    }
324
325    /**
326     * Sax a sub node.
327     * @param node the sub node to SAX.
328     * @throws RepositoryException if an error occurred
329     * @throws SAXException if an error occurred
330     */
331    private void _saxSubnode(Node node) throws RepositoryException, SAXException
332    {
333        AttributesImpl subNodeAttributes = new AttributesImpl();
334        int index = node.getIndex();
335        String name = node.getName();
336        
337        if (index != 1)
338        {
339            name = name + "[" + index + "]";
340        }
341        
342        subNodeAttributes.addCDATAAttribute("parentPath", node.getParent().getPath());
343        subNodeAttributes.addCDATAAttribute("name", name);
344        subNodeAttributes.addCDATAAttribute("index", String.valueOf(index));
345        subNodeAttributes.addCDATAAttribute("hasOrderableChildNodes", Boolean.toString(node.getPrimaryNodeType().hasOrderableChildNodes()));
346        subNodeAttributes.addCDATAAttribute("isNew", Boolean.toString(node.isNew()));
347        subNodeAttributes.addCDATAAttribute("isModified", Boolean.toString(node.isModified()));
348        subNodeAttributes.addCDATAAttribute("type", "node");
349        
350        if (!node.hasNodes())
351        {
352            subNodeAttributes.addCDATAAttribute("noChild", "true");
353        }
354        
355        XMLUtils.createElement(contentHandler, "node", subNodeAttributes);
356    }
357
358    private void _saxReferers(Node node) throws SAXException, RepositoryException
359    {
360        PropertyIterator itReference = node.getReferences();
361
362        while (itReference.hasNext())
363        {
364            Property property = itReference.nextProperty();
365            AttributesImpl referenceAttributes = new AttributesImpl();
366            
367            Node parent = property.getParent();
368            
369            referenceAttributes.addCDATAAttribute("path", parent.getPath());
370            referenceAttributes.addCDATAAttribute("pathWithGroups", NodeGroupHelper.getPathWithGroups(parent));
371            referenceAttributes.addCDATAAttribute("name", property.getName());
372            
373            XMLUtils.createElement(contentHandler, "referer", referenceAttributes);
374        }
375    }
376
377    private void _saxValue(Value value) throws RepositoryException, SAXException
378    {
379        Attributes attrs = XMLUtils.EMPTY_ATTRIBUTES;
380        String textValue = null;
381        
382        switch (value.getType())
383        {
384            case PropertyType.BINARY:
385                textValue = "";
386                break;
387            case PropertyType.DATE:
388                textValue = DateUtils.dateToString(value.getDate().getTime());
389                break;
390            case PropertyType.STRING:
391            case PropertyType.PATH:
392            case PropertyType.NAME:
393            case PropertyType.LONG:
394            case PropertyType.BOOLEAN:
395            case PropertyType.REFERENCE:
396            default:
397                textValue = value.getString();
398                break;
399        }
400        
401        if (textValue != null)
402        {
403            XMLUtils.createElement(contentHandler, "value", attrs, textValue);
404        }
405    }
406    
407    /**
408     * Compares two nodes on its names.
409     */
410    protected static class NodeNameComparator implements Comparator<Node>
411    {
412        
413        /** Indicates if the comparators acts in ascending order. */
414        protected boolean _ascending;
415        
416        /**
417         * Constructs a node name comparator.
418         */
419        public NodeNameComparator()
420        {
421            this(true);
422        }
423        
424        /**
425         * Constructs a node name comparator, specifying ascending or descending order.
426         * @param ascending true if we want to compare node names in ascending order, false otherwise.
427         */
428        public NodeNameComparator(boolean ascending)
429        {
430            this._ascending = ascending;
431        }
432        
433        @Override
434        public int compare(Node o1, Node o2)
435        {
436            try
437            {
438                if (_ascending)
439                {
440                    return o1.getName().compareTo(o2.getName());
441                }
442                else
443                {
444                    return o2.getName().compareTo(o1.getName());
445                }
446            }
447            catch (RepositoryException e)
448            {
449                throw new RuntimeException("Impossible to get a node name.", e);
450            }
451        }
452        
453    }
454    
455}