View Javadoc

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