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}