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