View Javadoc

1   /**
2    * 
3    */
4   package org.kuali.student.lum.workflow;
5   
6   import java.util.Iterator;
7   import java.util.List;
8   
9   import javax.xml.namespace.QName;
10  
11  import org.apache.commons.lang.StringUtils;
12  import org.kuali.rice.core.api.resourceloader.GlobalResourceLoader;
13  import org.kuali.rice.kew.api.KewApiConstants;
14  import org.kuali.rice.kew.api.action.ActionTaken;
15  import org.kuali.rice.kew.framework.postprocessor.ActionTakenEvent;
16  import org.kuali.rice.kew.framework.postprocessor.DocumentRouteStatusChange;
17  import org.kuali.rice.kew.framework.postprocessor.IDocumentEvent;
18  import org.kuali.student.r1.core.proposal.dto.ProposalInfo;
19  import org.kuali.student.r1.core.statement.dto.ReqComponentInfo;
20  import org.kuali.student.r1.core.statement.dto.StatementTreeViewInfo;
21  
22  import org.kuali.student.r2.common.dto.AttributeInfo;
23  import org.kuali.student.r2.common.dto.DtoConstants;
24  import org.kuali.student.r2.common.exceptions.DoesNotExistException;
25  import org.kuali.student.r2.common.exceptions.OperationFailedException;
26  import org.kuali.student.r2.common.util.ContextUtils;
27  import org.kuali.student.r2.lum.clu.CLUConstants;
28  import org.kuali.student.r2.lum.course.dto.CourseInfo;
29  import org.kuali.student.r2.lum.course.service.CourseService;
30  import org.springframework.transaction.annotation.Transactional;
31  
32  /**
33   * A base post processor class for Course document types in Workflow.
34   *
35   */
36  @Transactional(readOnly=true, rollbackFor={Throwable.class})
37  public class CoursePostProcessorBase extends KualiStudentPostProcessorBase {
38      private static final org.apache.log4j.Logger LOG = org.apache.log4j.Logger.getLogger(CoursePostProcessorBase.class);
39  
40      private CourseService courseService;
41      private CourseStateChangeServiceImpl courseStateChangeService;
42  
43      /**
44       *    This method changes the state of the course when a Withdraw action is processed on a proposal.
45       *    For create and modify proposals, a new clu was created which needs to be cancelled via
46       *    setting it to "not approved."
47        *    
48       *    For retirement proposals, a clu is never actually created, therefore we don't update the clu at
49       *    all if it is withdrawn.
50       *       
51       *    @param actionTakenEvent - contains the docId, the action taken (code "d"), the principalId which submitted it, etc
52       *    @param proposalInfo - The proposal object being withdrawn 
53       */   
54      @Override
55      protected void processWithdrawActionTaken(ActionTakenEvent actionTakenEvent, ProposalInfo proposalInfo) throws Exception {
56          
57          if (proposalInfo != null){
58              String proposalDocType=proposalInfo.getType();      
59              // The current two proposal docTypes which being withdrawn will cause a course to be 
60              // disapproved are Create and Modify (because a new DRAFT version is created when these 
61              // proposals are submitted.)
62              if ( CLUConstants.PROPOSAL_TYPE_COURSE_CREATE.equals(proposalDocType)
63                      ||  CLUConstants.PROPOSAL_TYPE_COURSE_MODIFY.equals(proposalDocType)) {
64                  LOG.info("Will set CLU state to '"
65                          + DtoConstants.STATE_NOT_APPROVED + "'");
66                  // Get Clu
67                  CourseInfo courseInfo = getCourseService().getCourse(
68                          getCourseId(proposalInfo), ContextUtils.getContextInfo());
69                  // Update Clu
70                  updateCourse(actionTakenEvent, DtoConstants.STATE_NOT_APPROVED,
71                          courseInfo, proposalInfo);
72              } 
73              // Retire proposal is the only proposal type at this time which will not require a 
74              // change to the clu if withdrawn.
75                          else if ( CLUConstants.PROPOSAL_TYPE_COURSE_RETIRE.equals(proposalDocType)) {
76                  LOG.info("Withdrawing a retire proposal with ID'" + proposalInfo.getId() 
77                          + ", will not change any CLU state as there is no new CLU object to set.");
78              }
79          } else {
80              LOG.info("Proposal Info is null when a withdraw proposal action was taken, doing nothing.");
81          }
82      }
83  
84      @Override
85      protected boolean processCustomActionTaken(ActionTakenEvent actionTakenEvent, ActionTaken actionTaken, ProposalInfo proposalInfo) throws Exception {
86          String cluId = getCourseId(proposalInfo);
87          CourseInfo courseInfo = getCourseService().getCourse(cluId, ContextUtils.getContextInfo());
88          // submit, blanket approve action taken comes through here.        
89          updateCourse(actionTakenEvent, null, courseInfo, proposalInfo);
90          return true;
91      }
92  
93      /**
94       * This method takes a clu proposal, determines what the "new state"
95       * of the clu should be, then routes the clu I, and the new state
96       * to CourseStateChangeServiceImpl.java
97       */
98       @Override   
99       protected boolean processCustomRouteStatusChange(DocumentRouteStatusChange statusChangeEvent, ProposalInfo proposalInfo) throws Exception {
100 
101          String courseId = getCourseId(proposalInfo);        
102          String prevEndTermAtpId = proposalInfo.getAttributes().get("prevEndTerm");
103          
104          // Get the current "existing" courseInfo
105          CourseInfo courseInfo = getCourseService().getCourse(courseId, ContextUtils.getContextInfo());
106          
107          // Get the new state the course should now change to        
108          String newCourseState = getCluStateForRouteStatus(courseInfo.getStateKey(), statusChangeEvent.getNewRouteStatus(), proposalInfo.getType());
109          
110          //Use the state change service to update to active and update preceding versions
111          if (newCourseState != null){
112              if(DtoConstants.STATE_ACTIVE.equals(newCourseState)){     
113                  
114                  // Change the state using the effective date as the version start date
115                  // update course and save it for retire if state = retire           
116                  getCourseStateChangeService().changeState(courseId, newCourseState, prevEndTermAtpId, ContextUtils.getContextInfo());
117              } else
118                  
119                  // Retire By Proposal will come through here, extra data will need 
120                  // to be copied from the proposalInfo to the courseInfo fields before 
121                  // the save happens.            
122                  if(DtoConstants.STATE_RETIRED.equals(newCourseState)){
123                      retireCourseByProposalCopyAndSave(newCourseState, courseInfo, proposalInfo);
124                      getCourseStateChangeService().changeState(courseId, newCourseState, prevEndTermAtpId, ContextUtils.getContextInfo());
125              }
126                else{ // newCourseState of null comes here, is this desired?
127                  updateCourse(statusChangeEvent, newCourseState, courseInfo, proposalInfo);
128              }                  
129          }
130          return true;
131      }
132      
133      /**
134       * 
135       * 
136       * In this method, the proposal object fields are copied to the cluInfo object
137       * fields to pass validation. This method copies data from the custom Retire
138       * By Proposal proposalInfo Object Fields into the courseInfo object so that upon save it will
139       * pass validation.
140       * 
141       * Admin Retire and Retire by Proposal both end up here.
142       * 
143       * This Route will get you here, Route Statuses:
144       * 'S' Saved 
145       * 'R' Enroute 
146       * 'A' Approved - After final approve, status is set to 'A'  
147       * 'P' Processed - During this run through coursepostprocessorbase, assuming 
148       * doctype is Retire, we end up here.  
149       * 
150       * @param courseState - used to confirm state is retired
151      * @param courseInfo - course object we are updating
152      * @param proposalInfo - proposal object which has the on-screen fields we are copying from
153       */
154      protected void retireCourseByProposalCopyAndSave(String courseState, CourseInfo courseInfo, ProposalInfo proposalInfo) throws Exception {
155          
156          // Copy the data to the object - 
157          // These Proposal Attribs need to go back to courseInfo Object 
158          // to pass validation.
159          if (DtoConstants.STATE_RETIRED.equals(courseState)){
160              if ((proposalInfo != null) && (proposalInfo.getAttributes() != null))
161              {
162              String rationale = proposalInfo.getRationale();
163              String proposedEndTerm = proposalInfo.getAttributes().get("proposedEndTerm");          
164              String proposedLastTermOffered = proposalInfo.getAttributes().get("proposedLastTermOffered");
165              String proposedLastCourseCatalogYear = proposalInfo.getAttributes().get("proposedLastCourseCatalogYear");
166                    
167              courseInfo.setEndTerm(proposedEndTerm);             
168              courseInfo.getAttributes().add(new AttributeInfo("retirementRationale", rationale));
169              courseInfo.getAttributes().add(new AttributeInfo("lastTermOffered", proposedLastTermOffered));
170              courseInfo.getAttributes().add(new AttributeInfo("lastPublicationYear", proposedLastCourseCatalogYear));
171              
172                // lastTermOffered is a special case field, as it is required upon retire state
173                // but not required for submit.  Therefore it is possible for a user to submit a retire proposal
174                // without this field filled out, then when the course gets approved, and the state changes to RETIRED
175                // validation would fail and the proposal will then go into exception routing.  
176                // We can't simply make lastTermOffered a required field as it is not a desired field  
177                // on the course proposal screen.
178                //              
179                // So in the case of lastTermOffered being null when a course is retired,
180                // Just copy the "proposalInfo.proposedEndTerm" value (required for saves, so it will be filled out) 
181                // into "courseInfo.lastTermOffered" to pass validation.   
182                if ((proposalInfo!=null) && (courseInfo!=null) && 
183                          (courseInfo.getAttributeValue("lastTermOffered")==null)) {
184                     courseInfo.getAttributes().add(new AttributeInfo("lastTermOffered", proposalInfo.getAttributes().get("proposedEndTerm")));
185                }
186              }
187          }
188          // Save the Data to the DB
189          getCourseService().updateCourse(courseInfo.getId(), courseInfo, ContextUtils.getContextInfo());
190      }
191 
192     protected String getCourseId(ProposalInfo proposalInfo) throws OperationFailedException {
193         if (proposalInfo.getProposalReference().size() != 1) {
194             LOG.error("Found " + proposalInfo.getProposalReference().size() + " CLU objects linked to proposal with proposalId='" + proposalInfo.getId() + "'. Must have exactly 1 linked.");
195             throw new OperationFailedException("Found " + proposalInfo.getProposalReference().size() + " CLU objects linked to proposal with docId='" + proposalInfo.getWorkflowId() + "' and proposalId='" + proposalInfo.getId() + "'. Must have exactly 1 linked.");
196         }
197         return proposalInfo.getProposalReference().get(0);
198     }
199 
200     /** This method returns the state a clu should go to, based on 
201      *  the Proposal's docType and the newWorkflow StatusCode 
202      *  which are passed in.
203      * 
204      * @param currentCluState - the current state set on the CLU
205      * @param newWorkflowStatusCode - the new route status code that is getting set on the workflow document
206      * @param docType - The doctype of the proposal which kicked off this workflow.
207      * @return the CLU state to set or null if the CLU does not need it's state changed
208      */
209     protected String getCluStateForRouteStatus(String currentCluState, String newWorkflowStatusCode, String docType) {
210         if (CLUConstants.PROPOSAL_TYPE_COURSE_RETIRE.equals(docType)) {
211             // This is for Retire Proposal, Course State should remain active for
212             // all other route statuses.            
213             if (KewApiConstants.ROUTE_HEADER_PROCESSED_CD.equals(newWorkflowStatusCode)){
214                 return DtoConstants.STATE_RETIRED;
215             }   
216             return null;  // returning null indicates no change in course state required
217         } else {
218             //  The following is for Create, Modify, and Admin Modify proposals.    
219             if (StringUtils.equals(KewApiConstants.ROUTE_HEADER_SAVED_CD, newWorkflowStatusCode)) {
220                 return getCourseStateFromNewState(currentCluState, DtoConstants.STATE_DRAFT);
221             } else if (KewApiConstants.ROUTE_HEADER_CANCEL_CD .equals(newWorkflowStatusCode)) {
222                 return getCourseStateFromNewState(currentCluState, DtoConstants.STATE_NOT_APPROVED);
223             } else if (KewApiConstants.ROUTE_HEADER_ENROUTE_CD.equals(newWorkflowStatusCode)) {
224                 return getCourseStateFromNewState(currentCluState, DtoConstants.STATE_DRAFT);
225             } else if (KewApiConstants.ROUTE_HEADER_DISAPPROVED_CD.equals(newWorkflowStatusCode)) {
226                 /* current requirements state that on a Withdraw (which is a KEW Disapproval) the 
227                  * CLU state should be submitted so no special handling required here
228                  */
229                 return getCourseStateFromNewState(currentCluState, DtoConstants.STATE_NOT_APPROVED);
230             } else if (KewApiConstants.ROUTE_HEADER_PROCESSED_CD.equals(newWorkflowStatusCode)) {
231                 return getCourseStateFromNewState(currentCluState, DtoConstants.STATE_ACTIVE);
232             } else if (KewApiConstants.ROUTE_HEADER_EXCEPTION_CD.equals(newWorkflowStatusCode)) {
233                 return getCourseStateFromNewState(currentCluState, DtoConstants.STATE_DRAFT);
234             } else {
235                 // no status to set
236                 return null;
237             }
238         }
239     }
240 
241     /**
242      * Default behavior is to return the <code>newCluState</code> variable only if it differs from the
243      * <code>currentCluState</code> value. Otherwise <code>null</code> will be returned.
244      */
245     protected String getCourseStateFromNewState(String currentCourseState, String newCourseState) {
246         if (LOG.isInfoEnabled()) {
247             LOG.info("current CLU state is '" + currentCourseState + "' and new CLU state will be '" + newCourseState + "'");
248         }
249         return getStateFromNewState(currentCourseState, newCourseState);
250     }
251 
252     @Transactional(readOnly=false,noRollbackFor={DoesNotExistException.class},rollbackFor={Throwable.class})
253     protected void updateCourse(IDocumentEvent iDocumentEvent, String courseState, CourseInfo courseInfo, ProposalInfo proposalInfo) throws Exception {
254         // only change the state if the course is not currently set to that state
255         boolean requiresSave = false;
256         if (courseState != null) {
257             if (LOG.isInfoEnabled()) {
258                 LOG.info("Setting state '" + courseState + "' on CLU with cluId='" + courseInfo.getId() + "'");
259             }
260             courseInfo.setStateKey(courseState);
261             requiresSave = true;
262         }
263         if (LOG.isInfoEnabled()) {
264             LOG.info("Running preProcessCluSave with cluId='" + courseInfo.getId() + "'");
265         }
266         requiresSave |= preProcessCourseSave(iDocumentEvent, courseInfo);
267 
268         if (requiresSave) {
269             getCourseService().updateCourse(courseInfo.getId(), courseInfo, ContextUtils.getContextInfo());
270             
271             //For a newly approved course (w/no prior active versions), make the new course the current version.
272             if (DtoConstants.STATE_ACTIVE.equals(courseState) && courseInfo.getVersion().getCurrentVersionStart() == null){
273             	// TODO: set states of other approved courses to superseded                
274                 
275             	// if current version's state is not active then we can set this course as the active course
276             	//if (!DtoConstants.STATE_ACTIVE.equals(getCourseService().getCourse(getCourseService().getCurrentVersion(CourseServiceConstants.COURSE_NAMESPACE_URI, courseInfo.getVersion().getVersionIndId()).getId()).getState())) { 
277             		getCourseService().setCurrentCourseVersion(courseInfo.getId(), null, ContextUtils.getContextInfo());
278             	//}
279             }
280             
281             List<StatementTreeViewInfo> statementTreeViewInfos = courseService.getCourseStatements(courseInfo.getId(), null, null, ContextUtils.getContextInfo());
282             if(statementTreeViewInfos!=null){
283 	            statementTreeViewInfoStateSetter(courseInfo.getStateKey(), statementTreeViewInfos.iterator());
284 	            
285 	            for(Iterator<StatementTreeViewInfo> it = statementTreeViewInfos.iterator(); it.hasNext();)
286 
287 	        		courseService.updateCourseStatement(courseInfo.getId(), courseState, it.next(), ContextUtils.getContextInfo());
288             }
289         }
290         
291     }
292 
293     protected boolean preProcessCourseSave(IDocumentEvent iDocumentEvent, CourseInfo courseInfo) {
294         return false;
295     }
296 
297     protected CourseService getCourseService() {
298         if (this.courseService == null) {
299             this.courseService = (CourseService) GlobalResourceLoader.getService(new QName("http://student.kuali.org/wsdl/course","CourseService")); 
300         }
301         return this.courseService;
302     }
303     protected CourseStateChangeServiceImpl getCourseStateChangeService() {
304         if (this.courseStateChangeService == null) {
305             this.courseStateChangeService = new CourseStateChangeServiceImpl();
306             this.courseStateChangeService.setCourseService(getCourseService());
307         }
308         return this.courseStateChangeService;
309     }    
310     /*
311      * Recursively set state for StatementTreeViewInfo
312      * TODO: We are not able to reuse the code in CourseStateUtil for dependency reason.
313      */   
314     public void statementTreeViewInfoStateSetter(String courseState, Iterator<StatementTreeViewInfo> itr) {
315     	while(itr.hasNext()) {
316         	StatementTreeViewInfo statementTreeViewInfo = (StatementTreeViewInfo)itr.next();
317         	statementTreeViewInfo.setState(courseState);
318         	List<ReqComponentInfo> reqComponents = statementTreeViewInfo.getReqComponents();
319         	for(Iterator<ReqComponentInfo> it = reqComponents.iterator(); it.hasNext();)
320         		it.next().setState(courseState);
321 
322         	statementTreeViewInfoStateSetter(courseState, statementTreeViewInfo.getStatements().iterator());
323         }
324     }
325 }