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.workflow.store;
017
018import java.util.ArrayList;
019import java.util.Calendar;
020import java.util.Collections;
021import java.util.Comparator;
022import java.util.Date;
023import java.util.GregorianCalendar;
024import java.util.List;
025import java.util.Map;
026
027import javax.jcr.Node;
028import javax.jcr.NodeIterator;
029import javax.jcr.Property;
030import javax.jcr.PropertyIterator;
031import javax.jcr.Repository;
032import javax.jcr.RepositoryException;
033import javax.jcr.Session;
034import javax.jcr.Value;
035import javax.jcr.ValueFactory;
036import javax.jcr.query.Query;
037import javax.jcr.query.QueryManager;
038import javax.jcr.query.QueryResult;
039
040import org.apache.jackrabbit.commons.JcrUtils;
041import org.apache.jackrabbit.util.ISO8601;
042import org.slf4j.Logger;
043import org.slf4j.LoggerFactory;
044
045import org.ametys.plugins.repository.AmetysRepositoryException;
046
047import com.opensymphony.module.propertyset.PropertySet;
048import com.opensymphony.module.propertyset.memory.MemoryPropertySet;
049import com.opensymphony.workflow.StoreException;
050import com.opensymphony.workflow.query.Expression;
051import com.opensymphony.workflow.query.FieldExpression;
052import com.opensymphony.workflow.query.NestedExpression;
053import com.opensymphony.workflow.query.WorkflowExpressionQuery;
054import com.opensymphony.workflow.query.WorkflowQuery;
055import com.opensymphony.workflow.spi.SimpleWorkflowEntry;
056import com.opensymphony.workflow.spi.Step;
057import com.opensymphony.workflow.spi.WorkflowEntry;
058
059/**
060 * Abstract workflow store for Jackrabbit
061 */
062@SuppressWarnings("deprecation")
063public abstract class AbstractJackrabbitWorkflowStore implements AmetysWorkflowStore
064{
065    /** Namespace for node type names, node names and property names. */
066    public static final String __NAMESPACE = "http://ametys.org/plugin/workflow/1.0";
067    /** Prefix used for this namespace. */
068    public static final String __NAMESPACE_PREFIX = "oswf";
069    /** Prefix with colon for this namespace. */
070    public static final String __NM_PREFIX = __NAMESPACE_PREFIX + ":";
071    /** Root node type. */
072    public static final String __ROOT_NT = __NM_PREFIX + "root";
073    
074    /** Entry node type. */
075    static final String __ENTRY_NT = __NM_PREFIX + "entry";
076    /** Step node type. */
077    static final String __STEP_NT = __NM_PREFIX + "step";
078
079    /** Root node name. */
080    static final String __ROOT_NODE = __ROOT_NT;
081    /** Entry node name prefix. */
082    static final String __ENTRY_NODE_PREFIX = "workflow-";
083    /** Current step node name. */
084    static final String __CURRENT_STEP_NODE = __NM_PREFIX + "currentStep";
085    /** History step node name. */
086    static final String __HISTORY_STEP_NODE = __NM_PREFIX + "historyStep";
087
088    /** Next entry id property for root node. */
089    static final String __NEXT_ENTRY_ID_PROPERTY = __NM_PREFIX + "nextEntryId";
090    /** ID for entry and step node. */
091    static final String __ID_PROPERTY = __NM_PREFIX + "id";
092    /** Workflow name property for entry node. */
093    static final String __WF_NAME_PROPERTY = __NM_PREFIX + "workflowName";
094    /** State property for entry node. */
095    static final String __STATE_PROPERTY = __NM_PREFIX + "state";
096    /** Next step id property for entry node. */
097    static final String __NEXT_STEP_ID_PROPERTY = __NM_PREFIX + "nextStepId";
098    
099    /** Step id property for step node. */
100    static final String __STEP_ID_PROPERTY = __NM_PREFIX + "stepId";
101    /** Action id property for step node. */
102    static final String __ACTION_ID_PROPERTY = __NM_PREFIX + "actionId";
103    /** Owner property for step node. */
104    static final String __OWNER_PROPERTY = __NM_PREFIX + "owner";
105    /** Caller property for step node. */
106    static final String __CALLER_PROPERTY = __NM_PREFIX + "caller";
107    /** Start date property for step node. */
108    static final String __START_DATE_PROPERTY = __NM_PREFIX + "startDate";
109    /** Due date property for step node. */
110    static final String __DUE_DATE_PROPERTY = __NM_PREFIX + "dueDate";
111    /** Finish date property for step node. */
112    static final String __FINISH_DATE_PROPERTY = __NM_PREFIX + "finishDate";
113    /** Status property for step node. */
114    static final String __STATUS_PROPERTY = __NM_PREFIX + "status";
115    /** Previous steps ids property for step node. */
116    static final String __PREVIOUS_STEPS_PROPERTY = __NM_PREFIX + "previousSteps";
117    
118    /** Log instance for logging events, errors, warnings, etc. */
119    protected final Logger _log = LoggerFactory.getLogger(getClass());
120    
121    /** JCR Repository */
122    protected Repository _repository;
123
124    /**
125     * Create a JackrabbitWorkflowStore.
126     * @param repository the JCR Repository to use.
127     */
128    public AbstractJackrabbitWorkflowStore(Repository repository)
129    {
130        _repository = repository;
131    }
132    
133    /**
134     * Open a session to the _repository.
135     * @return the session opened.
136     * @throws RepositoryException if an error occurs.
137     */
138    protected Session _getSession() throws RepositoryException
139    {
140        return _repository.login();
141    }
142    
143    /**
144     * Release a session.<p>
145     * Default implementation calls logout on the session.
146     * @param session the session to release.
147     */
148    protected void _release(Session session)
149    {
150        if (session != null)
151        {
152            session.logout();
153        }
154    }
155    
156    /**
157     * Convert a Date to a Calendar.
158     * @param date The date to convert.
159     * @return The date converted as a Calendar.
160     */
161    protected static Calendar __toCalendar(Date date)
162    {
163        if (date == null)
164        {
165            return null;
166        }
167
168        Calendar calendar = new GregorianCalendar();
169        calendar.setTime(date);
170        return calendar;
171    }
172
173    @Override
174    public void init(Map props) throws StoreException
175    {
176        try
177        {
178            _createRootNode();
179        }
180        catch (RepositoryException e)
181        {
182            throw new StoreException("Unable to initialize repository", e);
183        }
184    }
185    
186    /**
187     * Create the root node.
188     * @throws RepositoryException if an error occurs.
189     */
190    protected abstract void _createRootNode() throws RepositoryException;
191    
192    /**
193     * Get the workflow store root node
194     * @param session the session to use
195     * @return The workflow store root node
196     * @throws RepositoryException if an error occurs.
197     */
198    protected abstract Node _getRootNode(Session session) throws RepositoryException;
199    
200    @Override
201    public void setEntryState(long entryId, int state) throws StoreException
202    {
203        Session session = null;
204
205        try
206        {
207            session = _getSession();
208            // Retrieve existing entry
209            Node entry = getEntryNode(session, entryId);
210
211            // Modify state
212            entry.setProperty(__STATE_PROPERTY, state);
213
214            session.save();
215        }
216        catch (RepositoryException e)
217        {
218            throw new StoreException("Unable to change entry state for entryId: " + entryId, e);
219        }
220        finally
221        {
222            _release(session);
223        }
224    }
225
226    @Override
227    public PropertySet getPropertySet(long entryId) throws StoreException
228    {
229        // FIXME use a JcrPropertySet
230        PropertySet ps = new MemoryPropertySet();
231        ps.init(null, null);
232        return ps;
233        //throw new StoreException("PropertySet is not supported");
234    }
235
236    @Override
237    public Step createCurrentStep(long entryId, int stepId, String owner, Date startDate, Date dueDate, String status, long[] previousIds) throws StoreException
238    {
239        Session session = null;
240        try
241        {
242            session = _getSession();
243            
244            // Retrieve existing entry
245            Node entry = getEntryNode(session, entryId);
246
247            // Generate an unique step id inside the entry
248            long id = _getNextStepId(entry);
249            int actionId = 0;
250
251            // Create a new current step node
252            Node stepNode = entry.addNode(__CURRENT_STEP_NODE, __STEP_NT);
253            stepNode.setProperty(__ID_PROPERTY, id);
254            
255            AmetysStep step = new AmetysStep(stepNode, this);
256            
257            step.setStepId(stepId);
258            step.setActionId(actionId);
259            step.setOwner(owner);
260            step.setStartDate(startDate);
261            
262            if (dueDate != null)
263            {
264                step.setDueDate(dueDate);
265            }
266            
267            step.setStatus(status);
268            step.setPreviousStepIds(previousIds);
269            
270            step.save();
271            
272            return step;
273        }
274        catch (RepositoryException e)
275        {
276            throw new StoreException("Unable to store new entry", e);
277        }
278    }
279
280    @Override
281    public WorkflowEntry createEntry(String workflowName) throws StoreException
282    {
283        int state = WorkflowEntry.CREATED;
284        
285        // Generate an unique entry id
286        long id;
287        try
288        {
289            id = _getNextEntryId();
290        }
291        catch (RepositoryException e)
292        {
293            throw new StoreException("Unable to store new entry", e);
294        }
295        
296        WorkflowEntry workflowEntry = new SimpleWorkflowEntry(id, workflowName, state);
297        
298        // store in JCR
299        storeNewEntry(workflowEntry);
300        
301        return workflowEntry;
302        
303    }
304    
305    /**
306     * Store a new workflow entry into the JCR repository
307     * @param workflowEntry The new entry
308     * @throws StoreException on error
309     */
310    protected void storeNewEntry(WorkflowEntry workflowEntry) throws StoreException
311    {
312        Session session = null;
313        
314        String workflowName = workflowEntry.getWorkflowName();
315        long id = workflowEntry.getId();
316        int state = workflowEntry.getState();
317        
318        try
319        {
320            session = _getSession();
321            
322            // Retrieve workflow store root node and then the parent entry node
323            Node root = _getRootNode(session);
324            Node parentNode = _getOrCreateParentEntryNode(root, id);
325            
326            // Create entry node
327            Node entryNode = parentNode.addNode(__ENTRY_NODE_PREFIX + id, __ENTRY_NT);
328            
329            if (_log.isDebugEnabled())
330            {
331                try
332                {
333                    _log.debug("Storing entry into path: " + entryNode.getPath());
334                }
335                catch (RepositoryException e)
336                {
337                    _log.warn("Unable to retrieve entry node path", e);
338                }
339            }
340            
341            entryNode.setProperty(__ID_PROPERTY, id);
342            entryNode.setProperty(__WF_NAME_PROPERTY, workflowName);
343            entryNode.setProperty(__STATE_PROPERTY, state);
344            
345            session.save();
346        }
347        catch (RepositoryException e)
348        {
349            throw new StoreException("Unable to store new entry", e);
350        }
351        finally
352        {
353            _release(session);
354        }
355    }
356    
357    /**
358     * Remove a workflow entry
359     * @param entryId The id of workflow entry
360     * @throws StoreException if an error occurred
361     */
362    public void removeEntry (long entryId) throws StoreException
363    {
364        Session session = null;
365        
366        try
367        {
368            session = _getSession();
369            
370            Node entryNode = getEntryNode(session, entryId);
371            entryNode.remove();
372            session.save();
373        }
374        catch (RepositoryException e)
375        {
376            throw new StoreException("Unable to delete workflow entry " + entryId, e);
377        }
378        finally
379        {
380            _release(session);
381        }
382    }
383    
384    /**
385     * Retrieves the parent node of a workflow entry.
386     * Creates non existing ancestor nodes when necessary.
387     * @param root The workflow store root node
388     * @param id The workflow entry id
389     * @return The parent node
390     * @throws RepositoryException on repository error
391     */
392    protected abstract Node _getOrCreateParentEntryNode(Node root, long id) throws RepositoryException;
393
394    /**
395     * Retrieve an entry node from its id.
396     * @param session the session to use.
397     * @param entryId the id of the entry.
398     * @return the entry node.
399     * @throws RepositoryException if there is no entry for this id.
400     */
401    public Node getEntryNode(Session session, long entryId) throws RepositoryException
402    {
403        Node rootNode = _getRootNode(session);
404        Node parentNode = _getOrCreateParentEntryNode(rootNode, entryId);
405
406        return parentNode.getNode(__ENTRY_NODE_PREFIX + entryId);
407    }
408
409    /**
410     * Retrieve an history step node from its id for a particular entry.
411     * @param session The session to use.
412     * @param entryId The id of the entry.
413     * @param stepId The id of the step.
414     * @return The step node.
415     * @throws RepositoryException if no step matches or multiple steps match.
416     */
417    Node getHistoryStepNode(Session session, long entryId, long stepId) throws RepositoryException
418    {
419        Node entryNode = getEntryNode(session, entryId);
420        NodeIterator itNode = entryNode.getNodes(__HISTORY_STEP_NODE);
421        
422        while (itNode.hasNext())
423        {
424            Node stepNode = itNode.nextNode();
425            if (JcrUtils.getLongProperty(stepNode, __ID_PROPERTY, -1) == stepId)
426            {
427                return stepNode;
428            }
429        }
430        
431        throw new RepositoryException("Unknown entry node for entryId: " + entryId + " and stepId: " + stepId);
432    }
433    
434    /**
435     * Generate an unique entry id.
436     * @return A new entry id.
437     * @throws RepositoryException if an error occurs.
438     */
439    protected synchronized long _getNextEntryId() throws RepositoryException
440    {
441        Session session = null;
442
443        try
444        {
445            // Use a new session to ensure that just this property is saved.
446            session = _getSession();
447            // Retrieve root node containing entries
448            Node root = _getRootNode(session);
449            long nextEntryId = root.getProperty(__NEXT_ENTRY_ID_PROPERTY).getLong();
450
451            // Set the next entry id
452            root.setProperty(__NEXT_ENTRY_ID_PROPERTY, nextEntryId + 1);
453            session.save();
454
455            // Return the previous entry id
456            return nextEntryId;
457        }
458        finally
459        {
460            _release(session);
461        }
462    }
463
464
465    /**
466     * Generate an unique step id for an entry.
467     * @param entry The entry.
468     * @return A new step id.
469     * @throws RepositoryException if an error occurs.
470     */
471    protected synchronized long _getNextStepId(Node entry) throws RepositoryException
472    {
473        long nextStepId = entry.getProperty(__NEXT_STEP_ID_PROPERTY).getLong();
474
475        // Set the next step id
476        entry.setProperty(__NEXT_STEP_ID_PROPERTY, nextStepId + 1);
477        entry.getSession().save();
478
479        // Return the previous step id
480        return nextStepId;
481    }
482
483    @Override
484    public List findCurrentSteps(long entryId) throws StoreException
485    {
486        List<Step> currentSteps = new ArrayList<>();
487        Session session = null;
488        
489        try
490        {
491            session = _getSession();
492            // Retrieve existing entry
493            Node entry = getEntryNode(session, entryId);
494            // Get current steps
495            NodeIterator nodeIterator = entry.getNodes(__CURRENT_STEP_NODE);
496
497            while (nodeIterator.hasNext())
498            {
499                // Convert each step node to a Step
500                currentSteps.add(new AmetysStep(nodeIterator.nextNode(), this));
501            }
502        }
503        catch (RepositoryException e)
504        {
505            throw new StoreException("Unable to change entry state for entryId: " + entryId, e);
506        }
507        
508        return currentSteps;
509    }
510    
511    @Override
512    public WorkflowEntry findEntry(long entryId) throws StoreException
513    {
514        Session session = null;
515
516        try
517        {
518            session = _getSession();
519            // Retrieve existing entry
520            Node entry = getEntryNode(session, entryId);
521
522            String workflowName = entry.getProperty(__WF_NAME_PROPERTY).getString();
523            int state = (int) entry.getProperty(__STATE_PROPERTY).getLong();
524
525            return new SimpleWorkflowEntry(entryId, workflowName, state);
526        }
527        catch (RepositoryException e)
528        {
529            throw new StoreException("Unable to change entry state for entryId: " + entryId, e);
530        }
531        finally
532        {
533            _release(session);
534        }
535    }
536
537    @Override
538    public List findHistorySteps(long entryId) throws StoreException
539    {
540        List<Step> historySteps = new ArrayList<>();
541        Session session = null;
542
543        try
544        {
545            session = _getSession();
546            // Retrieve existing entry
547            Node entry = getEntryNode(session, entryId);
548            // Get history steps
549            NodeIterator nodeIterator = entry.getNodes(__HISTORY_STEP_NODE);
550
551            while (nodeIterator.hasNext())
552            {
553                // Convert each step node to a Step
554                historySteps.add(new AmetysStep(nodeIterator.nextNode(), this));
555            }
556            
557            // Order by step descendant
558            Collections.sort(historySteps, new Comparator<Step>()
559            {
560                @Override
561                public int compare(Step step1, Step step2)
562                {
563                    return -new Long(step1.getId()).compareTo(step2.getId());
564                }
565            });
566        }
567        catch (RepositoryException e)
568        {
569            throw new StoreException("Unable to change entry state for entryId: " + entryId, e);
570        }
571        
572        return historySteps;
573    }
574
575    @Override
576    public Step markFinished(Step step, int actionId, Date finishDate, String status, String caller) throws StoreException
577    {
578        try
579        {
580            // Add new properties
581            AmetysStep theStep = (AmetysStep) step;
582
583            theStep.setActionId(actionId);
584            theStep.setFinishDate(finishDate);
585            theStep.setStatus(status);
586            theStep.setCaller(caller);
587            
588            theStep.save();
589            
590            return theStep;
591        }
592        catch (AmetysRepositoryException e)
593        {
594            throw new StoreException("Unable to modify step for entryId: " + step.getEntryId() + " and stepId: " + step.getStepId(), e);
595        }
596    }
597
598    @Override
599    public void moveToHistory(Step step) throws StoreException
600    {
601        try
602        {
603            // Retrieve the step node
604            Node stepNode = ((AmetysStep) step).getNode();
605            Node entry = stepNode.getParent();
606
607            // Get the existing path of the current node
608            String currentStepPath = stepNode.getPath();
609            // Set the destination path to history steps
610            String historyStepPath = entry.getPath() + "/" + __HISTORY_STEP_NODE;
611            // Move node
612            stepNode.getSession().move(currentStepPath, historyStepPath);
613
614            stepNode.getSession().save();
615        }
616        catch (RepositoryException e)
617        {
618            throw new StoreException("Unable to move step to history for entryId: " + step.getEntryId() + " and stepId: " + step.getStepId(), e);
619        }
620    }
621
622    @Override
623    public List query(WorkflowQuery query) throws StoreException
624    {
625        List<Long> results = new ArrayList<>();
626        Session session = null;
627
628        try
629        {
630            session = _getSession();
631            
632            // Build XPath query
633            QueryManager queryManager = session.getWorkspace().getQueryManager();
634            StringBuilder xPathQuery = new StringBuilder("//element(*, ");
635            xPathQuery.append(__ENTRY_NT);
636            xPathQuery.append(")");
637            xPathQuery.append("[");
638            xPathQuery.append(getCondition(query));
639            xPathQuery.append("]");
640
641            if (_log.isInfoEnabled())
642            {
643                _log.info("Executing xpath: " + xPathQuery);
644            }
645
646            Query jcrQuery = queryManager.createQuery(xPathQuery.toString(), Query.XPATH);
647            QueryResult result = jcrQuery.execute();
648            NodeIterator nodeIterator = result.getNodes();
649
650            // Add matching entries
651            while (nodeIterator.hasNext())
652            {
653                Node entry = nodeIterator.nextNode();
654                results.add(new Long(entry.getProperty(__ID_PROPERTY).getLong()));
655            }
656        }
657        catch (RepositoryException e)
658        {
659            throw new StoreException("Unable to query entries", e);
660        }
661        finally
662        {
663            _release(session);
664        }
665
666        return results;
667    }
668    
669    @Override
670    public void clearHistory(long entryId) throws StoreException
671    {
672        Session session = null;
673
674        try
675        {
676            session = _getSession();
677            Node entryNode = getEntryNode(session, entryId);
678
679            // Remove all history step nodes
680            NodeIterator itNode = entryNode.getNodes(__HISTORY_STEP_NODE);
681            
682            while (itNode.hasNext())
683            {
684                Node historyStepNode = itNode.nextNode();
685                String historyStepNodeIdentifier = historyStepNode.getIdentifier();
686                // Remove references to this node
687                PropertyIterator itProperty = historyStepNode.getReferences();
688                
689                while (itProperty.hasNext())
690                {
691                    Property property = itProperty.nextProperty();
692                    Node parentNode = property.getParent();
693                    String nodeTypeName = parentNode.getPrimaryNodeType().getName();
694                    
695                    if (nodeTypeName.equals(__STEP_NT))
696                    {
697                        // Remove current reference to this node
698                        ValueFactory valueFactory = session.getValueFactory();
699                        List<Value> newValues = new ArrayList<>();
700                        Value[] values = property.getValues();
701                        
702                        for (Value value : values)
703                        {
704                            String identifier = value.getString();
705                            
706                            if (!identifier.equals(historyStepNodeIdentifier))
707                            {
708                                newValues.add(valueFactory.createValue(identifier));
709                            }
710                        }
711                        
712                        property.setValue(newValues.toArray(new Value[newValues.size()]));
713                    }
714                    else
715                    {
716                        // Not an expected reference
717                        throw new RepositoryException("An history node is references by a unknown node of type: " + nodeTypeName);
718                    }
719                }
720                
721                // Remove history step node
722                historyStepNode.remove();
723            }
724            
725            session.save();
726        }
727        catch (RepositoryException e)
728        {
729            throw new StoreException("Unable to clear entry history for entry id: " + entryId, e);
730        }
731        finally
732        {
733            _release(session);
734        }
735    }
736
737    @Override
738    public void deleteInstance(long entryId) throws StoreException
739    {
740        Session session = null;
741
742        try
743        {
744            session = _getSession();
745            Node entryNode = getEntryNode(session, entryId);
746            
747            // Remove entry node
748            entryNode.remove();
749            
750            session.save();
751        }
752        catch (RepositoryException e)
753        {
754            throw new StoreException("Unable to delete instance for entry id: " + entryId, e);
755        }
756        finally
757        {
758            _release(session);
759        }
760    }
761
762    /**
763     * Get the condition from the query.
764     * 
765     * @param query The query.
766     * @return The resulting condition.
767     * @throws StoreException if the query is not valid.
768     */
769    protected String getCondition(WorkflowQuery query) throws StoreException
770    {
771        if (query.getLeft() == null)
772        {
773            // leaf node
774            return buildFieldExpression(query);
775        }
776        else
777        {
778            int operator = query.getOperator();
779            WorkflowQuery left = query.getLeft();
780            WorkflowQuery right = query.getRight();
781
782            switch (operator)
783            {
784                case WorkflowQuery.AND:
785                    return '(' + getCondition(left) + ") and (" + getCondition(right) + ')';
786
787                case WorkflowQuery.OR:
788                    return '(' + getCondition(left) + ") or (" + getCondition(right) + ')';
789
790                case WorkflowQuery.XOR:
791                default:
792                    throw new IllegalArgumentException("Not supported operator: " + operator);
793            }
794        }
795    }
796
797    /**
798     * Build a field expression.
799     * 
800     * @param query The query.
801     * @return The resulting field expression.
802     * @throws StoreException if the query is not valid.
803     */
804    protected String buildFieldExpression(WorkflowQuery query) throws StoreException
805    {
806        StringBuilder condition = new StringBuilder();
807
808        int qtype = query.getType();
809
810        if (qtype == 0)
811        { // then not set, so look in sub queries
812            if (query.getLeft() != null)
813            {
814                qtype = query.getLeft().getType();
815            }
816        }
817
818        if (qtype == WorkflowQuery.CURRENT)
819        {
820            condition.append(__CURRENT_STEP_NODE + "/");
821        }
822        else
823        {
824            condition.append(__HISTORY_STEP_NODE + "/");
825        }
826
827        Object value = query.getValue();
828        int operator = query.getOperator();
829        int field = query.getField();
830
831        String oper;
832
833        switch (operator)
834        {
835            case WorkflowQuery.EQUALS:
836                oper = " = ";
837                break;
838
839            case WorkflowQuery.NOT_EQUALS:
840                oper = " != ";
841                break;
842
843            case WorkflowQuery.GT:
844                oper = " > ";
845                break;
846
847            case WorkflowQuery.LT:
848                oper = " < ";
849                break;
850
851            default:
852                oper = " = ";
853        }
854
855        String left = getPropertyName(field);
856        String right = "";
857
858        if (value == null)
859        {
860            if (operator == WorkflowQuery.EQUALS)
861            {
862                oper = "";
863            }
864            else if (operator == WorkflowQuery.NOT_EQUALS)
865            {
866                left = "not(" + left + ")";
867                oper = "";
868            }
869            else
870            {
871                right = "null";
872            }
873        }
874        else
875        {
876            right = translateValue(value);
877        }
878
879        condition.append(left);
880        condition.append(oper);
881        condition.append(right);
882
883        return condition.toString();
884    }
885
886    @Override
887    public List query(WorkflowExpressionQuery query) throws StoreException
888    {
889        List<Long> results = new ArrayList<>();
890        Session session = null;
891
892        try
893        {
894            session = _getSession();
895            
896            // Build XPath query
897            QueryManager queryManager = session.getWorkspace().getQueryManager();
898            StringBuilder xPathQuery = new StringBuilder("//element(*, ");
899            xPathQuery.append(__ENTRY_NT);
900            xPathQuery.append(")");
901            xPathQuery.append(getPredicate(query));
902            xPathQuery.append(getSortCriteria(query));
903
904            if (_log.isInfoEnabled())
905            {
906                _log.info("Executing xpath: " + xPathQuery);
907            }
908
909            Query jcrQuery = queryManager.createQuery(xPathQuery.toString(), Query.XPATH);
910            QueryResult result = jcrQuery.execute();
911            NodeIterator nodeIterator = result.getNodes();
912
913            // Add matching entries
914            while (nodeIterator.hasNext())
915            {
916                Node entry = nodeIterator.nextNode();
917                results.add(new Long(entry.getProperty(__ID_PROPERTY).getLong()));
918            }
919        }
920        catch (RepositoryException e)
921        {
922            throw new StoreException("Unable to query entries", e);
923        }
924        finally
925        {
926            _release(session);
927        }
928
929        return results;
930    }
931    
932    /**
933     * Get the JCR sort criteria from the query.
934     * 
935     * @param query The query.
936     * @return The sort criteria.
937     * @throws StoreException if the criteria is not valid.
938     */
939    protected String getSortCriteria(WorkflowExpressionQuery query) throws StoreException
940    {
941        StringBuilder criteria = new StringBuilder();
942
943        if (query.getSortOrder() != WorkflowExpressionQuery.SORT_NONE)
944        {
945            criteria.append(" order by ");
946
947            // Set child axis for specific context (not entry context)
948            // FIXME Use first field expression found for determining context
949            FieldExpression fieldExpr = getFirstFieldExpression(query.getExpression());
950
951            if (fieldExpr == null)
952            {
953                // No field expression, no sorting
954                return "";
955            }
956
957            if (fieldExpr.getContext() == FieldExpression.CURRENT_STEPS)
958            {
959                criteria.append(__CURRENT_STEP_NODE + "/");
960            }
961            else if (fieldExpr.getContext() == FieldExpression.HISTORY_STEPS)
962            {
963                criteria.append(__HISTORY_STEP_NODE + "/");
964            }
965
966            criteria.append(getPropertyName(query.getOrderBy()));
967
968            if (query.getSortOrder() == WorkflowExpressionQuery.SORT_DESC)
969            {
970                criteria.append(" descending");
971            }
972            else
973            {
974                criteria.append(" ascending");
975            }
976        }
977
978        return criteria.toString();
979    }
980    
981    /**
982     * Retrieve the first field expression from an expression.
983     * @param expression the expression to inspect.
984     * @return the first field expression.
985     */
986    protected FieldExpression getFirstFieldExpression(Expression expression)
987    {
988        if (expression.isNested())
989        {
990            NestedExpression nestedExpr = (NestedExpression) expression;
991
992            for (int i = 0; i < nestedExpr.getExpressionCount(); i++)
993            {
994                FieldExpression fieldExpr = getFirstFieldExpression(nestedExpr.getExpression(i));
995
996                if (fieldExpr != null)
997                {
998                    // Field expression found
999                    return fieldExpr;
1000                }
1001            }
1002        }
1003        else
1004        {
1005            // Field expression found
1006            return (FieldExpression) expression;
1007        }
1008
1009        // No field expression found
1010        return null;
1011    }
1012
1013    /**
1014     * Build a predicate from the query.
1015     * 
1016     * @param query The query.
1017     * @return The predicate.
1018     * @throws StoreException if the query is not valid.
1019     */
1020    protected String getPredicate(WorkflowExpressionQuery query) throws StoreException
1021    {
1022        Expression expression = query.getExpression();
1023        String predicate;
1024
1025        if (expression.isNested())
1026        {
1027            predicate = buildNestedExpression((NestedExpression) expression);
1028        }
1029        else
1030        {
1031            predicate = buildFieldExpression((FieldExpression) expression);
1032        }
1033
1034        return "[" + predicate + "]";
1035    }
1036
1037    /**
1038     * Build a nested expression.
1039     * 
1040     * @param nestedExpr The nested expression.
1041     * @return The resulting condition.
1042     * @throws StoreException if the expression is not valid.
1043     */
1044    protected String buildNestedExpression(NestedExpression nestedExpr) throws StoreException
1045    {
1046        StringBuilder query = new StringBuilder();
1047        int exprCount = nestedExpr.getExpressionCount();
1048
1049        for (int i = 0; i < exprCount; i++)
1050        {
1051            Expression subExpression = nestedExpr.getExpression(i);
1052
1053            query.append("(");
1054
1055            if (subExpression instanceof NestedExpression)
1056            {
1057                query.append(buildNestedExpression((NestedExpression) subExpression));
1058            }
1059            else if (subExpression instanceof FieldExpression)
1060            {
1061                query.append(buildFieldExpression((FieldExpression) subExpression));
1062            }
1063
1064            query.append(")");
1065
1066            if (i != exprCount - 1)
1067            {
1068                switch (nestedExpr.getExpressionOperator())
1069                {
1070                    case NestedExpression.OR:
1071                        query.append(" or ");
1072                        break;
1073                    case NestedExpression.AND:
1074                    default:
1075                        query.append(" and ");
1076                        break;
1077                }
1078            }
1079        }
1080
1081        return query.toString();
1082    }
1083
1084    /**
1085     * Build a field expression.
1086     * 
1087     * @param expr The field expression.
1088     * @return The resulting condition.
1089     * @throws StoreException if the expression is not valid.
1090     */
1091    private String buildFieldExpression(FieldExpression expr) throws StoreException
1092    {
1093        StringBuilder query = new StringBuilder();
1094
1095        // Set child axis for specific context (not entry context)
1096        if (expr.getContext() == FieldExpression.CURRENT_STEPS)
1097        {
1098            query.append(__CURRENT_STEP_NODE + "/");
1099        }
1100        else if (expr.getContext() == FieldExpression.HISTORY_STEPS)
1101        {
1102            query.append(__HISTORY_STEP_NODE + "/");
1103        }
1104
1105        Object value = expr.getValue();
1106        int operator = expr.getOperator();
1107        int field = expr.getField();
1108
1109        String oper;
1110
1111        switch (operator)
1112        {
1113            case FieldExpression.EQUALS:
1114                oper = " = ";
1115                break;
1116
1117            case FieldExpression.NOT_EQUALS:
1118                oper = " != ";
1119                break;
1120
1121            case FieldExpression.GT:
1122                oper = " > ";
1123                break;
1124
1125            case FieldExpression.LT:
1126                oper = " < ";
1127                break;
1128
1129            default:
1130                oper = " = ";
1131        }
1132
1133        String left = getPropertyName(field);
1134        String right = "";
1135
1136        if (value == null)
1137        {
1138            if (operator == FieldExpression.EQUALS)
1139            {
1140                oper = "";
1141            }
1142            else if (operator == FieldExpression.NOT_EQUALS)
1143            {
1144                left = "not(" + left + ")";
1145                oper = "";
1146            }
1147            else
1148            {
1149                right = "null";
1150            }
1151        }
1152        else
1153        {
1154            right = translateValue(value);
1155        }
1156
1157        if (expr.isNegate())
1158        {
1159            query.append("not(");
1160        }
1161
1162        query.append(left);
1163        query.append(oper);
1164        query.append(right);
1165
1166        if (expr.isNegate())
1167        {
1168            query.append(")");
1169        }
1170
1171        return query.toString();
1172    }
1173
1174    /**
1175     * Convert and maybe escape a value to support XPath grammar.
1176     * 
1177     * @param value The value to translate.
1178     * @return The translated value.
1179     * @throws StoreException if the value cannot be translated.
1180     */
1181    protected String translateValue(Object value) throws StoreException
1182    {
1183        if (value instanceof Date)
1184        {
1185            Date date = (Date) value;
1186            Calendar calendar = new GregorianCalendar();
1187            calendar.setTime(date);
1188            return " xs:dateTime('" + ISO8601.format(calendar) + "')";
1189        }
1190        else if (value instanceof Integer || value instanceof Long)
1191        {
1192            // No need of escaping
1193            return value.toString();
1194        }
1195
1196        // Escape single quote into two successive single quote
1197        return "'" + value.toString().replaceAll("'", "''") + "'";
1198    }
1199
1200    /**
1201     * Retrieve a propertyName from a field id.
1202     * 
1203     * @param field The field id.
1204     * @return The corresponding property name.
1205     * @throws StoreException if the field is not valid.
1206     * @see com.opensymphony.workflow.query.FieldExpression
1207     */
1208    protected String getPropertyName(int field) throws StoreException
1209    {
1210        switch (field)
1211        {
1212            case FieldExpression.ACTION:
1213                return "@" + __ACTION_ID_PROPERTY;
1214
1215            case FieldExpression.CALLER:
1216                return "@" + __CALLER_PROPERTY;
1217
1218            case FieldExpression.FINISH_DATE:
1219                return "@" + __FINISH_DATE_PROPERTY;
1220
1221            case FieldExpression.OWNER:
1222                return "@" + __OWNER_PROPERTY;
1223
1224            case FieldExpression.START_DATE:
1225                return "@" + __START_DATE_PROPERTY;
1226
1227            case FieldExpression.STEP:
1228                return "@" + __STEP_ID_PROPERTY;
1229
1230            case FieldExpression.STATUS:
1231                return "@" + __STATUS_PROPERTY;
1232
1233            case FieldExpression.STATE:
1234                return "@" + __STATE_PROPERTY;
1235
1236            case FieldExpression.NAME:
1237                return "@" + __WF_NAME_PROPERTY;
1238
1239            case FieldExpression.DUE_DATE:
1240                return "@" + __DUE_DATE_PROPERTY;
1241
1242            default:
1243                throw new StoreException("Invalid field: " + field);
1244        }
1245    }
1246}