001/*
002 *  Copyright 2016 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.plugins.repository.events;
017
018import java.util.Date;
019import java.util.GregorianCalendar;
020
021import javax.jcr.Node;
022import javax.jcr.NodeIterator;
023import javax.jcr.RepositoryException;
024import javax.jcr.Session;
025import javax.jcr.query.Query;
026
027import org.apache.avalon.framework.component.Component;
028import org.apache.cocoon.util.HashUtil;
029import org.apache.commons.lang.StringUtils;
030import org.apache.commons.lang.time.DateFormatUtils;
031
032import org.ametys.core.user.UserIdentity;
033import org.ametys.plugins.repository.RepositoryConstants;
034import org.ametys.plugins.repository.query.SortCriteria;
035import org.ametys.plugins.repository.query.expression.Expression;
036import org.ametys.plugins.repository.query.expression.Expression.Operator;
037
038/**
039 * Helper for implementing {@link EventHolder} in JCR.
040 */
041public final class JCREventHelper implements Component
042{
043    private static final String __EVENT_HOLDER_NODE_NAME = RepositoryConstants.NAMESPACE_PREFIX_INTERNAL + ":events";
044    
045    private static final String __NODETYPE_EVENT = RepositoryConstants.NAMESPACE_PREFIX + ":event";
046    
047    private static final String __NODETYPE_EVENT_ELEMENT = RepositoryConstants.NAMESPACE_PREFIX + ":eventElement";
048    
049    private static final String __EVENT_NAME_PREFIX = "ametys-event_";
050
051    private JCREventHelper()
052    {
053        // Nothing
054    }
055    
056    /**
057     * Get the node holding the JCR events under the given node
058     * @param node The parent node 
059     * @return the events' root node
060     * @throws RepositoryException if an error occurred
061     */
062    public static Node getEventsRootNode(Node node) throws RepositoryException
063    {
064        if (node.hasNode(__EVENT_HOLDER_NODE_NAME))
065        {
066            return node.getNode(__EVENT_HOLDER_NODE_NAME);
067        }
068        
069        return node.addNode(__EVENT_HOLDER_NODE_NAME, "nt:unstructured");
070    }
071    
072    /**
073     * Returns the event child nodes
074     * @param eventHolder The event holder
075     * @return The event child nodes
076     * @throws RepositoryException if an error occurred
077     */
078    public static NodeIterator getEvents(EventHolder eventHolder) throws RepositoryException
079    {
080        return getEvents(eventHolder, null);
081    }
082    
083    /**
084     * Returns the events sorted by ascending date
085     * @param eventHolder The event holder
086     * @param types The types of events to return. Can be <code>null</code> to get all events
087     * @return The event child nodes
088     * @throws RepositoryException if an error occurred
089     */
090    public static NodeIterator getEvents(EventHolder eventHolder, String[] types) throws RepositoryException
091    {
092        EventTypeExpression eventExpr = null;
093        if (types != null && types.length > 0)
094        {
095            eventExpr = new EventTypeExpression(Operator.EQ, types);
096        }
097        
098        SortCriteria sortCriteria = new SortCriteria();
099        sortCriteria.addJCRPropertyCriterion(EventType.EVENT_DATE, false, false);
100        
101        String xPathQuery = getEventXPathQuery(eventHolder, eventExpr, sortCriteria);
102        
103        Session session = eventHolder.getEventsRootNode().getSession();
104        
105        @SuppressWarnings("deprecation")
106        Query query = session.getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH);
107        return query.execute().getNodes();
108    }
109    
110    /**
111     * Creates the XPath query corresponding to specified {@link Expression}.
112     * @param eventHolder the event holder
113     * @param eventExpression the query predicates.
114     * @param sortCriteria the sort criteria.
115     * @return the created XPath query.
116     * @throws RepositoryException if an error occurred
117     */
118    public static String getEventXPathQuery(EventHolder eventHolder, Expression eventExpression, SortCriteria sortCriteria) throws RepositoryException
119    {
120        String predicats = null;
121        
122        if (eventExpression != null)
123        {
124            predicats = StringUtils.trimToNull(eventExpression.build());
125        }
126        
127        String xpathQuery = "/jcr:root"
128            + eventHolder.getEventsRootNode().getPath()
129            + "//element(*, ametys:event)" 
130            + (predicats != null ? "[" + predicats + "]" : "") 
131            + ((sortCriteria != null) ? (" " + sortCriteria.build()) : "");
132        return xpathQuery;
133    }
134    
135    /**
136     * Add an event node
137     * @param eventHolder The event holder
138     * @param eventDate The event's date
139     * @param eventType The event's type
140     * @param author The event's author
141     * @return The created event node
142     * @throws RepositoryException if an error occurred
143     */
144    public static Node addEventNode (EventHolder eventHolder, Date eventDate, String eventType, UserIdentity author) throws RepositoryException
145    {
146        Node contextNode = eventHolder.getEventsRootNode();
147        
148        String eventName = __EVENT_NAME_PREFIX + DateFormatUtils.format(eventDate, "yyyy_MM_dd_HH_mm_ss_SSS");
149        String[] hash = _getHashedPath(eventName);
150        
151        for (String hashPart : hash)
152        {
153            if (!contextNode.hasNode(hashPart))
154            {
155                contextNode = contextNode.addNode(hashPart, __NODETYPE_EVENT_ELEMENT);
156            }
157            else
158            {
159                contextNode = contextNode.getNode(hashPart); 
160            }
161        }
162        
163        Node eventNode = contextNode.addNode(eventName, __NODETYPE_EVENT);
164        
165        // Date
166        GregorianCalendar gc = new GregorianCalendar();
167        gc.setTime(eventDate);
168        
169        eventNode.setProperty(EventType.EVENT_DATE, gc);
170        
171        // Type
172        eventNode.setProperty(EventType.EVENT_TYPE, eventType);
173        
174        // Author
175        Node authorNode = eventNode.addNode(EventType.EVENT_AUTHOR);
176        authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", author.getLogin());
177        authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", author.getPopulationId());
178        
179        return eventNode;
180    }
181    
182    /**
183     * Computes a hashed path in the JCR tree from the name of the child object.<br>
184     * Subclasses may override this method to provide a more suitable hash function.<br>
185     * This implementation relies on the buzhash algorithm.
186     * This method MUST return an array of the same length for each name.
187     * @param name the name of the child object
188     * @return a hashed path of the name.
189     */
190    private static String[] _getHashedPath(String name)
191    {
192        long hash = Math.abs(HashUtil.hash(name));
193        String hashStr = Long.toString(hash, 16);
194        hashStr = StringUtils.leftPad(hashStr, 4, '0');
195        
196        return new String[]{hashStr.substring(0, 2), hashStr.substring(2, 4)};
197    }
198}