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}