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     * Returns events sorted by ascending date
112     * The method search for events in all the repository and not a specific {@link EventHolder}.
113     * @param session The session to use to find the query manager
114     * @param filterExpression The {@link Expression} used to filter the result. Can be {@code null}.
115     * @return The event child nodes
116     * @throws RepositoryException if an error occurred
117     */
118    public static NodeIterator getEvents(Session session, Expression filterExpression) throws RepositoryException
119    {
120        SortCriteria sortCriteria = new SortCriteria();
121        sortCriteria.addJCRPropertyCriterion(EventType.EVENT_DATE, false, false);
122        
123        String xPathQuery = getEventXPathQuery(null, filterExpression, sortCriteria);
124        
125        @SuppressWarnings("deprecation")
126        Query query = session.getWorkspace().getQueryManager().createQuery(xPathQuery, Query.XPATH);
127        return query.execute().getNodes();
128    }
129    
130    /**
131     * Creates the XPath query corresponding to specified {@link Expression}.
132     * @param eventHolder the event holder
133     * @param eventExpression the query predicates.
134     * @param sortCriteria the sort criteria.
135     * @return the created XPath query.
136     * @throws RepositoryException if an error occurred
137     */
138    public static String getEventXPathQuery(EventHolder eventHolder, Expression eventExpression, SortCriteria sortCriteria) throws RepositoryException
139    {
140        String predicats = null;
141        
142        if (eventExpression != null)
143        {
144            predicats = StringUtils.trimToNull(eventExpression.build());
145        }
146        
147        StringBuilder sb = new StringBuilder();
148        if (eventHolder != null)
149        {
150            sb.append("/jcr:root").append(eventHolder.getEventsRootNode().getPath());
151        }
152        sb.append(" //element(*, ametys:event)");
153        if (predicats != null)
154        {
155            sb.append("[").append(predicats).append("]");
156        }
157        if (sortCriteria != null)
158        {
159            sb.append(" ").append(sortCriteria.build());
160        }
161        return sb.toString();
162    }
163    
164    /**
165     * Add an event node
166     * @param eventHolder The event holder
167     * @param eventDate The event's date
168     * @param eventType The event's type
169     * @param author The event's author
170     * @return The created event node
171     * @throws RepositoryException if an error occurred
172     */
173    public static Node addEventNode (EventHolder eventHolder, Date eventDate, String eventType, UserIdentity author) throws RepositoryException
174    {
175        Node contextNode = eventHolder.getEventsRootNode();
176        
177        String eventName = __EVENT_NAME_PREFIX + DateFormatUtils.format(eventDate, "yyyy_MM_dd_HH_mm_ss_SSS");
178        String[] hash = _getHashedPath(eventName);
179        
180        for (String hashPart : hash)
181        {
182            if (!contextNode.hasNode(hashPart))
183            {
184                contextNode = contextNode.addNode(hashPart, __NODETYPE_EVENT_ELEMENT);
185            }
186            else
187            {
188                contextNode = contextNode.getNode(hashPart); 
189            }
190        }
191        
192        Node eventNode = contextNode.addNode(eventName, __NODETYPE_EVENT);
193        
194        // Date
195        GregorianCalendar gc = new GregorianCalendar();
196        gc.setTime(eventDate);
197        
198        eventNode.setProperty(EventType.EVENT_DATE, gc);
199        
200        // Type
201        eventNode.setProperty(EventType.EVENT_TYPE, eventType);
202        
203        // Author
204        Node authorNode = eventNode.addNode(EventType.EVENT_AUTHOR);
205        authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":login", author.getLogin());
206        authorNode.setProperty(RepositoryConstants.NAMESPACE_PREFIX + ":population", author.getPopulationId());
207        
208        return eventNode;
209    }
210    
211    /**
212     * Computes a hashed path in the JCR tree from the name of the child object.<br>
213     * Subclasses may override this method to provide a more suitable hash function.<br>
214     * This implementation relies on the buzhash algorithm.
215     * This method MUST return an array of the same length for each name.
216     * @param name the name of the child object
217     * @return a hashed path of the name.
218     */
219    private static String[] _getHashedPath(String name)
220    {
221        long hash = Math.abs(HashUtil.hash(name));
222        String hashStr = Long.toString(hash, 16);
223        hashStr = StringUtils.leftPad(hashStr, 4, '0');
224        
225        return new String[]{hashStr.substring(0, 2), hashStr.substring(2, 4)};
226    }
227}