View Javadoc

1   /*
2    * Copyright 2005-2007 The Kuali Foundation
3    *
4    *
5    * Licensed under the Educational Community License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.opensource.org/licenses/ecl2.php
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.kuali.rice.kew.service;
18  
19  import java.sql.Timestamp;
20  import java.util.ArrayList;
21  import java.util.Calendar;
22  import java.util.List;
23  
24  import org.kuali.rice.core.exception.RiceRuntimeException;
25  import org.kuali.rice.core.resourceloader.GlobalResourceLoader;
26  import org.kuali.rice.kew.dto.ActionRequestDTO;
27  import org.kuali.rice.kew.dto.ActionTakenDTO;
28  import org.kuali.rice.kew.dto.AdHocRevokeDTO;
29  import org.kuali.rice.kew.dto.DocumentContentDTO;
30  import org.kuali.rice.kew.dto.DocumentDetailDTO;
31  import org.kuali.rice.kew.dto.DocumentLinkDTO;
32  import org.kuali.rice.kew.dto.EmplIdDTO;
33  import org.kuali.rice.kew.dto.ModifiableDocumentContentDTO;
34  import org.kuali.rice.kew.dto.MovePointDTO;
35  import org.kuali.rice.kew.dto.NetworkIdDTO;
36  import org.kuali.rice.kew.dto.NoteDTO;
37  import org.kuali.rice.kew.dto.ReturnPointDTO;
38  import org.kuali.rice.kew.dto.RouteHeaderDTO;
39  import org.kuali.rice.kew.dto.RouteNodeInstanceDTO;
40  import org.kuali.rice.kew.dto.UserIdDTO;
41  import org.kuali.rice.kew.dto.WorkflowAttributeDefinitionDTO;
42  import org.kuali.rice.kew.dto.WorkflowAttributeValidationErrorDTO;
43  import org.kuali.rice.kew.dto.WorkflowIdDTO;
44  import org.kuali.rice.kew.exception.WorkflowException;
45  import org.kuali.rice.kew.exception.WorkflowRuntimeException;
46  import org.kuali.rice.kew.util.KEWConstants;
47  import org.kuali.rice.kim.bo.Person;
48  import org.kuali.rice.kim.bo.entity.KimPrincipal;
49  import org.kuali.rice.kim.service.IdentityManagementService;
50  import org.kuali.rice.kim.service.PersonService;
51  import org.kuali.rice.kim.util.KimConstants;
52  
53  /**
54   * Represents a document in Workflow from the perspective of the client.  This class is one of two
55   * (Java) client interfaces to the KEW system (the other being {@link WorkflowInfo} class).  The
56   * first time an instance of this class is created, it will read the client configuration to determine
57   * how to connect to KEW.
58   *
59   * <p>This class is used by creating new instances using the appropriate constructor.  To create a new
60   * document in KEW, create an instance of this class passing a UserIdVO and a
61   * document type name.  To load an existing document, create an instance of this class passing a
62   * UserIdVO and a document ID number.
63   *
64   * <p>Internally, this wrapper interacts with the {@link WorkflowDocumentActions} service exported
65   * over the KSB, maintaining state.
66   *
67   * <p>This class is not thread safe and must by synchronized externally for concurrent access.
68   *
69   * @author Kuali Rice Team (rice.collab@kuali.org)
70   */
71  public class WorkflowDocument implements java.io.Serializable {
72  
73  	private static final long serialVersionUID = -3672966990721719088L;
74  
75  	/**
76  	 * The principal ID of the user as whom actions will be taken on the KEW document
77  	 */
78      private String principalId;
79      /**
80       * RouteHeader VO of the KEW document this WorkflowDocument represents
81       */
82      private RouteHeaderDTO routeHeader;
83      /**
84       * Flag that indicates whether the document content currently loaded needs to be refreshed.
85       * This is the case either if the document content has not yet been loaded, or an action
86       * that might possibly affect the document content (which is potentially any action) has
87       * subsequently been taken on the document through this API.
88       */
89      private boolean documentContentDirty = false;
90      /**
91       * Value Object encapsulating the document content
92       */
93      private ModifiableDocumentContentDTO documentContent;
94  
95      /**
96       * @deprecated Use the constructor which takes a principal ID instead.
97       */
98      public WorkflowDocument(UserIdDTO userId, String documentType) throws WorkflowException {
99      	String principalId = convertUserIdToPrincipalId(userId);
100     	init(principalId, documentType, null);
101     }
102 
103     /**
104      * @deprecated Use the constructor which takes a principal ID instead.
105      */
106     public WorkflowDocument(UserIdDTO userId, Long routeHeaderId) throws WorkflowException {
107     	String principalId = convertUserIdToPrincipalId(userId);
108     	init(principalId, null, routeHeaderId);
109     }
110 
111     private String convertUserIdToPrincipalId(UserIdDTO userId) {
112 
113         if (userId == null) {
114             return null;
115         } else if (userId instanceof WorkflowIdDTO) {
116             return ((WorkflowIdDTO)userId).getWorkflowId();
117         } else if (userId instanceof NetworkIdDTO) {
118             IdentityManagementService identityManagementService = (IdentityManagementService)GlobalResourceLoader.getService(KimConstants.KIM_IDENTITY_MANAGEMENT_SERVICE);
119             String principalName = ((NetworkIdDTO)userId).getNetworkId();
120             KimPrincipal principal = identityManagementService.getPrincipalByPrincipalName(principalName);
121             return principal.getPrincipalId();
122         } else if (userId instanceof EmplIdDTO) {
123             PersonService personService = (PersonService)GlobalResourceLoader.getService(KimConstants.KIM_PERSON_SERVICE);
124             String employeeId = ((EmplIdDTO)userId).getEmplId();
125             Person person = personService.getPersonByEmployeeId(employeeId);
126             if (person == null) {
127                 throw new RiceRuntimeException("Could not locate a person with the given employee id of " + employeeId);
128             }
129             return person.getPrincipalId();
130         }
131         throw new IllegalArgumentException("Invalid UserIdDTO type was passed: " + userId);
132     }
133 
134     /**
135      * Constructs a WorkflowDocument representing a new document in the workflow system.
136      * Creation/committing of the new document is deferred until the first action is
137      * taken on the document.
138      * @param principalId the user as which to take actions on the document
139      * @param documentType the type of the document to create
140      * @throws WorkflowException if anything goes awry
141      */
142     public WorkflowDocument(String principalId, String documentType) throws WorkflowException {
143         init(principalId, documentType, null);
144     }
145 
146     /**
147      * Loads a workflow document with the given route header ID for the given User.  If no document
148      * can be found with the given ID, then the {@link getRouteHeader()} method of the WorkflowDocument
149      * which is created will return null.
150      *
151      * @throws WorkflowException if there is a problem loading the WorkflowDocument
152      */
153     public WorkflowDocument(String principalId, Long routeHeaderId) throws WorkflowException {
154         init(principalId, null, routeHeaderId);
155     }
156 
157     /**
158      * Initializes this WorkflowDocument object, by either attempting to load an existing document by routeHeaderid
159      * if one is supplied (non-null), or by constructing an empty document of the specified type.
160      * @param principalId the user under which actions via this API on the specified document will be taken
161      * @param documentType the type of document this WorkflowDocument should represent (either this parameter or routeHeaderId must be specified, non-null)
162      * @param routeHeaderId the id of an existing document to load (either this parameter or documentType must be specified, non-null)
163      * @throws WorkflowException if a routeHeaderId is specified but an exception occurs trying to load the document route header
164      */
165     private void init(String principalId, String documentType, Long routeHeaderId) throws WorkflowException {
166     	this.principalId = principalId;
167     	routeHeader = new RouteHeaderDTO();
168     	routeHeader.setDocTypeName(documentType);
169     	if (routeHeaderId != null) {
170     		routeHeader = getWorkflowUtility().getRouteHeaderWithPrincipal(principalId, routeHeaderId);
171     	}
172     }
173 
174     /**
175      * Retrieves the WorkflowUtility proxy from the locator.  The locator will cache this for us.
176      */
177     private WorkflowUtility getWorkflowUtility() throws WorkflowException {
178         WorkflowUtility workflowUtility = 
179         	(WorkflowUtility)GlobalResourceLoader.getService(KEWConstants.WORKFLOW_UTILITY_SERVICE);
180     	if (workflowUtility == null) {
181     		throw new WorkflowException("Could not locate the WorkflowUtility service.  Please ensure that KEW client is configured properly!");
182     	}
183     	return workflowUtility;
184 
185     }
186 
187     /**
188      * Retrieves the WorkflowDocumentActions proxy from the locator.  The locator will cache this for us.
189      */
190     private WorkflowDocumentActions getWorkflowDocumentActions() throws WorkflowException {
191     	WorkflowDocumentActions workflowDocumentActions = 
192     		(WorkflowDocumentActions)GlobalResourceLoader.getService(KEWConstants.WORKFLOW_DOCUMENT_ACTIONS_SERVICE);
193     	if (workflowDocumentActions == null) {
194     		throw new WorkflowException("Could not locate the WorkflowDocumentActions service.  Please ensure that KEW client is configured properly!");
195     	}
196     	return workflowDocumentActions;
197     }
198 
199     // ########################
200     // Document Content methods
201     // ########################
202 
203     /**
204      * Returns an up-to-date DocumentContent of this document.
205      * @see WorkflowUtility#getDocumentContent(Long)
206      */
207     public DocumentContentDTO getDocumentContent() {
208     	try {
209     		// create the document if it hasn't already been created
210     		if (getRouteHeader().getRouteHeaderId() == null) {
211         		routeHeader = getWorkflowDocumentActions().createDocument(principalId, getRouteHeader());
212         	}
213     		if (documentContent == null || documentContentDirty) {
214     			documentContent = new ModifiableDocumentContentDTO(getWorkflowUtility().getDocumentContent(routeHeader.getRouteHeaderId()));
215     			documentContentDirty = false;
216     		}
217     	} catch (Exception e) {
218     		throw handleExceptionAsRuntime(e);
219     	}
220     	return documentContent;
221     }
222 
223     /**
224      * Returns the application specific section of the document content. This is
225      * a convenience method that delegates to the {@link DocumentContentDTO}.
226      *
227      * For documents routed prior to Workflow 2.0:
228      * If the application did NOT use attributes for XML generation, this method will
229      * return the entire document content XML.  Otherwise it will return the empty string.
230      * @see DocumentContentDTO#getApplicationContent()
231      */
232     public String getApplicationContent() {
233         return getDocumentContent().getApplicationContent();
234     }
235 
236     /**
237      * Sets the application specific section of the document content. This is
238      * a convenience method that delegates to the {@link DocumentContentDTO}.
239      */
240     public void setApplicationContent(String applicationContent) {
241         getDocumentContent().setApplicationContent(applicationContent);
242     }
243 
244     /**
245      * Clears all attribute document content from the document.
246      * Typically, this will be used if it is necessary to update the attribute doc content on
247      * the document.  This can be accomplished by clearing the content and then adding the
248      * desired attribute definitions.
249      *
250      * This is a convenience method that delegates to the {@link DocumentContentDTO}.
251      *
252      * In order for these changes to take effect, an action must be performed on the document (such as "save").
253      */
254     public void clearAttributeContent() {
255         getDocumentContent().setAttributeContent("");
256     }
257 
258     /**
259      * Returns the attribute-generated section of the document content. This is
260      * a convenience method that delegates to the {@link DocumentContentDTO}.
261      * @see DocumentContentDTO#getAttributeContent()
262      */
263     public String getAttributeContent() {
264         return getDocumentContent().getAttributeContent();
265     }
266 
267     /**
268      * Adds an attribute definition which defines creation parameters for a WorkflowAttribute
269      * implementation.  The created attribute will be used to generate attribute document content.
270      * When the document is sent to the server, this will be appended to the existing attribute
271      * doc content.  If it is required to replace the attribute document content, then the
272      * clearAttributeContent() method should be invoked prior to adding attribute definitions.
273      *
274      * This is a convenience method that delegates to the {@link DocumentContentDTO}.
275      * @see DocumentContentDTO#addAttributeDefinition(WorkflowAttributeDefinitionDTO)
276      */
277     public void addAttributeDefinition(WorkflowAttributeDefinitionDTO attributeDefinition) {
278         getDocumentContent().addAttributeDefinition(attributeDefinition);
279     }
280 
281     /**
282      * Validate the WorkflowAttributeDefinition against it's attribute on the server.  This will validate
283      * the inputs that will eventually become xml.
284      *
285      * Only applies to attributes implementing WorkflowAttributeXmlValidator.
286      *
287      * This is a call through to the WorkflowInfo object and is here for convenience.
288      *
289      * @param attributeDefinition the workflow attribute definition VO to validate
290      * @return WorkflowAttributeValidationErrorVO[] of error from the attribute
291      * @throws WorkflowException when attribute doesn't implement WorkflowAttributeXmlValidator
292      * @see WorkflowUtility#validateWorkflowAttributeDefinitionVO(WorkflowAttributeDefinitionDTO)
293      */
294     public WorkflowAttributeValidationErrorDTO[] validateAttributeDefinition(WorkflowAttributeDefinitionDTO attributeDefinition) throws WorkflowException {
295     	return getWorkflowUtility().validateWorkflowAttributeDefinitionVO(attributeDefinition);
296     }
297 
298     /**
299      * Removes an attribute definition from the document content.  This is
300      * a convenience method that delegates to the {@link DocumentContentDTO}.
301      * @param attributeDefinition the attribute definition VO to remove
302      */
303     public void removeAttributeDefinition(WorkflowAttributeDefinitionDTO attributeDefinition) {
304         getDocumentContent().removeAttributeDefinition(attributeDefinition);
305     }
306 
307     /**
308      * Removes all attribute definitions from the document content. This is
309      * a convenience method that delegates to the {@link DocumentContentDTO}.
310      */
311     public void clearAttributeDefinitions() {
312     	getDocumentContent().setAttributeDefinitions(new WorkflowAttributeDefinitionDTO[0]);
313     }
314 
315     /**
316      * Returns the attribute definition VOs currently defined on the document content. This is
317      * a convenience method that delegates to the {@link DocumentContentDTO}.
318      * @return the attribute definition VOs currently defined on the document content.
319      * @see DocumentContentDTO#getAttributeDefinitions()
320      */
321     public WorkflowAttributeDefinitionDTO[] getAttributeDefinitions() {
322         return getDocumentContent().getAttributeDefinitions();
323     }
324 
325     /**
326      * Adds a searchable attribute definition which defines creation parameters for a SearchableAttribute
327      * implementation.  The created attribute will be used to generate searchable document content.
328      * When the document is sent to the server, this will be appended to the existing searchable
329      * doc content.  If it is required to replace the searchable document content, then the
330      * clearSearchableContent() method should be invoked prior to adding definitions. This is
331      * a convenience method that delegates to the {@link DocumentContentDTO}.
332      */
333     public void addSearchableDefinition(WorkflowAttributeDefinitionDTO searchableDefinition) {
334         getDocumentContent().addSearchableDefinition(searchableDefinition);
335     }
336 
337     /**
338      * Removes a searchable attribute definition from the document content. This is
339      * a convenience method that delegates to the {@link DocumentContentDTO}.
340      * @param searchableDefinition the searchable attribute definition to remove
341      */
342     public void removeSearchableDefinition(WorkflowAttributeDefinitionDTO searchableDefinition) {
343         getDocumentContent().removeSearchableDefinition(searchableDefinition);
344     }
345 
346     /**
347      * Removes all searchable attribute definitions from the document content. This is
348      * a convenience method that delegates to the {@link DocumentContentDTO}.
349      */
350     public void clearSearchableDefinitions() {
351         getDocumentContent().setSearchableDefinitions(new WorkflowAttributeDefinitionDTO[0]);
352     }
353 
354     /**
355      * Clears the searchable content from the document content. This is
356      * a convenience method that delegates to the {@link DocumentContentDTO}.
357      */
358     public void clearSearchableContent() {
359     	getDocumentContent().setSearchableContent("");
360     }
361 
362     /**
363      * Returns the searchable attribute definitions on the document content. This is
364      * a convenience method that delegates to the {@link DocumentContentDTO}.
365      * @return the searchable attribute definitions on the document content.
366      * @see DocumentContentDTO#getSearchableDefinitions()
367      */
368     public WorkflowAttributeDefinitionDTO[] getSearchableDefinitions() {
369         return getDocumentContent().getSearchableDefinitions();
370     }
371 
372     // ########################
373     // END Document Content methods
374     // ########################
375 
376     /**
377      * Returns the RouteHeaderVO for the workflow document this WorkflowDocument represents
378      */
379     public RouteHeaderDTO getRouteHeader() {
380         return routeHeader;
381     }
382 
383     /**
384      * Returns the id of the workflow document this WorkflowDocument represents.  If this is a new document
385      * that has not yet been created, the document is first created (and therefore this will return a new id)
386      * @return the id of the workflow document this WorkflowDocument represents
387      * @throws WorkflowException if an error occurs during document creation
388      */
389     public Long getRouteHeaderId() throws WorkflowException {
390     	createDocumentIfNeccessary();
391     	return getRouteHeader().getRouteHeaderId();
392     }
393 
394     /**
395      * Returns VOs of the pending ActionRequests on this document.  If this object represents a new document
396      * that has not yet been created, then an empty array will be returned.  The ordering of ActionRequests
397      * returned by this method is not guaranteed.
398      *
399      * This method relies on the WorkflowUtility service
400      *
401      * @return VOs of the pending ActionRequests on this document
402      * @throws WorkflowException if an error occurs obtaining the pending action requests for this document
403      * @see WorkflowUtility#getActionRequests(Long)
404      */
405     public ActionRequestDTO[] getActionRequests() throws WorkflowException {
406         if (getRouteHeaderId() == null) {
407             return new ActionRequestDTO[0];
408         }
409         return getWorkflowUtility().getAllActionRequests(getRouteHeaderId());
410     }
411 
412     /**
413      * Returns VOs of the actions taken on this document.  If this object represents a new document
414      * that has not yet been created, then an empty array will be returned.  The ordering of actions taken
415      * returned by this method is not guaranteed.
416      *
417      * This method relies on the WorkflowUtility service
418      *
419      * @return VOs of the actions that have been taken on this document
420      * @throws WorkflowException if an error occurs obtaining the actions taken on this document
421      * @see WorkflowUtility#getActionsTaken(Long)
422      */
423     public ActionTakenDTO[] getActionsTaken() throws WorkflowException {
424         if (getRouteHeaderId() == null) {
425             return new ActionTakenDTO[0];
426         }
427         return getWorkflowUtility().getActionsTaken(getRouteHeaderId());
428     }
429 
430     /**
431      * Sets the "application doc id" on the document
432      * @param appDocId the "application doc id" to set on the workflow document
433      */
434     public void setAppDocId(String appDocId) {
435         routeHeader.setAppDocId(appDocId);
436     }
437 
438     /**
439      * Returns the "application doc id" set on this workflow document (if any)
440      * @return the "application doc id" set on this workflow document (if any)
441      */
442     public String getAppDocId() {
443         return routeHeader.getAppDocId();
444     }
445 
446     /**
447      * Returns the date/time the document was created, or null if the document has not yet been created
448      * @return the date/time the document was created, or null if the document has not yet been created
449      */
450     public Timestamp getDateCreated() {
451     	if (routeHeader.getDateCreated() == null) {
452     		return null;
453     	}
454     	return new Timestamp(routeHeader.getDateCreated().getTime().getTime());
455     }
456 
457     /**
458      * Returns the title of the document
459      * @return the title of the document
460      */
461     public String getTitle() {
462         return getRouteHeader().getDocTitle();
463     }
464 
465     /**
466      * Performs the 'save' action on the document this WorkflowDocument represents.  If this is a new document,
467      * the document is created first.
468      * @param annotation the message to log for the action
469      * @throws WorkflowException in case an error occurs saving the document
470      * @see WorkflowDocumentActions#saveDocument(UserIdDTO, RouteHeaderDTO, String)
471      */
472     public void saveDocument(String annotation) throws WorkflowException {
473     	createDocumentIfNeccessary();
474     	routeHeader = getWorkflowDocumentActions().saveDocument(principalId, getRouteHeader(), annotation);
475     	documentContentDirty = true;
476     }
477 
478     /**
479      * Performs the 'route' action on the document this WorkflowDocument represents.  If this is a new document,
480      * the document is created first.
481      * @param annotation the message to log for the action
482      * @throws WorkflowException in case an error occurs routing the document
483      * @see WorkflowDocumentActions#routeDocument(UserIdDTO, RouteHeaderDTO, String)
484      */
485     public void routeDocument(String annotation) throws WorkflowException {
486     	createDocumentIfNeccessary();
487     	routeHeader = getWorkflowDocumentActions().routeDocument(principalId, routeHeader, annotation);
488     	documentContentDirty = true;
489     }
490 
491     /**
492      * Performs the 'disapprove' action on the document this WorkflowDocument represents.  If this is a new document,
493      * the document is created first.
494      * @param annotation the message to log for the action
495      * @throws WorkflowException in case an error occurs disapproving the document
496      * @see WorkflowDocumentActions#disapproveDocument(UserIdDTO, RouteHeaderDTO, String)
497      */
498     public void disapprove(String annotation) throws WorkflowException {
499     	createDocumentIfNeccessary();
500     	routeHeader = getWorkflowDocumentActions().disapproveDocument(principalId, getRouteHeader(), annotation);
501     	documentContentDirty = true;
502     }
503 
504     /**
505      * Performs the 'approve' action on the document this WorkflowDocument represents.  If this is a new document,
506      * the document is created first.
507      * @param annotation the message to log for the action
508      * @throws WorkflowException in case an error occurs approving the document
509      * @see WorkflowDocumentActions#approveDocument(UserIdDTO, RouteHeaderDTO, String)
510      */
511     public void approve(String annotation) throws WorkflowException {
512     	createDocumentIfNeccessary();
513     	routeHeader = getWorkflowDocumentActions().approveDocument(principalId, getRouteHeader(), annotation);
514     	documentContentDirty = true;
515     }
516 
517     /**
518      * Performs the 'cancel' action on the document this WorkflowDocument represents.  If this is a new document,
519      * the document is created first.
520      * @param annotation the message to log for the action
521      * @throws WorkflowException in case an error occurs canceling the document
522      * @see WorkflowDocumentActions#cancelDocument(UserIdDTO, RouteHeaderDTO, String)
523      */
524     public void cancel(String annotation) throws WorkflowException {
525     	createDocumentIfNeccessary();
526     	routeHeader = getWorkflowDocumentActions().cancelDocument(principalId, getRouteHeader(), annotation);
527     	documentContentDirty = true;
528     }
529 
530     /**
531      * Performs the 'blanket-approve' action on the document this WorkflowDocument represents.  If this is a new document,
532      * the document is created first.
533      * @param annotation the message to log for the action
534      * @throws WorkflowException in case an error occurs blanket-approving the document
535      * @see WorkflowDocumentActions#blanketApprovalToNodes(UserIdDTO, RouteHeaderDTO, String, String[])
536      */
537     public void blanketApprove(String annotation) throws WorkflowException {
538         blanketApprove(annotation, (String)null);
539     }
540 
541     /**
542      * Commits any changes made to the local copy of this document to the workflow system.  If this is a new document,
543      * the document is created first.
544      * @throws WorkflowException in case an error occurs saving the document
545      * @see WorkflowDocumentActions#saveRoutingData(UserIdDTO, RouteHeaderDTO)
546      */
547     public void saveRoutingData() throws WorkflowException {
548     	createDocumentIfNeccessary();
549     	routeHeader = getWorkflowDocumentActions().saveRoutingData(principalId, getRouteHeader());
550     	documentContentDirty = true;
551     }
552 
553     /**
554      * 
555      * This method sets the Application Document Status and then calls saveRoutingData() to commit 
556      * the changes to the workflow system.
557      * 
558      * @param appDocStatus
559      * @throws WorkflowException
560      */
561     public void updateAppDocStatus(String appDocStatus) throws WorkflowException {
562        	getRouteHeader().setAppDocStatus(appDocStatus);
563        	saveRoutingData();
564     }
565     
566     /**
567      * Performs the 'acknowledge' action on the document this WorkflowDocument represents.  If this is a new document,
568      * the document is created first.
569      * @param annotation the message to log for the action
570      * @throws WorkflowException in case an error occurs acknowledging the document
571      * @see WorkflowDocumentActions#acknowledgeDocument(UserIdDTO, RouteHeaderDTO, String)
572      */
573     public void acknowledge(String annotation) throws WorkflowException {
574     	createDocumentIfNeccessary();
575     	routeHeader = getWorkflowDocumentActions().acknowledgeDocument(principalId, getRouteHeader(), annotation);
576     	documentContentDirty = true;
577     }
578 
579     /**
580      * Performs the 'fyi' action on the document this WorkflowDocument represents.  If this is a new document,
581      * the document is created first.
582      * @param annotation the message to log for the action
583      * @throws WorkflowException in case an error occurs fyi-ing the document
584      */
585     public void fyi() throws WorkflowException {
586     	createDocumentIfNeccessary();
587     	routeHeader = getWorkflowDocumentActions().clearFYIDocument(principalId, getRouteHeader());
588     	documentContentDirty = true;
589     }
590 
591     /**
592      * Performs the 'delete' action on the document this WorkflowDocument represents.  If this is a new document,
593      * the document is created first.
594      * @param annotation the message to log for the action
595      * @throws WorkflowException in case an error occurs deleting the document
596      * @see WorkflowDocumentActions#deleteDocument(UserIdDTO, RouteHeaderDTO)
597      */
598     public void delete() throws WorkflowException {
599     	createDocumentIfNeccessary();
600     	getWorkflowDocumentActions().deleteDocument(principalId, getRouteHeader());
601     	documentContentDirty = true;
602     }
603 
604     /**
605      * Reloads the document route header.  If this is a new document, the document is created first.
606      * Next time document content is accessed, an up-to-date copy will be retrieved from workflow.
607      * @throws WorkflowException in case an error occurs retrieving the route header
608      */
609     public void refreshContent() throws WorkflowException {
610     	createDocumentIfNeccessary();
611     	routeHeader = getWorkflowUtility().getRouteHeader(getRouteHeaderId());
612     	documentContentDirty = true;
613     }
614 
615     /**
616      * Sends an ad hoc request to the specified user at the current active node on the document.  If the document is
617      * in a terminal state, the request will be attached to the terminal node.
618      */
619     public void adHocRouteDocumentToPrincipal(String actionRequested, String annotation, String principalId, String responsibilityDesc, boolean forceAction) throws WorkflowException {
620     	adHocRouteDocumentToPrincipal(actionRequested, null, annotation, principalId, responsibilityDesc, forceAction);
621     }
622 
623     /**
624      * Sends an ad hoc request to the specified user at the specified node on the document.  If the document is
625      * in a terminal state, the request will be attached to the terminal node.
626      */
627     public void adHocRouteDocumentToPrincipal(String actionRequested, String nodeName, String annotation, String principalId, String responsibilityDesc, boolean forceAction) throws WorkflowException {
628     	adHocRouteDocumentToPrincipal(actionRequested, nodeName, annotation, principalId, responsibilityDesc, forceAction, null);
629     }
630 
631     /**
632      * Sends an ad hoc request to the specified user at the specified node on the document.  If the document is
633      * in a terminal state, the request will be attached to the terminal node.
634      */
635     public void adHocRouteDocumentToPrincipal(String actionRequested, String nodeName, String annotation, String principalId, String responsibilityDesc, boolean forceAction, String requestLabel) throws WorkflowException {
636     	createDocumentIfNeccessary();
637     	routeHeader = getWorkflowDocumentActions().adHocRouteDocumentToPrincipal(principalId, getRouteHeader(), actionRequested, nodeName, annotation, principalId, responsibilityDesc, forceAction, requestLabel);
638     	documentContentDirty = true;
639     }
640 
641     /**
642      * Sends an ad hoc request to the specified workgroup at the current active node on the document.  If the document is
643      * in a terminal state, the request will be attached to the terminal node.
644      */
645     public void adHocRouteDocumentToGroup(String actionRequested, String annotation, String groupId, String responsibilityDesc, boolean forceAction) throws WorkflowException {
646     	adHocRouteDocumentToGroup(actionRequested, null, annotation, groupId, responsibilityDesc, forceAction);
647     }
648 
649     /**
650      * Sends an ad hoc request to the specified workgroup at the specified node on the document.  If the document is
651      * in a terminal state, the request will be attached to the terminal node.
652      */
653     public void adHocRouteDocumentToGroup(String actionRequested, String nodeName, String annotation, String groupId, String responsibilityDesc, boolean forceAction) throws WorkflowException {
654     	adHocRouteDocumentToGroup(actionRequested, nodeName, annotation, groupId, responsibilityDesc, forceAction, null);
655     }
656 
657     /**
658      * Sends an ad hoc request to the specified workgroup at the specified node on the document.  If the document is
659      * in a terminal state, the request will be attached to the terminal node.
660      */
661     public void adHocRouteDocumentToGroup(String actionRequested, String nodeName, String annotation, String groupId, String responsibilityDesc, boolean forceAction, String requestLabel) throws WorkflowException {
662     	createDocumentIfNeccessary();
663     	routeHeader = getWorkflowDocumentActions().adHocRouteDocumentToGroup(principalId, getRouteHeader(), actionRequested, nodeName, annotation, groupId, responsibilityDesc, forceAction, requestLabel);
664     	documentContentDirty = true;
665     }
666 
667     /**
668      * Revokes AdHoc request(s) according to the given AdHocRevokeVO which is passed in.
669      *
670      * If a specific action request ID is specified on the revoke bean, and that ID is not a valid ID, this method should throw a
671      * WorkflowException.
672      * @param revoke AdHocRevokeVO
673      * @param annotation message to note for this action
674      * @throws WorkflowException if an error occurs revoking adhoc requests
675      * @see WorkflowDocumentActions#revokeAdHocRequests(UserIdDTO, RouteHeaderDTO, AdHocRevokeDTO, String)
676      */
677     public void revokeAdHocRequests(AdHocRevokeDTO revoke, String annotation) throws WorkflowException {
678     	if (getRouteHeader().getRouteHeaderId() == null) {
679     		throw new WorkflowException("Can't revoke request, the workflow document has not yet been created!");
680     	}
681     	createDocumentIfNeccessary();
682     	routeHeader = getWorkflowDocumentActions().revokeAdHocRequests(principalId, getRouteHeader(), revoke, annotation);
683     	documentContentDirty = true;
684     }
685 
686     /**
687      * Sets the title of the document, empty string if null is specified.
688      * @param title title of the document to set, or null
689      */
690     // WorkflowException is declared but not thrown...
691     public void setTitle(String title) throws WorkflowException {
692         if (title == null) {
693             title = "";
694         }
695         if (title.length() > KEWConstants.TITLE_MAX_LENGTH) {
696             title = title.substring(0, KEWConstants.TITLE_MAX_LENGTH);
697         }
698         getRouteHeader().setDocTitle(title);
699     }
700 
701     /**
702      * Returns the document type of the workflow document
703      * @return the document type of the workflow document
704      * @throws RuntimeException if document does not exist (is not yet created)
705      * @see RouteHeaderDTO#getDocTypeName()
706      */
707     public String getDocumentType() {
708         if (getRouteHeader() == null) {
709             // HACK: FIXME: we should probably proscribe, or at least handle consistently, these corner cases
710             // NPEs are not nice
711             throw new RuntimeException("No such document!");
712         }
713         return getRouteHeader().getDocTypeName();
714     }
715 
716     /**
717      * Returns whether an acknowledge is requested of the user for this document.  This is
718      * a convenience method that delegates to {@link RouteHeaderDTO#isAckRequested()}.
719      * @return whether an acknowledge is requested of the user for this document
720      * @see RouteHeaderDTO#isAckRequested()
721      */
722     public boolean isAcknowledgeRequested() {
723         return getRouteHeader().isAckRequested();
724     }
725 
726     /**
727      * Returns whether an approval is requested of the user for this document.  This is
728      * a convenience method that delegates to {@link RouteHeaderDTO#isApproveRequested()}.
729      * @return whether an approval is requested of the user for this document
730      * @see RouteHeaderDTO#isApproveRequested()
731      */
732     public boolean isApprovalRequested() {
733         return getRouteHeader().isApproveRequested();
734     }
735 
736     /**
737      * Returns whether a completion is requested of the user for this document.  This is
738      * a convenience method that delegates to {@link RouteHeaderDTO#isCompleteRequested()}.
739      * @return whether an approval is requested of the user for this document
740      * @see RouteHeaderDTO#isCompleteRequested()
741      */
742     public boolean isCompletionRequested() {
743         return getRouteHeader().isCompleteRequested();
744     }
745 
746     /**
747      * Returns whether an FYI is requested of the user for this document.  This is
748      * a convenience method that delegates to {@link RouteHeaderDTO#isFyiRequested()}.
749      * @return whether an FYI is requested of the user for this document
750      * @see RouteHeaderDTO#isFyiRequested()
751      */
752     public boolean isFYIRequested() {
753         return getRouteHeader().isFyiRequested();
754     }
755 
756     /**
757      * Returns whether the user can blanket approve the document
758      * @return whether the user can blanket approve the document
759      * @see RouteHeaderDTO#getValidActions()
760      */
761     public boolean isBlanketApproveCapable() {
762         // TODO delyea - refactor this to take into account non-initiator owned documents
763     	return getRouteHeader().getValidActions().contains(KEWConstants.ACTION_TAKEN_BLANKET_APPROVE_CD) && (isCompletionRequested() || isApprovalRequested() || stateIsInitiated());
764     }
765 
766     /**
767      * Returns whether the specified action code is valid for the current user and document
768      * @return whether the user can blanket approve the document
769      * @see RouteHeaderDTO#getValidActions()
770      */
771     public boolean isActionCodeValidForDocument(String actionTakenCode) {
772     	return getRouteHeader().getValidActions().contains(actionTakenCode);
773     }
774 
775     /**
776      * Performs the 'super-user-approve' action on the document this WorkflowDocument represents.  If this is a new document,
777      * the document is created first.
778      * @param annotation the message to log for the action
779      * @throws WorkflowException in case an error occurs super-user-approve-ing the document
780      * @see WorkflowDocumentActions#superUserApprove(UserIdDTO, RouteHeaderDTO, String)
781      */
782     public void superUserApprove(String annotation) throws WorkflowException {
783     	createDocumentIfNeccessary();
784     	routeHeader = getWorkflowDocumentActions().superUserApprove(principalId, getRouteHeader(), annotation);
785     	documentContentDirty = true;
786     }
787 
788     /**
789      * Performs the 'super-user-action-request-approve' action on the document this WorkflowDocument represents and the action
790      * request the id represents.
791      * @param actionRequestId the action request id for the action request the super user is approved
792      * @param annotation the message to log for the action
793      * @throws WorkflowException in case an error occurs super-user-action-request-approve-ing the document
794      * @see WorkflowDocumentActions#superUserApprove(UserIdVO, RouteHeaderVO, String)(UserIdVO, RouteHeaderVO, String)
795      */
796     public void superUserActionRequestApprove(Long actionRequestId, String annotation) throws WorkflowException {
797     	createDocumentIfNeccessary();
798     	routeHeader = getWorkflowDocumentActions().superUserActionRequestApprove(principalId, getRouteHeader(), actionRequestId, annotation);
799     	documentContentDirty = true;
800     }
801 
802     /**
803      * Performs the 'super-user-disapprove' action on the document this WorkflowDocument represents.  If this is a new document,
804      * the document is created first.
805      * @param annotation the message to log for the action
806      * @throws WorkflowException in case an error occurs super-user-disapprove-ing the document
807      * @see WorkflowDocumentActions#superUserDisapprove(UserIdDTO, RouteHeaderDTO, String)
808      */
809     public void superUserDisapprove(String annotation) throws WorkflowException {
810     	createDocumentIfNeccessary();
811     	routeHeader = getWorkflowDocumentActions().superUserDisapprove(principalId, getRouteHeader(), annotation);
812     	documentContentDirty = true;
813     }
814 
815     /**
816      * Performs the 'super-user-cancel' action on the document this WorkflowDocument represents.  If this is a new document,
817      * the document is created first.
818      * @param annotation the message to log for the action
819      * @throws WorkflowException in case an error occurs super-user-cancel-ing the document
820      * @see WorkflowDocumentActions#superUserCancel(UserIdDTO, RouteHeaderDTO, String)
821      */
822     public void superUserCancel(String annotation) throws WorkflowException {
823     	createDocumentIfNeccessary();
824     	routeHeader = getWorkflowDocumentActions().superUserCancel(principalId, getRouteHeader(), annotation);
825     	documentContentDirty = true;
826     }
827 
828     /**
829      * Returns whether the user is a super user on this document
830      * @return whether the user is a super user on this document
831      * @throws WorkflowException if an error occurs determining whether the user is a super user on this document
832      * @see WorkflowUtility#isSuperUserForDocumentType(UserIdDTO, Long)
833      */
834     public boolean isSuperUser() throws WorkflowException {
835     	createDocumentIfNeccessary();
836     	return getWorkflowUtility().isSuperUserForDocumentType(principalId, getRouteHeader().getDocTypeId());
837 	}
838 
839     /**
840      * Returns whether the user passed into WorkflowDocument at instantiation can route
841      * the document.
842 	 * @return if user passed into WorkflowDocument at instantiation can route
843 	 *         the document.
844 	 */
845     public boolean isRouteCapable() {
846         return isActionCodeValidForDocument(KEWConstants.ACTION_TAKEN_ROUTED_CD);
847     }
848 
849     /**
850      * Performs the 'clearFYI' action on the document this WorkflowDocument represents.  If this is a new document,
851      * the document is created first.
852      * @param annotation the message to log for the action
853      * @throws WorkflowException in case an error occurs clearing FYI on the document
854      * @see WorkflowDocumentActions#clearFYIDocument(UserIdDTO, RouteHeaderDTO)
855      */
856     public void clearFYI() throws WorkflowException {
857     	createDocumentIfNeccessary();
858     	getWorkflowDocumentActions().clearFYIDocument(principalId, getRouteHeader());
859     	documentContentDirty = true;
860     }
861 
862     /**
863      * Performs the 'complete' action on the document this WorkflowDocument represents.  If this is a new document,
864      * the document is created first.
865      * @param annotation the message to log for the action
866      * @throws WorkflowException in case an error occurs clearing completing the document
867      * @see WorkflowDocumentActions#completeDocument(UserIdDTO, RouteHeaderDTO, String)
868      */
869     public void complete(String annotation) throws WorkflowException {
870     	createDocumentIfNeccessary();
871     	routeHeader = getWorkflowDocumentActions().completeDocument(principalId, getRouteHeader(), annotation);
872     	documentContentDirty = true;
873     }
874 
875     /**
876      * Performs the 'logDocumentAction' action on the document this WorkflowDocument represents.  If this is a new document,
877      * the document is created first.  The 'logDocumentAction' simply logs a message on the document.
878      * @param annotation the message to log for the action
879      * @throws WorkflowException in case an error occurs logging a document action on the document
880      * @see WorkflowDocumentActions#logDocumentAction(UserIdDTO, RouteHeaderDTO, String)
881      */
882     public void logDocumentAction(String annotation) throws WorkflowException {
883     	createDocumentIfNeccessary();
884     	getWorkflowDocumentActions().logDocumentAction(principalId, getRouteHeader(), annotation);
885     	documentContentDirty = true;
886     }
887 
888     /**
889      * Indicates if the document is in the initiated state or not.
890      *
891      * @return true if in the specified state
892      */
893     public boolean stateIsInitiated() {
894         return KEWConstants.ROUTE_HEADER_INITIATED_CD.equals(getRouteHeader().getDocRouteStatus());
895     }
896 
897     /**
898      * Indicates if the document is in the saved state or not.
899      *
900      * @return true if in the specified state
901      */
902     public boolean stateIsSaved() {
903         return KEWConstants.ROUTE_HEADER_SAVED_CD.equals(getRouteHeader().getDocRouteStatus());
904     }
905 
906     /**
907      * Indicates if the document is in the enroute state or not.
908      *
909      * @return true if in the specified state
910      */
911     public boolean stateIsEnroute() {
912         return KEWConstants.ROUTE_HEADER_ENROUTE_CD.equals(getRouteHeader().getDocRouteStatus());
913     }
914 
915     /**
916      * Indicates if the document is in the exception state or not.
917      *
918      * @return true if in the specified state
919      */
920     public boolean stateIsException() {
921         return KEWConstants.ROUTE_HEADER_EXCEPTION_CD.equals(getRouteHeader().getDocRouteStatus());
922     }
923 
924     /**
925      * Indicates if the document is in the canceled state or not.
926      *
927      * @return true if in the specified state
928      */
929     public boolean stateIsCanceled() {
930         return KEWConstants.ROUTE_HEADER_CANCEL_CD.equals(getRouteHeader().getDocRouteStatus());
931     }
932 
933     /**
934      * Indicates if the document is in the disapproved state or not.
935      *
936      * @return true if in the specified state
937      */
938     public boolean stateIsDisapproved() {
939         return KEWConstants.ROUTE_HEADER_DISAPPROVED_CD.equals(getRouteHeader().getDocRouteStatus());
940     }
941 
942     /**
943      * Indicates if the document is in the approved state or not. Will answer true is document is in Processed or Finalized state.
944      *
945      * @return true if in the specified state
946      */
947     public boolean stateIsApproved() {
948         return KEWConstants.ROUTE_HEADER_APPROVED_CD.equals(getRouteHeader().getDocRouteStatus()) || stateIsProcessed() || stateIsFinal();
949     }
950 
951     /**
952      * Indicates if the document is in the processed state or not.
953      *
954      * @return true if in the specified state
955      */
956     public boolean stateIsProcessed() {
957         return KEWConstants.ROUTE_HEADER_PROCESSED_CD.equals(getRouteHeader().getDocRouteStatus());
958     }
959 
960     /**
961      * Indicates if the document is in the final state or not.
962      *
963      * @return true if in the specified state
964      */
965     public boolean stateIsFinal() {
966         return KEWConstants.ROUTE_HEADER_FINAL_CD.equals(getRouteHeader().getDocRouteStatus());
967     }
968 
969     /**
970      * Returns the display value of the current document status
971      * @return the display value of the current document status
972      */
973     public String getStatusDisplayValue() {
974         return (String) KEWConstants.DOCUMENT_STATUSES.get(getRouteHeader().getDocRouteStatus());
975     }
976 
977     /**
978      * Returns the principalId with which this WorkflowDocument was constructed
979      * @return the principalId with which this WorkflowDocument was constructed
980      */
981     public String getPrincipalId() {
982         return principalId;
983     }
984 
985     /**
986      * Sets the principalId under which actions against this document should be taken
987      * @param principalId principalId under which actions against this document should be taken
988      */
989     public void setPrincipalId(String principalId) {
990         this.principalId = principalId;
991     }
992 
993     /**
994      * Checks if the document has been created or not (i.e. has a route header id or not) and issues
995      * a call to the server to create the document if it has not yet been created.
996      *
997      * Also checks if the document content has been updated and saves it if it has.
998      */
999     private void createDocumentIfNeccessary() throws WorkflowException {
1000     	if (getRouteHeader().getRouteHeaderId() == null) {
1001     		routeHeader = getWorkflowDocumentActions().createDocument(principalId, getRouteHeader());
1002     	}
1003     	if (documentContent != null && documentContent.isModified()) {
1004     		saveDocumentContent(documentContent);
1005     	}
1006     }
1007 
1008     /**
1009      * Like handleException except it returns a RuntimeException.
1010      */
1011     private RuntimeException handleExceptionAsRuntime(Exception e) {
1012     	if (e instanceof RuntimeException) {
1013     		return (RuntimeException)e;
1014     	}
1015     	return new WorkflowRuntimeException(e);
1016     }
1017 
1018     // WORKFLOW 2.1: new methods
1019 
1020     /**
1021      * Performs the 'blanketApprove' action on the document this WorkflowDocument represents.  If this is a new document,
1022      * the document is created first.
1023      * @param annotation the message to log for the action
1024      * @param nodeName the extent to which to blanket approve; blanket approval will stop at this node
1025      * @throws WorkflowException in case an error occurs blanket-approving the document
1026      * @see WorkflowDocumentActions#blanketApprovalToNodes(UserIdDTO, RouteHeaderDTO, String, String[])
1027      */
1028     public void blanketApprove(String annotation, String nodeName) throws WorkflowException {
1029         blanketApprove(annotation, (nodeName == null ? null : new String[] { nodeName }));
1030     }
1031 
1032     /**
1033      * Performs the 'blanketApprove' action on the document this WorkflowDocument represents.  If this is a new document,
1034      * the document is created first.
1035      * @param annotation the message to log for the action
1036      * @param nodeNames the nodes at which blanket approval will stop (in case the blanket approval traverses a split, in which case there may be multiple "active" nodes)
1037      * @throws WorkflowException in case an error occurs blanket-approving the document
1038      * @see WorkflowDocumentActions#blanketApprovalToNodes(UserIdDTO, RouteHeaderDTO, String, String[])
1039      */
1040     public void blanketApprove(String annotation, String[] nodeNames) throws WorkflowException {
1041     	createDocumentIfNeccessary();
1042     	routeHeader = getWorkflowDocumentActions().blanketApprovalToNodes(principalId, getRouteHeader(), annotation, nodeNames);
1043     	documentContentDirty = true;
1044     }
1045 
1046     /**
1047      * The user taking action removes the action items for this workgroup and document from all other
1048      * group members' action lists.   If this is a new document, the document is created first.
1049      *
1050      * @param annotation the message to log for the action
1051      * @param workgroupId the workgroup on which to take authority
1052      * @throws WorkflowException user taking action is not in workgroup
1053      */
1054     public void takeGroupAuthority(String annotation, String groupId) throws WorkflowException {
1055     	createDocumentIfNeccessary();
1056     	routeHeader = getWorkflowDocumentActions().takeGroupAuthority(principalId, getRouteHeader(), groupId, annotation);
1057     	documentContentDirty = true;
1058     }
1059 
1060     /**
1061      * The user that took the group authority is putting the action items back in the other users action lists.
1062      * If this is a new document, the document is created first.
1063      *
1064      * @param annotation the message to log for the action
1065      * @param workgroupId the workgroup on which to take authority
1066      * @throws WorkflowException user taking action is not in workgroup or did not take workgroup authority
1067      */
1068     public void releaseGroupAuthority(String annotation, String groupId) throws WorkflowException {
1069     	createDocumentIfNeccessary();
1070     	routeHeader = getWorkflowDocumentActions().releaseGroupAuthority(principalId, getRouteHeader(), groupId, annotation);
1071     	documentContentDirty = true;
1072     }
1073 
1074     /**
1075      * Returns names of all active nodes the document is currently at.
1076      *
1077      * @return names of all active nodes the document is currently at.
1078      * @throws WorkflowException if there is an error obtaining the currently active nodes on the document
1079      * @see WorkflowUtility#getActiveNodeInstances(Long)
1080      */
1081     public String[] getNodeNames() throws WorkflowException {
1082     	RouteNodeInstanceDTO[] activeNodeInstances = getWorkflowUtility().getActiveNodeInstances(getRouteHeaderId());
1083     	String[] nodeNames = new String[(activeNodeInstances == null ? 0 : activeNodeInstances.length)];
1084     	for (int index = 0; index < activeNodeInstances.length; index++) {
1085     		nodeNames[index] = activeNodeInstances[index].getName();
1086     	}
1087     	return nodeNames;
1088     }
1089 
1090     /**
1091      * Performs the 'returnToPrevious' action on the document this WorkflowDocument represents.  If this is a new document,
1092      * the document is created first.
1093      * @param annotation the message to log for the action
1094      * @param nodeName the node to return to
1095      * @throws WorkflowException in case an error occurs returning to previous node
1096      * @see WorkflowDocumentActions#returnDocumentToPreviousNode(UserIdDTO, RouteHeaderDTO, ReturnPointDTO, String)
1097      */
1098     public void returnToPreviousNode(String annotation, String nodeName) throws WorkflowException {
1099         ReturnPointDTO returnPoint = new ReturnPointDTO(nodeName);
1100         returnToPreviousNode(annotation, returnPoint);
1101     }
1102 
1103     /**
1104      * Performs the 'returnToPrevious' action on the document this WorkflowDocument represents.  If this is a new document,
1105      * the document is created first.
1106      * @param annotation the message to log for the action
1107      * @param ReturnPointDTO the node to return to
1108      * @throws WorkflowException in case an error occurs returning to previous node
1109      * @see WorkflowDocumentActions#returnDocumentToPreviousNode(UserIdDTO, RouteHeaderDTO, ReturnPointDTO, String)
1110      */
1111     public void returnToPreviousNode(String annotation, ReturnPointDTO returnPoint) throws WorkflowException {
1112     	createDocumentIfNeccessary();
1113     	routeHeader = getWorkflowDocumentActions().returnDocumentToPreviousNode(principalId, getRouteHeader(), returnPoint, annotation);
1114     	documentContentDirty = true;
1115     }
1116 
1117     /**
1118      * Moves the document from a current node in it's route to another node.  If this is a new document,
1119      * the document is created first.
1120      * @param MovePointDTO VO representing the node at which to start, and the number of steps to move (negative steps is reverse)
1121      * @param annotation the message to log for the action
1122      * @throws WorkflowException in case an error occurs moving the document
1123      * @see WorkflowDocumentActions#moveDocument(UserIdDTO, RouteHeaderDTO, MovePointDTO, String)
1124      */
1125     public void moveDocument(MovePointDTO movePoint, String annotation) throws WorkflowException {
1126     	createDocumentIfNeccessary();
1127     	routeHeader =  getWorkflowDocumentActions().moveDocument(principalId, getRouteHeader(), movePoint, annotation);
1128     	documentContentDirty = true;
1129     }
1130 
1131     /**
1132      * Returns the route node instances that have been created so far during the life of this document.  This includes
1133      * all previous instances which have already been processed and are no longer active.
1134      * @return the route node instances that have been created so far during the life of this document
1135      * @throws WorkflowException if there is an error getting the route node instances for the document
1136      * @see WorkflowUtility#getDocumentRouteNodeInstances(Long)
1137      */
1138     public RouteNodeInstanceDTO[] getRouteNodeInstances() throws WorkflowException {
1139     	return getWorkflowUtility().getDocumentRouteNodeInstances(getRouteHeaderId());
1140     }
1141 
1142     /**
1143      * Returns Array of Route Nodes Names that can be safely returned to using the 'returnToPreviousXXX' methods.
1144      * Names are sorted in reverse chronological order.
1145      *
1146      * @return array of Route Nodes Names that can be safely returned to using the 'returnToPreviousXXX' methods
1147      * @throws WorkflowException if an error occurs obtaining the names of the previous route nodes for this document
1148      * @see WorkflowUtility#getPreviousRouteNodeNames(Long)
1149      */
1150     public String[] getPreviousNodeNames() throws WorkflowException {
1151     	return getWorkflowUtility().getPreviousRouteNodeNames(getRouteHeaderId());
1152 	}
1153 
1154     /**
1155      * Returns a document detail VO representing the route header along with action requests, actions taken,
1156      * and route node instances.
1157      * @return Returns a document detail VO representing the route header along with action requests, actions taken, and route node instances.
1158      * @throws WorkflowException
1159      */
1160     public DocumentDetailDTO getDetail() throws WorkflowException {
1161     	return getWorkflowUtility().getDocumentDetail(getRouteHeaderId());
1162     }
1163 
1164     /**
1165      * Saves the given DocumentContentVO for this document.
1166      * @param documentContent document content VO to store for this document
1167      * @since 2.3
1168      * @see WorkflowDocumentActions#saveDocumentContent(DocumentContentDTO)
1169      */
1170     public DocumentContentDTO saveDocumentContent(DocumentContentDTO documentContent) throws WorkflowException {
1171     	if (documentContent.getRouteHeaderId() == null) {
1172     		throw new WorkflowException("Document Content does not have a valid document ID.");
1173     	}
1174     	// important to check directly against getRouteHeader().getRouteHeaderId() instead of just getRouteHeaderId() because saveDocumentContent
1175     	// is called from createDocumentIfNeccessary which is called from getRouteHeaderId().  If that method was used, we would have an infinite loop.
1176     	if (!documentContent.getRouteHeaderId().equals(getRouteHeader().getRouteHeaderId())) {
1177     		throw new WorkflowException("Attempted to save content on this document with an invalid document id of " + documentContent.getRouteHeaderId());
1178     	}
1179     	DocumentContentDTO newDocumentContent = getWorkflowDocumentActions().saveDocumentContent(documentContent);
1180     	this.documentContent = new ModifiableDocumentContentDTO(newDocumentContent);
1181     	documentContentDirty = false;
1182     	return this.documentContent;
1183     }
1184     
1185     public void placeInExceptionRouting(String annotation) throws WorkflowException {
1186     	createDocumentIfNeccessary();
1187     	routeHeader = getWorkflowDocumentActions().placeInExceptionRouting(principalId, getRouteHeader(), annotation);
1188     	documentContentDirty = true;
1189     }
1190 
1191 
1192 
1193     // DEPRECATED: as of Workflow 2.1
1194 
1195     /**
1196      * @deprecated use blanketApprove(String annotation, String nodeName) instead
1197      */
1198     public void blanketApprove(String annotation, Integer routeLevel) throws WorkflowException {
1199     	createDocumentIfNeccessary();
1200     	routeHeader = getWorkflowDocumentActions().blanketApproval(principalId, getRouteHeader(), annotation, routeLevel);
1201     	documentContentDirty = true;
1202     }
1203 
1204     /**
1205      * @deprecated use getNodeNames() instead
1206      */
1207     public Integer getDocRouteLevel() {
1208         return routeHeader.getDocRouteLevel();
1209     }
1210 
1211     /**
1212      * @deprecated use returnToPreviousNode(String annotation, String nodeName) instead
1213      */
1214     public void returnToPreviousRouteLevel(String annotation, Integer destRouteLevel) throws WorkflowException {
1215     	createDocumentIfNeccessary();
1216     	getWorkflowDocumentActions().returnDocumentToPreviousRouteLevel(principalId, getRouteHeader(), destRouteLevel, annotation);
1217     	documentContentDirty = true;
1218     }
1219 
1220     /**
1221      * Returns a list of NoteVO representing the notes on the document
1222      * @return a list of NoteVO representing the notes on the document
1223      * @see RouteHeaderDTO#getNotes()
1224      */
1225     public List<NoteDTO> getNoteList(){
1226     	List<NoteDTO> notesList = new ArrayList<NoteDTO>();
1227     	NoteDTO[] notes = routeHeader.getNotes();
1228     	if (notes != null){
1229 	    	for (int i=0; i<notes.length; i++){
1230 	    		if (! isDeletedNote(notes[i])){
1231 	    			notesList.add(notes[i]);
1232 	    		}
1233 	    	}
1234     	}
1235     	return notesList;
1236     }
1237 
1238     /**
1239      * Deletes a note from the document.  The deletion is deferred until the next time the document is committed (via an action).
1240      * @param noteVO the note to remove from the document
1241      */
1242     public void deleteNote(NoteDTO noteVO){
1243     	if (noteVO != null && noteVO.getNoteId()!=null){
1244     		NoteDTO noteToDelete = new NoteDTO();
1245     		noteToDelete.setNoteId(new Long(noteVO.getNoteId().longValue()));
1246     		/*noteToDelete.setRouteHeaderId(noteVO.getRouteHeaderId());
1247     		noteToDelete.setNoteAuthorWorkflowId(noteVO.getNoteAuthorWorkflowId());
1248     		noteToDelete.setNoteCreateDate(noteVO.getNoteCreateDate());
1249     		noteToDelete.setNoteText(noteVO.getNoteText());
1250     		noteToDelete.setLockVerNbr(noteVO.getLockVerNbr());*/
1251     		increaseNotesToDeleteArraySizeByOne();
1252     		routeHeader.getNotesToDelete()[routeHeader.getNotesToDelete().length - 1]=noteToDelete;
1253     	}
1254     }
1255 
1256     /**
1257      * Updates the note of the same note id, on the document. The update is deferred until the next time the document is committed (via an action).
1258      * @param noteVO the note to update
1259      */
1260     public void updateNote (NoteDTO noteVO){
1261     	boolean isUpdateNote = false;
1262     	if (noteVO != null){
1263     		NoteDTO[] notes = routeHeader.getNotes();
1264     		NoteDTO  copyNote = new NoteDTO();
1265 			if (noteVO.getNoteId() != null){
1266 				copyNote.setNoteId(new Long(noteVO.getNoteId().longValue()));
1267 			}
1268 
1269 			if (noteVO.getRouteHeaderId() != null){
1270 				copyNote.setRouteHeaderId(new Long(noteVO.getRouteHeaderId().longValue()));
1271 			} else {
1272 				copyNote.setRouteHeaderId(routeHeader.getRouteHeaderId());
1273 			}
1274 
1275 			if (noteVO.getNoteAuthorWorkflowId() != null){
1276 				copyNote.setNoteAuthorWorkflowId(new String(noteVO.getNoteAuthorWorkflowId()));
1277 			} else {
1278 			    copyNote.setNoteAuthorWorkflowId(principalId.toString())	;
1279 			}
1280 
1281 			if (noteVO.getNoteCreateDate() != null){
1282 				Calendar cal = Calendar.getInstance();
1283 				cal.setTimeInMillis(noteVO.getNoteCreateDate().getTimeInMillis());
1284 				copyNote.setNoteCreateDate(cal);
1285 			} else {
1286 				copyNote.setNoteCreateDate(Calendar.getInstance());
1287 			}
1288 
1289 			if (noteVO.getNoteText() != null){
1290 				copyNote.setNoteText(new String(noteVO.getNoteText()));
1291 			}
1292 			if (noteVO.getLockVerNbr() != null){
1293 				copyNote.setLockVerNbr(new Integer(noteVO.getLockVerNbr().intValue()));
1294 			}
1295     		if (notes != null){
1296 	    		for (int i=0; i<notes.length; i++){
1297 	    			if (notes[i].getNoteId()!= null && notes[i].getNoteId().equals(copyNote.getNoteId())){
1298 	    				notes[i] = copyNote;
1299 	    				isUpdateNote = true;
1300 	    				break;
1301 	    			}
1302 	    		}
1303     		}
1304     		// add new note to the notes array
1305     		if (! isUpdateNote){
1306 	    		copyNote.setNoteId(null);
1307 	    		increaseNotesArraySizeByOne();
1308 	    		routeHeader.getNotes()[routeHeader.getNotes().length-1]= copyNote;
1309     		}
1310     	}
1311     }
1312 
1313     /**
1314      * Sets a variable on the document.  The assignment is deferred until the next time the document is committed (via an action).
1315      * @param name name of the variable
1316      * @param value value of the variable
1317      */
1318     public void setVariable(String name, String value) throws WorkflowException {
1319     	createDocumentIfNeccessary();
1320         getRouteHeader().setVariable(name, value);
1321     }
1322 
1323     /**
1324      * Gets the value of a variable on the document, creating the document first if it does not exist.
1325      * @param name variable name
1326      * @return variable value
1327      */
1328     public String getVariable(String name) throws WorkflowException {
1329     	createDocumentIfNeccessary();
1330         return getRouteHeader().getVariable(name);
1331     }
1332 
1333     /**
1334      *
1335      * Tells workflow that the current the document is constructed as will receive all future requests routed to them
1336      * disregarding any force action flags set on the action request.  Uses the setVariable method behind the seens so
1337      * an action needs taken on the document to set this state on the document.
1338      *
1339      * @throws WorkflowException
1340      */
1341     public void setReceiveFutureRequests() throws WorkflowException {
1342         WorkflowUtility workflowUtility = getWorkflowUtility();
1343         this.setVariable(workflowUtility.getFutureRequestsKey(principalId), workflowUtility.getReceiveFutureRequestsValue());
1344     }
1345 
1346     /**
1347      * Tell workflow that the current document is constructed as will not receive any future requests routed to them
1348      * disregarding any force action flags set on action requests.  Uses the setVariable method behind the scenes so
1349      * an action needs taken on the document to set this state on the document.
1350      *
1351      * @throws WorkflowException
1352      */
1353     public void setDoNotReceiveFutureRequests() throws WorkflowException {
1354         WorkflowUtility workflowUtility = getWorkflowUtility();
1355         this.setVariable(workflowUtility.getFutureRequestsKey(principalId), workflowUtility.getDoNotReceiveFutureRequestsValue());
1356     }
1357 
1358     /**
1359      * Clears any state set on the document regarding a user receiving or not receiving action requests.  Uses the setVariable method
1360      * behind the seens so an action needs taken on the document to set this state on the document.
1361      *
1362      * @throws WorkflowException
1363      */
1364     public void setClearFutureRequests() throws WorkflowException {
1365         WorkflowUtility workflowUtility = getWorkflowUtility();
1366         this.setVariable(workflowUtility.getFutureRequestsKey(principalId), workflowUtility.getClearFutureRequestsValue());
1367     }
1368 
1369     /**
1370      * Deletes the note of with the same id as that of the argument on the document.
1371      * @param noteVO the note to test for deletion
1372      * @return whether the note is already marked for deletion.
1373      */
1374     private boolean isDeletedNote(NoteDTO noteVO) {
1375     	NoteDTO[] notesToDelete = routeHeader.getNotesToDelete();
1376     	if (notesToDelete != null){
1377     		for (int i=0; i<notesToDelete.length; i++){
1378     			if (notesToDelete[i].getNoteId().equals(noteVO.getNoteId())){
1379     				return true;
1380     			}
1381     		}
1382     	}
1383     	return false;
1384     }
1385 
1386     /**
1387      * Increases the size of the routeHeader notes VO array
1388      */
1389    private void increaseNotesArraySizeByOne() {
1390 	   NoteDTO[] tempArray;
1391 	   NoteDTO[] notes = routeHeader.getNotes();
1392 	   if (notes == null){
1393 		   tempArray = new NoteDTO[1];
1394 	   } else {
1395 		   tempArray = new NoteDTO[notes.length + 1];
1396 		   for (int i=0; i<notes.length; i++){
1397 			   tempArray[i] = notes[i];
1398 		   }
1399 	   }
1400 	   routeHeader.setNotes(tempArray);
1401    }
1402 
1403    /**
1404     * Increases the size of the routeHeader notesToDelete VO array
1405     */
1406    private void increaseNotesToDeleteArraySizeByOne() {
1407 	   NoteDTO[] tempArray;
1408 	   NoteDTO[] notesToDelete = routeHeader.getNotesToDelete();
1409 	   if (notesToDelete == null){
1410 		   tempArray = new NoteDTO[1];
1411 	   } else {
1412 		   tempArray = new NoteDTO[notesToDelete.length + 1];
1413 		   for (int i=0; i<notesToDelete.length; i++){
1414 			   tempArray[i] = notesToDelete[i];
1415 		   }
1416 	   }
1417 	   routeHeader.setNotesToDelete(tempArray);
1418    }
1419    
1420    //add 1 link between 2 docs by DTO, double link added
1421    public void addLinkedDocument(DocumentLinkDTO docLinkVO) throws WorkflowException{
1422 	   try{
1423 		   if(DocumentLinkDTO.checkDocLink(docLinkVO))
1424 			   getWorkflowUtility().addDocumentLink(docLinkVO);
1425 	   }
1426 	   catch(Exception e){
1427 		   throw handleExceptionAsRuntime(e); 
1428 	   } 
1429    }
1430    
1431    //get link from orgn doc to a specifc doc
1432    public DocumentLinkDTO getLinkedDocument(DocumentLinkDTO docLinkVO) throws WorkflowException{
1433 	   try{
1434 		   if(DocumentLinkDTO.checkDocLink(docLinkVO))
1435 			   return getWorkflowUtility().getLinkedDocument(docLinkVO);
1436 		   else
1437 			   return null;
1438 	   }
1439 	   catch(Exception e){
1440 		   throw handleExceptionAsRuntime(e); 
1441 	   }
1442    }
1443    
1444    //get all links to orgn doc
1445    public List<DocumentLinkDTO> getLinkedDocumentsByDocId(Long id) throws WorkflowException{
1446 	   if(id == null)
1447 		   throw new WorkflowException("doc id is null");
1448 	   try{   
1449 		   return getWorkflowUtility().getLinkedDocumentsByDocId(id);
1450 	   } 
1451 	   catch (Exception e) {
1452 		   throw handleExceptionAsRuntime(e);
1453 	   }
1454    }
1455    
1456    //remove all links from orgn: double links removed
1457    public void removeLinkedDocuments(Long docId) throws WorkflowException{
1458 	   
1459 	   if(docId == null)
1460 		   throw new WorkflowException("doc id is null");
1461 	   
1462 	   try{   
1463 		   getWorkflowUtility().deleteDocumentLinksByDocId(docId);
1464 	   } 
1465 	   catch (Exception e) {
1466 		   throw handleExceptionAsRuntime(e);
1467 	   }
1468    }
1469    
1470    //remove link between 2 docs, double link removed
1471    public void removeLinkedDocument(DocumentLinkDTO docLinkVO) throws WorkflowException{
1472 	   
1473 	   try{
1474 		   if(DocumentLinkDTO.checkDocLink(docLinkVO))
1475 			   getWorkflowUtility().deleteDocumentLink(docLinkVO);
1476 	   }
1477 	   catch(Exception e){
1478 		   throw handleExceptionAsRuntime(e); 
1479 	   } 
1480    }
1481 
1482 }