View Javadoc
1   /**
2    * Copyright 2005-2014 The Kuali Foundation
3    *
4    * Licensed under the Educational Community License, Version 2.0 (the "License");
5    * you may not use this file except in compliance with the License.
6    * You may obtain a copy of the License at
7    *
8    * http://www.opensource.org/licenses/ecl2.php
9    *
10   * Unless required by applicable law or agreed to in writing, software
11   * distributed under the License is distributed on an "AS IS" BASIS,
12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   * See the License for the specific language governing permissions and
14   * limitations under the License.
15   */
16  package org.kuali.rice.kew.postprocessor;
17  
18  import static org.junit.Assert.assertEquals;
19  import static org.junit.Assert.assertNotNull;
20  import static org.junit.Assert.assertNull;
21  import static org.junit.Assert.assertTrue;
22  import static org.junit.Assert.fail;
23  
24  import java.util.ArrayList;
25  import java.util.List;
26  
27  import org.apache.commons.lang.StringUtils;
28  import org.custommonkey.xmlunit.XMLAssert;
29  import org.custommonkey.xmlunit.XMLUnit;
30  import org.junit.Test;
31  import org.kuali.rice.kew.api.WorkflowDocument;
32  import org.kuali.rice.kew.api.WorkflowDocumentFactory;
33  import org.kuali.rice.kew.api.action.ActionRequestType;
34  import org.kuali.rice.kew.doctype.bo.DocumentType;
35  import org.kuali.rice.kew.framework.postprocessor.AfterProcessEvent;
36  import org.kuali.rice.kew.framework.postprocessor.DocumentLockingEvent;
37  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteLevelChange;
38  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
39  import org.kuali.rice.kew.framework.postprocessor.ProcessDocReport;
40  import org.kuali.rice.kew.routeheader.DocumentRouteHeaderValue;
41  import org.kuali.rice.kew.service.KEWServiceLocator;
42  import org.kuali.rice.kew.test.KEWTestCase;
43  import org.kuali.rice.kew.api.KewApiConstants;
44  import org.kuali.rice.test.BaselineTestCase;
45  import org.springframework.transaction.TransactionStatus;
46  import org.springframework.transaction.support.TransactionCallback;
47  import org.springframework.transaction.support.TransactionTemplate;
48  
49  @BaselineTestCase.BaselineMode(BaselineTestCase.Mode.NONE)
50  public class PostProcessorTest extends KEWTestCase {
51  
52  	private static final String APPLICATION_CONTENT = "<some><application>content</application></some>";
53  	private static final String DOC_TITLE = "The Doc Title";
54  	
55  	protected void loadTestData() throws Exception {
56          loadXmlFile("PostProcessorConfig.xml");
57      }
58  	
59  	/**
60  	 * Tests that modifying a document in the post processor works.  This test will do a few things:
61  	 * 
62  	 * 1) Change the document content in the post processor
63  	 * 2) Send an app specific FYI request to the initiator of the document
64  	 * 3) Modify the document title.
65  	 * 
66  	 * This test is meant to expose the bug KULWF-668 where it appears an OptimisticLockException is
67  	 * being thrown after returning from the EPIC post processor.
68  	 */
69  	@Test public void testModifyDocumentInPostProcessor() throws Exception {
70          XMLUnit.setIgnoreWhitespace(true);
71  		WorkflowDocument document = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("ewestfal"), "testModifyDocumentInPostProcessor");
72  		document.saveDocument("");
73          assertEquals("application content should be empty initially", "", document.getApplicationContent());
74  		assertTrue("Doc title should be empty initially", StringUtils.isBlank(document.getTitle()));
75  
76          document.adHocToPrincipal(ActionRequestType.APPROVE, "AdHoc", "", "2002", "", true);
77  		document.complete("");
78          document = WorkflowDocumentFactory.loadDocument("2002", document.getDocumentId());
79  
80          // now approve the document, it should through a 2 nodes, then go PROCESSED then FINAL
81          document.approve("");
82  
83          assertEquals("Should have transitioned nodes twice", 2, DocumentModifyingPostProcessor.levelChanges);
84  		assertTrue("SHould have called the processed status change", DocumentModifyingPostProcessor.processedChange);
85  		assertTrue("Document should be final.", document.isFinal());
86  		XMLAssert.assertXMLEqual("Application content should have been sucessfully modified.", APPLICATION_CONTENT, document.getApplicationContent());
87  				
88  		// check that the title was modified successfully
89  		assertEquals("Wrong doc title", DOC_TITLE, document.getTitle());
90  		
91  		// check that the document we routed from the post processor exists
92  		assertNotNull("SHould have routed a document from the post processor.", DocumentModifyingPostProcessor.routedDocumentId);
93  		document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("ewestfal"), DocumentModifyingPostProcessor.routedDocumentId);
94  		assertTrue("document should be enroute", document.isEnroute());
95  		assertEquals("Document should have 1 pending request.", 1, KEWServiceLocator.getActionRequestService().findPendingByDoc(document.getDocumentId()).size());
96  		assertTrue("ewestfal should have an approve request.", document.isApprovalRequested());
97  		document.approve("");
98  		assertTrue("Document should be final.", document.isFinal());
99  	}
100 	/**
101      * Tests that modifying a document in the post processor works.  This test will do a few things:
102      * 
103      * 1) Change the document content in the post processor
104      * 2) Send an app specific FYI request to the initiator of the document
105      * 3) Modify the document title.
106      * 
107      * This test is meant to test that an empty post processor works.  The empty post processor should be handled as a DefaultPostProcessor.
108      */
109     
110 	@Test public void testEmptyPostProcessor() throws Exception {
111         WorkflowDocument document = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("ewestfal"), "testEmptyPostProcessor");
112         document.saveDocument("");
113         assertEquals("application content should be empty initially", "", document.getApplicationContent());
114         assertTrue("Doc title should be empty initially", StringUtils.isBlank(document.getTitle()));
115         
116         assertTrue("Document should be final.", document.isFinal());
117                
118         DocumentType testEmptyDocType = KEWServiceLocator.getDocumentTypeService().findByName("testEmptyPostProcessor");
119         assertTrue("Post Processor should be set to 'none'",  StringUtils.equals("none", testEmptyDocType.getPostProcessorName()));
120         assertTrue("Post Processor should be of type DefaultPostProcessor", testEmptyDocType.getPostProcessor() instanceof org.kuali.rice.kew.postprocessor.DefaultPostProcessor);
121     }
122     
123 	private static boolean shouldReturnDocumentIdsToLock = false;
124 	private static String documentAId = null;
125 	private static String documentBId = null;
126 	private static UpdateDocumentThread updateDocumentThread = null;
127 	
128 	protected String getPrincipalIdForName(String principalName) {
129         return KEWServiceLocator.getIdentityHelperService()
130                 .getIdForPrincipalName(principalName);
131     }
132 	/**
133 	 * Tests the locking of additional documents from the Post Processor.
134 	 * 
135 	 * @author Kuali Rice Team (rice.collab@kuali.org)
136 	 */
137 	@Test public void RtestGetDocumentIdsToLock() throws Exception {
138 		
139 		/**
140 		 * Let's recreate the original optimistic lock scenario that caused this issue to crop up, essentially:
141 		 * 
142 		 * 1) Thread one locks and processes document A in the workflow engine
143 		 * 2) Thread one loads document B
144 		 * 3) Thread two locks and processes document B from document A's post processor, doing an update which increments the version number of document B
145 		 * 4) Thread A attempts to update document B and gets an optimistic lock exception 
146 		 */
147 		
148 		WorkflowDocument documentB = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("ewestfal"), "TestDocumentType");
149 		documentB.saveDocument("");
150 		documentBId = documentB.getDocumentId();
151 		updateDocumentThread = new UpdateDocumentThread(documentBId);
152 		
153 		// this is the document with the post processor
154 		WorkflowDocument documentA = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("ewestfal"), "testGetDocumentIdsToLock");
155 		documentA.adHocToPrincipal(ActionRequestType.APPROVE, "", getPrincipalIdForName("rkirkend"), "", true);
156 		
157 		try {
158 			documentA.route(""); // this should trigger our post processor and our optimistic lock exception
159 			fail("An exception should have been thrown as the result of an optimistic lock!");
160 		} catch (Exception e) {
161 			e.printStackTrace();
162 		}
163 
164 		/**
165 		 * Now let's try the same thing again, this time returning document B's id as a document to lock, the error should not happen this time
166 		 */
167 		
168 		shouldReturnDocumentIdsToLock = true;
169 		
170 		documentB = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("ewestfal"), "TestDocumentType");
171 		documentB.saveDocument("");
172 		documentBId = documentB.getDocumentId();
173 		updateDocumentThread = new UpdateDocumentThread(documentBId);
174 		
175 		// this is the document with the post processor
176 		documentA = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("ewestfal"), "testGetDocumentIdsToLock");
177 		documentA.adHocToPrincipal(ActionRequestType.APPROVE, "", getPrincipalIdForName("rkirkend"), "", true);
178 		
179 		documentA.route(""); // this should trigger our post processor and our optimistic lock exception
180 		documentA = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("rkirkend"), documentA.getDocumentId());
181 		assertTrue("rkirkend should have approve request", documentA.isApprovalRequested());
182 		
183 	}
184 	
185 	public static class DocumentModifyingPostProcessor extends DefaultPostProcessor {
186 
187 		public static boolean processedChange = false;
188 		public static int levelChanges = 0;
189 		public static String routedDocumentId;
190 		
191 		protected String getPrincipalIdForName(String principalName) {
192 	        return KEWServiceLocator.getIdentityHelperService()
193 	                .getIdForPrincipalName(principalName);
194 	    }
195 		
196 		public ProcessDocReport doRouteStatusChange(DocumentRouteStatusChange statusChangeEvent) throws Exception {
197 			if (KewApiConstants.ROUTE_HEADER_PROCESSED_CD.equals(statusChangeEvent.getNewRouteStatus())) {
198 				WorkflowDocument document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("ewestfal"), statusChangeEvent.getDocumentId());
199 				document.setApplicationContent(APPLICATION_CONTENT);
200 				document.setTitle(DOC_TITLE);
201 				document.saveDocumentData();
202 				// now route another document from the post processor, sending it an adhoc request
203 				WorkflowDocument ppDocument = WorkflowDocumentFactory.createDocument(getPrincipalIdForName("user1"), "testModifyDocumentInPostProcessor");
204 				routedDocumentId = ppDocument.getDocumentId();
205 				// principal id 1 = ewestfal
206 				ppDocument.adHocToPrincipal(ActionRequestType.APPROVE, "AdHoc", "", "2001", "", true);
207 				ppDocument.route("");
208 				processedChange = true;
209 			}
210 			return new ProcessDocReport(true);
211 		}
212 
213 		public ProcessDocReport doRouteLevelChange(DocumentRouteLevelChange levelChangeEvent) throws Exception {
214 			levelChanges++;
215 			WorkflowDocument document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("ewestfal"), levelChangeEvent.getDocumentId());
216 			document.setTitle("Current level change: " + levelChanges);
217 			document.saveDocumentData();
218 			return new ProcessDocReport(true);
219 		}
220 		
221 	}
222 	
223 	public static class GetDocumentIdsToLockPostProcessor extends DefaultPostProcessor {
224 
225 	    protected String getPrincipalIdForName(String principalName) {
226 	        return KEWServiceLocator.getIdentityHelperService()
227 	                .getIdForPrincipalName(principalName);
228 	    }
229 	    
230 		@Override
231 		public List<String> getDocumentIdsToLock(DocumentLockingEvent lockingEvent) throws Exception {
232 			WorkflowDocument document = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("ewestfal"), lockingEvent.getDocumentId());
233 			if (shouldReturnDocumentIdsToLock) {
234 				List<String> docIds = new ArrayList<String>();
235 				docIds.add(documentBId);
236 				return docIds;
237 			}
238 			return null;
239 		}
240 
241 		@Override
242 		public ProcessDocReport afterProcess(AfterProcessEvent event) throws Exception {
243 			WorkflowDocument wfDocument = WorkflowDocumentFactory.loadDocument(getPrincipalIdForName("ewestfal"), event.getDocumentId());
244 			if (wfDocument.isEnroute()) {
245 				// first, let's load document B in this thread
246 				DocumentRouteHeaderValue document = KEWServiceLocator.getRouteHeaderService().getRouteHeader(documentBId);
247 				// now let's execute the thread
248 				new Thread(updateDocumentThread).start();
249 				// let's wait for a few seconds to either let the thread process or let it acquire the lock
250 				Thread.sleep(5000);
251 				// now update document B
252                 document.setDocTitle(document.getDocTitle() + "...making a change...");
253 				KEWServiceLocator.getRouteHeaderService().saveRouteHeader(document);
254 			}
255 			return super.afterProcess(event);
256 		}
257 		
258 		
259 		
260 	}
261 	
262 	/**
263 	 * A Thread which simply locks and updates the document
264 	 * 
265 	 * @author Kuali Rice Team (rice.collab@kuali.org)
266 	 */
267 	private class UpdateDocumentThread implements Runnable {
268 		private String documentId;
269 		public UpdateDocumentThread(String documentId) {
270 			this.documentId = documentId;
271 		}
272 		public void run() {
273 			TransactionTemplate template = new TransactionTemplate(KEWServiceLocator.getPlatformTransactionManager());
274 			template.execute(new TransactionCallback() {
275 				public Object doInTransaction(TransactionStatus status) {
276 					KEWServiceLocator.getRouteHeaderService().lockRouteHeader(documentId);
277 					DocumentRouteHeaderValue document = KEWServiceLocator.getRouteHeaderService().getRouteHeader(documentId);
278                     // have to actually change something on the doc to get this work
279                     document.setDocTitle(document.getDocTitle() + "UDT");
280 					KEWServiceLocator.getRouteHeaderService().saveRouteHeader(document);
281 					return null;
282 				}
283 			});
284 		}
285 	}
286 	
287 }