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