001/**
002 * Copyright 2005-2017 The Kuali Foundation
003 *
004 * Licensed under the Educational Community 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.opensource.org/licenses/ecl2.php
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.kuali.rice.kew.impl.document;
017
018import java.io.Serializable;
019import java.util.Arrays;
020import java.util.Collections;
021import java.util.Date;
022import java.util.HashSet;
023import java.util.List;
024import java.util.Map;
025import java.util.Set;
026
027import org.apache.commons.lang.StringUtils;
028import org.joda.time.DateTime;
029import org.kuali.rice.core.api.uif.RemotableAttributeErrorContract;
030import org.kuali.rice.kew.api.KewApiConstants;
031import org.kuali.rice.kew.api.KewApiServiceLocator;
032import org.kuali.rice.kew.api.action.ActionRequest;
033import org.kuali.rice.kew.api.action.ActionRequestType;
034import org.kuali.rice.kew.api.action.ActionTaken;
035import org.kuali.rice.kew.api.action.ActionType;
036import org.kuali.rice.kew.api.action.AdHocRevoke;
037import org.kuali.rice.kew.api.action.AdHocToGroup;
038import org.kuali.rice.kew.api.action.AdHocToPrincipal;
039import org.kuali.rice.kew.api.action.DocumentActionParameters;
040import org.kuali.rice.kew.api.action.DocumentActionResult;
041import org.kuali.rice.kew.api.action.MovePoint;
042import org.kuali.rice.kew.api.action.RequestedActions;
043import org.kuali.rice.kew.api.action.ReturnPoint;
044import org.kuali.rice.kew.api.action.ValidActions;
045import org.kuali.rice.kew.api.action.WorkflowDocumentActionsService;
046import org.kuali.rice.kew.api.document.Document;
047import org.kuali.rice.kew.api.document.DocumentContent;
048import org.kuali.rice.kew.api.document.DocumentContentUpdate;
049import org.kuali.rice.kew.api.document.DocumentDetail;
050import org.kuali.rice.kew.api.document.DocumentStatus;
051import org.kuali.rice.kew.api.document.DocumentUpdate;
052import org.kuali.rice.kew.api.document.attribute.WorkflowAttributeDefinition;
053import org.kuali.rice.kew.api.document.node.RouteNodeInstance;
054import org.kuali.rice.kew.api.document.WorkflowDocumentService;
055
056/**
057 * The implementation of {@link org.kuali.rice.kew.api.WorkflowDocument}.  Implements {@link WorkflowDocumentPrototype} to expose
058 * and initialization method used for construction.
059 * <p>NOTE: operations against document data on this are only "flushed" when an action is performed.</p>
060 * <p><b>This class is *not* thread safe.</b></p>
061 * @see org.kuali.rice.kew.api.WorkflowDocument
062 */
063public class WorkflowDocumentImpl implements Serializable, WorkflowDocumentPrototype {
064
065    private static final long serialVersionUID = -3672966990721719088L;
066
067    /**
068     * The principal id under which all document actions will be performed.
069     */
070    private String principalId;
071    /**
072     * Stores local changes that need to be committed.
073     */
074    private ModifiableDocument modifiableDocument;
075    /**
076     * Stores local changes that need to be committed.
077     */
078    private ModifiableDocumentContent modifiableDocumentContent;
079    /**
080     * Local cache of valid document actions.
081     * @see #getValidActions()  
082     */
083    private ValidActions validActions;
084    /**
085     * Local cache of requested document actions.
086     * @see #getRequestedActions()
087     */
088    private RequestedActions requestedActions;
089    /**
090     * Flag that indicates whether the document has been deleted; if so the object is thereafter in an illegal state.
091     */
092    private boolean documentDeleted = false;
093
094    private transient WorkflowDocumentActionsService workflowDocumentActionsService;
095    private transient WorkflowDocumentService workflowDocumentService;
096
097    public void init(String principalId, Document document) {
098        if (StringUtils.isBlank("principalId")) {
099            throw new IllegalArgumentException("principalId was null or blank");
100        }
101        if (document == null) {
102            throw new IllegalArgumentException("document was null");
103        }
104        this.principalId = principalId;
105        this.modifiableDocument = new ModifiableDocument(document);
106        this.modifiableDocumentContent = null;
107        this.validActions = null;
108        this.requestedActions = null;
109    }
110
111    public WorkflowDocumentActionsService getWorkflowDocumentActionsService() {
112        if (workflowDocumentActionsService == null) {
113            workflowDocumentActionsService = KewApiServiceLocator.getWorkflowDocumentActionsService();
114        }
115        return workflowDocumentActionsService;
116    }
117
118    public void setWorkflowDocumentActionsService(WorkflowDocumentActionsService workflowDocumentActionsService) {
119        this.workflowDocumentActionsService = workflowDocumentActionsService;
120    }
121
122    public WorkflowDocumentService getWorkflowDocumentService() {
123        if (workflowDocumentService == null) {
124            workflowDocumentService = KewApiServiceLocator.getWorkflowDocumentService();
125        }
126        return workflowDocumentService;
127    }
128
129    public void setWorkflowDocumentService(WorkflowDocumentService workflowDocumentService) {
130        this.workflowDocumentService = workflowDocumentService;
131    }
132
133    protected ModifiableDocument getModifiableDocument() {
134        return modifiableDocument;
135    }
136
137    protected ModifiableDocumentContent getModifiableDocumentContent() {
138        if (this.modifiableDocumentContent == null) {
139            DocumentContent documentContent = getWorkflowDocumentService().getDocumentContent(getDocumentId());
140            if (documentContent == null) {
141                throw new IllegalStateException("Failed to load document content for documentId: " + getDocumentId());
142            }
143            this.modifiableDocumentContent = new ModifiableDocumentContent(documentContent);
144        }
145        return this.modifiableDocumentContent;
146    }
147
148    @Override
149    public String getDocumentId() {
150        if (documentDeleted) {
151            throw new IllegalStateException("Document has been deleted.");
152        }
153        return getModifiableDocument().getDocumentId();
154    }
155
156    @Override
157    public Document getDocument() {
158        return getModifiableDocument().getDocument();
159    }
160
161    @Override
162    public DocumentContent getDocumentContent() {
163        return getModifiableDocumentContent().getDocumentContent();
164    }
165
166    @Override
167    public String getApplicationContent() {
168        return getDocumentContent().getApplicationContent();
169    }
170
171    @Override
172    public void setApplicationContent(String applicationContent) {
173        getModifiableDocumentContent().setApplicationContent(applicationContent);
174    }
175    
176    @Override
177    public void setAttributeContent(String attributeContent) {
178        getModifiableDocumentContent().setAttributeContent(attributeContent);
179    }
180
181    @Override
182    public void clearAttributeContent() {
183        getModifiableDocumentContent().setAttributeContent("");
184    }
185
186    @Override
187    public String getAttributeContent() {
188        return getDocumentContent().getAttributeContent();
189    }
190
191    @Override
192    public void addAttributeDefinition(WorkflowAttributeDefinition attributeDefinition) {
193        getModifiableDocumentContent().addAttributeDefinition(attributeDefinition);
194    }
195
196    @Override
197    public void removeAttributeDefinition(WorkflowAttributeDefinition attributeDefinition) {
198        getModifiableDocumentContent().removeAttributeDefinition(attributeDefinition);
199    }
200
201    @Override
202    public void clearAttributeDefinitions() {
203        getAttributeDefinitions().clear();
204    }
205
206    @Override
207    public List<WorkflowAttributeDefinition> getAttributeDefinitions() {
208        return getModifiableDocumentContent().getAttributeDefinitions();
209    }
210
211    @Override
212    public void setSearchableContent(String searchableContent) {
213        getModifiableDocumentContent().setSearchableContent(searchableContent);
214    }
215    
216    @Override
217    public void addSearchableDefinition(WorkflowAttributeDefinition searchableDefinition) {
218        getModifiableDocumentContent().addSearchableDefinition(searchableDefinition);
219    }
220
221    @Override
222    public void removeSearchableDefinition(WorkflowAttributeDefinition searchableDefinition) {
223        getModifiableDocumentContent().removeSearchableDefinition(searchableDefinition);
224    }
225
226    @Override
227    public void clearSearchableDefinitions() {
228        getSearchableDefinitions().clear();
229    }
230
231    @Override
232    public void clearSearchableContent() {
233        getModifiableDocumentContent().setSearchableContent("");
234    }
235
236    @Override
237    public List<WorkflowAttributeDefinition> getSearchableDefinitions() {
238        return getModifiableDocumentContent().getSearchableDefinitions();
239    }
240
241    @Override
242    public List<? extends RemotableAttributeErrorContract> validateAttributeDefinition(
243            WorkflowAttributeDefinition attributeDefinition) {
244        return getWorkflowDocumentActionsService().validateWorkflowAttributeDefinition(attributeDefinition);
245    }
246
247    @Override
248    public List<ActionRequest> getRootActionRequests() {
249        return getWorkflowDocumentService().getRootActionRequests(getDocumentId());
250    }
251
252    @Override
253    public List<ActionTaken> getActionsTaken() {
254        return getWorkflowDocumentService().getActionsTaken(getDocumentId());
255    }
256
257    @Override
258    public void setApplicationDocumentId(String applicationDocumentId) {
259        getModifiableDocument().setApplicationDocumentId(applicationDocumentId);
260    }
261
262    @Override
263    public String getApplicationDocumentId() {
264        return getModifiableDocument().getApplicationDocumentId();
265    }
266
267    @Override
268    public DateTime getDateCreated() {
269        return getModifiableDocument().getDateCreated();
270    }
271
272    @Override
273    public String getTitle() {
274        return getModifiableDocument().getTitle();
275    }
276
277    @Override
278    public ValidActions getValidActions() {
279        if (validActions == null) {
280            validActions = getWorkflowDocumentActionsService().determineValidActions(getDocumentId(), getPrincipalId());
281        }
282        return validActions;
283    }
284
285    @Override
286    public RequestedActions getRequestedActions() {
287        if (requestedActions == null) {
288            requestedActions = getWorkflowDocumentActionsService().determineRequestedActions(getDocumentId(),
289                    getPrincipalId());
290        }
291        return requestedActions;
292    }
293
294    protected DocumentUpdate getDocumentUpdateIfDirty() {
295        if (getModifiableDocument().isDirty()) {
296            return getModifiableDocument().build();
297        }
298        return null;
299    }
300
301    protected DocumentContentUpdate getDocumentContentUpdateIfDirty() {
302        if (getModifiableDocumentContent().isDirty()) {
303            return getModifiableDocumentContent().build();
304        }
305        return null;
306    }
307
308    protected void resetStateAfterAction(DocumentActionResult response) {
309        this.modifiableDocument = new ModifiableDocument(response.getDocument());
310        this.validActions = null;
311        if (response.getValidActions() != null) {
312            this.validActions = response.getValidActions();
313        }
314        this.requestedActions = null;
315        if (response.getRequestedActions() != null) {
316            this.requestedActions = response.getRequestedActions();
317        }
318        // regardless of whether modifiable document content is dirty, we null it out so it will be re-fetched next time it's needed
319        this.modifiableDocumentContent = null;
320    }
321
322    @Override
323    public void saveDocument(String annotation) {
324        DocumentActionResult result = getWorkflowDocumentActionsService().save(
325                constructDocumentActionParameters(annotation));
326        resetStateAfterAction(result);
327    }
328
329    @Override
330    public void route(String annotation) {
331        DocumentActionResult result = getWorkflowDocumentActionsService().route(
332                constructDocumentActionParameters(annotation));
333        resetStateAfterAction(result);
334    }
335
336    @Override
337    public void disapprove(String annotation) {
338        DocumentActionResult result = getWorkflowDocumentActionsService().disapprove(
339                constructDocumentActionParameters(annotation));
340        resetStateAfterAction(result);
341    }
342
343    @Override
344    public void approve(String annotation) {
345        DocumentActionResult result = getWorkflowDocumentActionsService().approve(
346                constructDocumentActionParameters(annotation));
347        resetStateAfterAction(result);
348    }
349
350    @Override
351    public void cancel(String annotation) {
352        DocumentActionResult result = getWorkflowDocumentActionsService().cancel(
353                constructDocumentActionParameters(annotation));
354        resetStateAfterAction(result);
355    }
356
357    @Override
358    public void recall(String annotation, boolean cancel) {
359        DocumentActionResult result = getWorkflowDocumentActionsService().recall(
360                constructDocumentActionParameters(annotation), cancel);
361        resetStateAfterAction(result);
362    }
363
364    @Override
365    public void blanketApprove(String annotation) {
366        DocumentActionResult result = getWorkflowDocumentActionsService().blanketApprove(
367                constructDocumentActionParameters(annotation));
368        resetStateAfterAction(result);
369    }
370
371    @Override
372    public void blanketApprove(String annotation, String... nodeNames) {
373        if (nodeNames == null) {
374            throw new IllegalArgumentException("nodeNames was null");
375        }
376        Set<String> nodeNamesSet = new HashSet<String>(Arrays.asList(nodeNames));
377        DocumentActionResult result = getWorkflowDocumentActionsService().blanketApproveToNodes(
378                constructDocumentActionParameters(annotation), nodeNamesSet);
379        resetStateAfterAction(result);
380    }
381
382    @Override
383    public void saveDocumentData() {
384        DocumentActionResult result = getWorkflowDocumentActionsService().saveDocumentData(
385                constructDocumentActionParameters(null));
386        resetStateAfterAction(result);
387    }
388
389    @Override
390    public void setApplicationDocumentStatus(String applicationDocumentStatus) {
391        getModifiableDocument().setApplicationDocumentStatus(applicationDocumentStatus);
392    }
393
394    @Override
395    public void acknowledge(String annotation) {
396        DocumentActionResult result = getWorkflowDocumentActionsService().acknowledge(
397                constructDocumentActionParameters(annotation));
398        resetStateAfterAction(result);
399    }
400
401    @Override
402    public void fyi(String annotation) {
403        DocumentActionResult result = getWorkflowDocumentActionsService().clearFyi(
404                constructDocumentActionParameters(annotation));
405        resetStateAfterAction(result);
406    }
407
408    @Override
409    public void fyi() {
410        fyi("");
411    }
412
413    @Override
414    public void delete() {
415        getWorkflowDocumentActionsService().delete(getDocumentId(), principalId);
416        documentDeleted = true;
417    }
418
419    @Override
420    public void refresh() {
421        Document document = getWorkflowDocumentService().getDocument(getDocumentId());
422        this.modifiableDocument = new ModifiableDocument(document);
423        this.validActions = null;
424        this.requestedActions = null;
425        this.modifiableDocumentContent = null;
426    }
427
428    @Override
429    public void adHocToPrincipal(ActionRequestType actionRequested, String annotation, String targetPrincipalId,
430            String responsibilityDescription, boolean forceAction) {
431        adHocToPrincipal(actionRequested, null, annotation, targetPrincipalId, responsibilityDescription, forceAction);
432    }
433
434    @Override
435    public void adHocToPrincipal(ActionRequestType actionRequested, String nodeName, String annotation,
436            String targetPrincipalId, String responsibilityDescription, boolean forceAction) {
437        adHocToPrincipal(actionRequested, nodeName, annotation, targetPrincipalId, responsibilityDescription,
438                forceAction, null);
439    }
440
441    @Override
442    public void adHocToPrincipal(ActionRequestType actionRequested, String nodeName, String annotation,
443            String targetPrincipalId, String responsibilityDescription, boolean forceAction, String requestLabel) {
444        AdHocToPrincipal.Builder builder = AdHocToPrincipal.Builder
445                .create(actionRequested, nodeName, targetPrincipalId);
446        builder.setResponsibilityDescription(responsibilityDescription);
447        builder.setForceAction(forceAction);
448        builder.setRequestLabel(requestLabel);
449        DocumentActionResult result = getWorkflowDocumentActionsService().adHocToPrincipal(
450                constructDocumentActionParameters(annotation), builder.build());
451        resetStateAfterAction(result);
452    }
453
454    @Override
455    public void adHocToPrincipal(AdHocToPrincipal adHocToPrincipal, String annotation) {
456        DocumentActionResult result = getWorkflowDocumentActionsService().adHocToPrincipal(
457                constructDocumentActionParameters(annotation), adHocToPrincipal);
458        resetStateAfterAction(result);
459    }
460
461    @Override
462    public void adHocToGroup(ActionRequestType actionRequested, String annotation, String targetGroupId,
463            String responsibilityDescription, boolean forceAction) {
464        adHocToGroup(actionRequested, null, annotation, targetGroupId, responsibilityDescription, forceAction);
465    }
466
467    @Override
468    public void adHocToGroup(ActionRequestType actionRequested, String nodeName, String annotation,
469            String targetGroupId, String responsibilityDescription, boolean forceAction) {
470        adHocToGroup(actionRequested, nodeName, annotation, targetGroupId, responsibilityDescription, forceAction, null);
471    }
472
473    @Override
474    public void adHocToGroup(ActionRequestType actionRequested, String nodeName, String annotation,
475            String targetGroupId, String responsibilityDescription, boolean forceAction, String requestLabel) {
476        AdHocToGroup.Builder builder = AdHocToGroup.Builder.create(actionRequested, nodeName, targetGroupId);
477        builder.setResponsibilityDescription(responsibilityDescription);
478        builder.setForceAction(forceAction);
479        builder.setRequestLabel(requestLabel);
480        DocumentActionResult result = getWorkflowDocumentActionsService().adHocToGroup(
481                constructDocumentActionParameters(annotation), builder.build());
482        resetStateAfterAction(result);
483    }
484
485    @Override
486    public void adHocToGroup(AdHocToGroup adHocToGroup, String annotation) {
487        DocumentActionResult result = getWorkflowDocumentActionsService().adHocToGroup(
488                constructDocumentActionParameters(annotation), adHocToGroup);
489        resetStateAfterAction(result);
490    }
491
492    @Override
493    public void revokeAdHocRequestById(String actionRequestId, String annotation) {
494        if (StringUtils.isBlank(actionRequestId)) {
495            throw new IllegalArgumentException("actionRequestId was null or blank");
496        }
497        DocumentActionResult result = getWorkflowDocumentActionsService().revokeAdHocRequestById(
498                constructDocumentActionParameters(annotation), actionRequestId);
499        resetStateAfterAction(result);
500    }
501
502    @Override
503    public void revokeAdHocRequests(AdHocRevoke revoke, String annotation) {
504        if (revoke == null) {
505            throw new IllegalArgumentException("revokeFromPrincipal was null");
506        }
507        DocumentActionResult result = getWorkflowDocumentActionsService().revokeAdHocRequests(
508                constructDocumentActionParameters(annotation), revoke);
509        resetStateAfterAction(result);
510    }
511
512    @Override
513    public void revokeAllAdHocRequests(String annotation) {
514        DocumentActionResult result = getWorkflowDocumentActionsService().revokeAllAdHocRequests(
515                constructDocumentActionParameters(annotation));
516        resetStateAfterAction(result);
517    }
518
519    @Override
520    public void setTitle(String title) {
521        getModifiableDocument().setTitle(title);
522    }
523
524    @Override
525    public String getDocumentTypeName() {
526        return getDocument().getDocumentTypeName();
527    }
528
529    @Override
530    public boolean isCompletionRequested() {
531        return getRequestedActions().isCompleteRequested();
532    }
533
534    @Override
535    public boolean isApprovalRequested() {
536        return getRequestedActions().isApproveRequested();
537    }
538
539    @Override
540    public boolean isAcknowledgeRequested() {
541        return getRequestedActions().isAcknowledgeRequested();
542    }
543
544    @Override
545    public boolean isFYIRequested() {
546        return getRequestedActions().isFyiRequested();
547    }
548
549    @Override
550    public boolean isBlanketApproveCapable() {
551        return isValidAction(ActionType.BLANKET_APPROVE)
552                && (isCompletionRequested() || isApprovalRequested() || isInitiated());
553    }
554
555    @Override
556    public boolean isRouteCapable() {
557        return isValidAction(ActionType.ROUTE);
558    }
559
560    @Override
561    public boolean isValidAction(ActionType actionType) {
562        if (actionType == null) {
563            throw new IllegalArgumentException("actionType was null");
564        }
565
566        return getWorkflowDocumentActionsService().isValidAction(actionType.getCode(), getDocumentId(),
567                getPrincipalId());
568    }
569
570    @Override
571    public void superUserBlanketApprove(String annotation) {
572        DocumentActionResult result = getWorkflowDocumentActionsService().superUserBlanketApprove(
573                constructDocumentActionParameters(annotation), true);
574        resetStateAfterAction(result);
575    }
576
577    @Override
578    public void superUserNodeApprove(String nodeName, String annotation) {
579        DocumentActionResult result = getWorkflowDocumentActionsService().superUserNodeApprove(
580                constructDocumentActionParameters(annotation), true, nodeName);
581        resetStateAfterAction(result);
582    }
583
584    @Override
585    public void superUserTakeRequestedAction(String actionRequestId, String annotation) {
586        DocumentActionResult result = getWorkflowDocumentActionsService().superUserTakeRequestedAction(
587                constructDocumentActionParameters(annotation), true, actionRequestId);
588        resetStateAfterAction(result);
589    }
590
591    @Override
592    public void superUserDisapprove(String annotation) {
593        DocumentActionResult result = getWorkflowDocumentActionsService().superUserDisapprove(
594                constructDocumentActionParameters(annotation), true);
595        resetStateAfterAction(result);
596    }
597
598    @Override
599    public void superUserCancel(String annotation) {
600        DocumentActionResult result = getWorkflowDocumentActionsService().superUserCancel(
601                constructDocumentActionParameters(annotation), true);
602        resetStateAfterAction(result);
603    }
604
605    @Override
606    public void superUserReturnToPreviousNode(ReturnPoint returnPoint, String annotation) {
607        DocumentActionResult result = getWorkflowDocumentActionsService().superUserReturnToPreviousNode(
608                constructDocumentActionParameters(annotation), true, returnPoint);
609        resetStateAfterAction(result);
610    }
611
612    @Override
613    public void complete(String annotation) {
614        DocumentActionResult result = getWorkflowDocumentActionsService().complete(
615                constructDocumentActionParameters(annotation));
616        resetStateAfterAction(result);
617    }
618
619    @Override
620    public void logAnnotation(String annotation) {
621        getWorkflowDocumentActionsService().logAnnotation(getDocumentId(), principalId, annotation);
622    }
623
624    @Override
625    public DocumentStatus getStatus() {
626        return getDocument().getStatus();
627    }
628
629    @Override
630    public boolean checkStatus(DocumentStatus status) {
631        if (status == null) {
632            throw new IllegalArgumentException("status was null");
633        }
634        return status == getStatus();
635    }
636
637    /**
638     * Indicates if the document is in the initiated state or not.
639     * 
640     * @return true if in the specified state
641     */
642    @Override
643    public boolean isInitiated() {
644        return checkStatus(DocumentStatus.INITIATED);
645    }
646
647    /**
648     * Indicates if the document is in the saved state or not.
649     * 
650     * @return true if in the specified state
651     */
652    @Override
653    public boolean isSaved() {
654        return checkStatus(DocumentStatus.SAVED);
655    }
656
657    /**
658     * Indicates if the document is in the enroute state or not.
659     * 
660     * @return true if in the specified state
661     */
662    @Override
663    public boolean isEnroute() {
664        return checkStatus(DocumentStatus.ENROUTE);
665    }
666
667    /**
668     * Indicates if the document is in the exception state or not.
669     * 
670     * @return true if in the specified state
671     */
672    @Override
673    public boolean isException() {
674        return checkStatus(DocumentStatus.EXCEPTION);
675    }
676
677    /**
678     * Indicates if the document is in the canceled state or not.
679     * 
680     * @return true if in the specified state
681     */
682    @Override
683    public boolean isCanceled() {
684        return checkStatus(DocumentStatus.CANCELED);
685    }
686
687    /**
688     * Indicates if the document is in the recalled state or not.
689     *
690     * @return true if in the specified state
691     */
692    @Override
693    public boolean isRecalled() {
694        return checkStatus(DocumentStatus.RECALLED);
695    }
696
697    /**
698     * Indicates if the document is in the disapproved state or not.
699     * 
700     * @return true if in the specified state
701     */
702    @Override
703    public boolean isDisapproved() {
704        return checkStatus(DocumentStatus.DISAPPROVED);
705    }
706
707    /**
708     * Indicates if the document is in the Processed or Finalized state.
709     * 
710     * @return true if in the specified state
711     */
712    @Override
713    public boolean isApproved() {
714        return isProcessed() || isFinal();
715    }
716
717    /**
718     * Indicates if the document is in the processed state or not.
719     * 
720     * @return true if in the specified state
721     */
722    @Override
723    public boolean isProcessed() {
724        return checkStatus(DocumentStatus.PROCESSED);
725    }
726
727    /**
728     * Indicates if the document is in the final state or not.
729     * 
730     * @return true if in the specified state
731     */
732    @Override
733    public boolean isFinal() {
734        return checkStatus(DocumentStatus.FINAL);
735    }
736
737    /**
738     * Returns the principalId with which this WorkflowDocument was constructed
739     * 
740     * @return the principalId with which this WorkflowDocument was constructed
741     */
742    @Override
743    public String getPrincipalId() {
744        return principalId;
745    }
746
747    @Override
748    public void switchPrincipal(String principalId) {
749        if (StringUtils.isBlank(this.principalId)) {
750            throw new IllegalArgumentException("principalId was null or blank");
751        }
752        this.principalId = principalId;
753        this.validActions = null;
754        this.requestedActions = null;
755    }
756
757    @Override
758    public void takeGroupAuthority(String annotation, String groupId) {
759        DocumentActionResult result = getWorkflowDocumentActionsService().takeGroupAuthority(
760                constructDocumentActionParameters(annotation), groupId);
761        resetStateAfterAction(result);
762    }
763
764    @Override
765    public void releaseGroupAuthority(String annotation, String groupId) {
766        DocumentActionResult result = getWorkflowDocumentActionsService().releaseGroupAuthority(
767                constructDocumentActionParameters(annotation), groupId);
768        resetStateAfterAction(result);
769    }
770
771    @Override
772    public Set<String> getNodeNames() {
773        final List<String> names = getWorkflowDocumentService().getActiveRouteNodeNames(getDocumentId());
774        return Collections.unmodifiableSet(new HashSet<String>(names));
775    }
776
777    @Override
778    public Set<String> getSimpleNodeNames() {
779        final List<String> names = getWorkflowDocumentService().getActiveSimpleRouteNodeNames(getDocumentId());
780        return Collections.unmodifiableSet(new HashSet<String>(names));
781    }
782
783    @Override
784    public Set<String> getCurrentNodeNames() {
785        final List<String> names = getWorkflowDocumentService().getCurrentRouteNodeNames(getDocumentId());
786        return Collections.unmodifiableSet(new HashSet<String>(names));
787    }
788
789    @Override
790    public Set<String> getCurrentSimpleNodeNames() {
791        final List<String> names = getWorkflowDocumentService().getCurrentSimpleRouteNodeNames(getDocumentId());
792        return Collections.unmodifiableSet(new HashSet<String>(names));}
793
794
795    @Override
796    public void returnToPreviousNode(String annotation, String nodeName) {
797        if (nodeName == null) {
798            throw new IllegalArgumentException("nodeName was null");
799        }
800        returnToPreviousNode(annotation, ReturnPoint.create(nodeName));
801    }
802
803    @Override
804    public void returnToPreviousNode(String annotation, ReturnPoint returnPoint) {
805        if (returnPoint == null) {
806            throw new IllegalArgumentException("returnPoint was null");
807        }
808        DocumentActionResult result = getWorkflowDocumentActionsService().returnToPreviousNode(
809                constructDocumentActionParameters(annotation), returnPoint);
810        resetStateAfterAction(result);
811    }
812
813    @Override
814    public void move(MovePoint movePoint, String annotation) {
815        if (movePoint == null) {
816            throw new IllegalArgumentException("movePoint was null");
817        }
818        DocumentActionResult result = getWorkflowDocumentActionsService().move(
819                constructDocumentActionParameters(annotation), movePoint);
820        resetStateAfterAction(result);
821    }
822
823    @Override
824    public List<RouteNodeInstance> getActiveRouteNodeInstances() {
825        return getWorkflowDocumentService().getActiveRouteNodeInstances(getDocumentId());
826    }
827
828    @Override
829    public List<RouteNodeInstance> getCurrentRouteNodeInstances() {
830        return getWorkflowDocumentService().getCurrentRouteNodeInstances(getDocumentId());
831    }
832
833    @Override
834    public List<RouteNodeInstance> getRouteNodeInstances() {
835        return getWorkflowDocumentService().getRouteNodeInstances(getDocumentId());
836    }
837
838    @Override
839    public List<String> getPreviousNodeNames() {
840        return getWorkflowDocumentService().getPreviousRouteNodeNames(getDocumentId());
841    }
842
843    @Override
844    public DocumentDetail getDocumentDetail() {
845        return getWorkflowDocumentService().getDocumentDetail(getDocumentId());
846    }
847
848    @Override
849    public void updateDocumentContent(DocumentContentUpdate documentContentUpdate) {
850        if (documentContentUpdate == null) {
851            throw new IllegalArgumentException("documentContentUpdate was null.");
852        }
853        getModifiableDocumentContent().setDocumentContentUpdate(documentContentUpdate);
854    }
855
856    @Override
857    public void placeInExceptionRouting(String annotation) {
858        DocumentActionResult result = getWorkflowDocumentActionsService().placeInExceptionRouting(
859                constructDocumentActionParameters(annotation));
860        resetStateAfterAction(result);
861    }
862
863    @Override
864    public void setVariable(String name, String value) {
865        getModifiableDocument().setVariable(name, value);
866    }
867
868    @Override
869    public String getVariableValue(String name) {
870        return getModifiableDocument().getVariableValue(name);
871    }
872
873    @Override
874    public void setReceiveFutureRequests() {
875        setVariable(getFutureRequestsKey(principalId), getReceiveFutureRequestsValue());
876    }
877
878    @Override
879    public void setDoNotReceiveFutureRequests() {
880        this.setVariable(getFutureRequestsKey(principalId), getDoNotReceiveFutureRequestsValue());
881    }
882
883    @Override
884    public void setClearFutureRequests() {
885        this.setVariable(getFutureRequestsKey(principalId), getClearFutureRequestsValue());
886    }
887
888    protected String getFutureRequestsKey(String principalId) {
889        return KewApiConstants.RECEIVE_FUTURE_REQUESTS_BRANCH_STATE_KEY + "," + principalId + ","
890                + new Date().toString() + ", " + Math.random();
891    }
892
893    @Override
894    public String getReceiveFutureRequestsValue() {
895        return KewApiConstants.RECEIVE_FUTURE_REQUESTS_BRANCH_STATE_VALUE;
896    }
897
898    @Override
899    public String getDoNotReceiveFutureRequestsValue() {
900        return KewApiConstants.DONT_RECEIVE_FUTURE_REQUESTS_BRANCH_STATE_VALUE;
901    }
902
903    @Override
904    public String getClearFutureRequestsValue() {
905        return KewApiConstants.CLEAR_FUTURE_REQUESTS_BRANCH_STATE_VALUE;
906    }
907
908    protected DocumentActionParameters constructDocumentActionParameters(String annotation) {
909        DocumentActionParameters.Builder builder = DocumentActionParameters.Builder.create(getDocumentId(),
910                getPrincipalId());
911        builder.setAnnotation(annotation);
912        builder.setDocumentUpdate(getDocumentUpdateIfDirty());
913        builder.setDocumentContentUpdate(getDocumentContentUpdateIfDirty());
914        return builder.build();
915    }
916    
917    @Override
918    public DateTime getDateLastModified() {
919        return getDocument().getDateLastModified();
920    }
921
922    @Override
923    public DateTime getDateApproved() {
924        return getDocument().getDateApproved();
925    }
926
927    @Override
928    public DateTime getDateFinalized() {
929        return getDocument().getDateFinalized();
930    }
931
932    @Override
933    public String getInitiatorPrincipalId() {
934        return getDocument().getInitiatorPrincipalId();
935    }
936
937    @Override
938    public String getRoutedByPrincipalId() {
939        return getDocument().getRoutedByPrincipalId();
940    }
941
942    @Override
943    public String getDocumentTypeId() {
944        return getDocument().getDocumentTypeId();
945    }
946
947    @Override
948    public String getDocumentHandlerUrl() {
949        return getDocument().getDocumentHandlerUrl();
950    }
951
952    @Override
953    public String getApplicationDocumentStatus() {
954        return getDocument().getApplicationDocumentStatus();
955    }
956
957    @Override
958    public DateTime getApplicationDocumentStatusDate() {
959        return getDocument().getApplicationDocumentStatusDate();
960    }
961
962    @Override
963    public Map<String, String> getVariables() {
964        return getDocument().getVariables();
965    }
966
967    /**
968     * A wrapper around DocumentContent which keeps track of local changes and generates
969     * a new updated DocumentContent as necessary.
970     */
971    protected static class ModifiableDocumentContent implements Serializable {
972
973        private static final long serialVersionUID = -4458431160327214042L;
974
975        private boolean dirty;
976        private DocumentContent originalDocumentContent;
977        private DocumentContentUpdate.Builder builder;
978
979        protected ModifiableDocumentContent(DocumentContent documentContent) {
980            this.dirty = false;
981            this.originalDocumentContent = documentContent;
982            this.builder = DocumentContentUpdate.Builder.create(documentContent);
983        }
984
985        protected DocumentContent getDocumentContent() {
986            if (!dirty) {
987                return originalDocumentContent;
988            }
989            DocumentContent.Builder documentContentBuilder = DocumentContent.Builder.create(originalDocumentContent);
990            documentContentBuilder.setApplicationContent(builder.getApplicationContent());
991            documentContentBuilder.setAttributeContent(builder.getAttributeContent());
992            documentContentBuilder.setSearchableContent(builder.getSearchableContent());
993            return documentContentBuilder.build();
994        }
995
996        protected DocumentContentUpdate build() {
997            return builder.build();
998        }
999
1000        protected void setDocumentContentUpdate(DocumentContentUpdate update) {
1001            this.builder = DocumentContentUpdate.Builder.create(update);
1002            this.dirty = true;
1003        }
1004
1005        protected void addAttributeDefinition(WorkflowAttributeDefinition definition) {
1006            builder.getAttributeDefinitions().add(definition);
1007            dirty = true;
1008        }
1009
1010        protected void removeAttributeDefinition(WorkflowAttributeDefinition definition) {
1011            builder.getAttributeDefinitions().remove(definition);
1012            dirty = true;
1013        }
1014
1015        protected List<WorkflowAttributeDefinition> getAttributeDefinitions() {
1016            return builder.getAttributeDefinitions();
1017        }
1018
1019        protected void addSearchableDefinition(WorkflowAttributeDefinition definition) {
1020            builder.getSearchableDefinitions().add(definition);
1021            dirty = true;
1022        }
1023
1024        protected void removeSearchableDefinition(WorkflowAttributeDefinition definition) {
1025            builder.getSearchableDefinitions().remove(definition);
1026            dirty = true;
1027        }
1028
1029        protected List<WorkflowAttributeDefinition> getSearchableDefinitions() {
1030            return builder.getAttributeDefinitions();
1031        }
1032
1033        protected void setApplicationContent(String applicationContent) {
1034            if ( !StringUtils.equals(applicationContent, builder.getApplicationContent() ) ) {
1035                builder.setApplicationContent(applicationContent);
1036                dirty = true;
1037            }
1038        }
1039
1040        protected void setAttributeContent(String attributeContent) {
1041            if ( !StringUtils.equals(attributeContent, builder.getAttributeContent() ) ) {
1042                builder.setAttributeContent(attributeContent);
1043                dirty = true;
1044            }
1045        }
1046
1047        public void setAttributeDefinitions(List<WorkflowAttributeDefinition> attributeDefinitions) {
1048            builder.setAttributeDefinitions(attributeDefinitions);
1049            dirty = true;
1050        }
1051
1052        public void setSearchableContent(String searchableContent) {
1053            if ( !StringUtils.equals(searchableContent, builder.getSearchableContent() ) ) {
1054                builder.setSearchableContent(searchableContent);
1055                dirty = true;
1056            }
1057        }
1058
1059        public void setSearchableDefinitions(List<WorkflowAttributeDefinition> searchableDefinitions) {
1060            builder.setSearchableDefinitions(searchableDefinitions);
1061            dirty = true;
1062        }
1063
1064        boolean isDirty() {
1065            return dirty;
1066        }
1067
1068    }
1069
1070    /**
1071     * A wrapper around Document which keeps track of local changes and generates
1072     * a new updated Document as necessary.
1073     */
1074    protected static class ModifiableDocument implements Serializable {
1075
1076        private static final long serialVersionUID = -3234793238863410378L;
1077
1078        private boolean dirty;
1079        private Document originalDocument;
1080        private DocumentUpdate.Builder builder;
1081
1082        protected ModifiableDocument(Document document) {
1083            this.dirty = false;
1084            this.originalDocument = document;
1085            this.builder = DocumentUpdate.Builder.create(document);
1086        }
1087
1088        protected Document getDocument() {
1089            if (!dirty) {
1090                return originalDocument;
1091            }
1092            Document.Builder documentBuilder = Document.Builder.create(originalDocument);
1093            documentBuilder.setApplicationDocumentId(builder.getApplicationDocumentId());
1094            documentBuilder.setTitle(builder.getTitle());
1095            documentBuilder.setApplicationDocumentStatus(builder.getApplicationDocumentStatus());
1096            documentBuilder.setVariables(builder.getVariables());
1097            return documentBuilder.build();
1098        }
1099
1100        protected DocumentUpdate build() {
1101            return builder.build();
1102        }
1103
1104        /**
1105         * Immutable value which is accessed frequently, provide direct access to it.
1106         */
1107        protected String getDocumentId() {
1108            return originalDocument.getDocumentId();
1109        }
1110
1111        /**
1112         * Immutable value which is accessed frequently, provide direct access to it.
1113         */
1114        protected DateTime getDateCreated() {
1115            return originalDocument.getDateCreated();
1116        }
1117
1118        protected String getApplicationDocumentId() {
1119            return builder.getApplicationDocumentId();
1120        }
1121
1122        protected void setApplicationDocumentId(String applicationDocumentId) {
1123            if ( !StringUtils.equals(applicationDocumentId, builder.getApplicationDocumentId() ) ) {
1124                builder.setApplicationDocumentId(applicationDocumentId);
1125                dirty = true;
1126                addDirtyField("applicationDocumentId");
1127            }
1128        }
1129
1130        protected String getTitle() {
1131            return builder.getTitle();
1132        }
1133
1134        protected void setTitle(String title) {
1135            if ( !StringUtils.equals(title, builder.getTitle() ) ) {
1136                builder.setTitle(title);
1137                dirty = true;
1138                addDirtyField("title");
1139            }
1140        }
1141
1142        protected String getApplicationDocumentStatus() {
1143            return builder.getApplicationDocumentStatus();
1144        }
1145
1146        protected void setApplicationDocumentStatus(String applicationDocumentStatus) {
1147            if ( !StringUtils.equals(applicationDocumentStatus, builder.getApplicationDocumentStatus() ) ) {
1148                builder.setApplicationDocumentStatus(applicationDocumentStatus);
1149                dirty = true;
1150                addDirtyField("applicationDocumentStatus");
1151            }
1152        }
1153
1154        protected void setVariable(String name, String value) {
1155            if ( !StringUtils.equals(value, builder.getVariableValue(name) ) ) {           
1156                builder.setVariable(name, value);
1157                dirty = true;
1158                addDirtyField("var[" + name + "]");
1159            }
1160        }
1161
1162        protected String getVariableValue(String name) {
1163            return builder.getVariableValue(name);
1164        }
1165
1166        boolean isDirty() {
1167            return dirty;
1168        }
1169
1170        void addDirtyField(String field) {
1171            builder.addDirtyField(field);
1172        }
1173
1174    }
1175
1176}