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