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