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