Coverage Report - org.kuali.student.process.poc.krms.KRMSProcessEvaluator
 
Classes in this File Line Coverage Branch Coverage Complexity
KRMSProcessEvaluator
0%
0/176
0%
0/70
5.643
 
 1  
 /**
 2  
  * Copyright 2011 The Kuali Foundation Licensed under the
 3  
  * Educational Community License, Version 2.0 (the "License"); you may
 4  
  * not use this file except in compliance with the License. You may
 5  
  * obtain a copy of the License at
 6  
  *
 7  
  * http://www.osedu.org/licenses/ECL-2.0
 8  
  *
 9  
  * Unless required by applicable law or agreed to in writing,
 10  
  * software distributed under the License is distributed on an "AS IS"
 11  
  * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
 12  
  * or implied. See the License for the specific language governing
 13  
  * permissions and limitations under the License.
 14  
  */
 15  
 package org.kuali.student.process.poc.krms;
 16  
 
 17  
 import org.joda.time.DateTime;
 18  
 import org.kuali.rice.krms.api.engine.EngineResults;
 19  
 import org.kuali.rice.krms.api.engine.ExecutionFlag;
 20  
 import org.kuali.rice.krms.api.engine.ExecutionOptions;
 21  
 import org.kuali.rice.krms.api.engine.ResultEvent;
 22  
 import org.kuali.rice.krms.api.engine.SelectionCriteria;
 23  
 import org.kuali.rice.krms.api.engine.TermResolver;
 24  
 import org.kuali.rice.krms.api.repository.LogicalOperator;
 25  
 import org.kuali.rice.krms.framework.engine.Agenda;
 26  
 import org.kuali.rice.krms.framework.engine.AgendaTreeEntry;
 27  
 import org.kuali.rice.krms.framework.engine.BasicAgenda;
 28  
 import org.kuali.rice.krms.framework.engine.BasicAgendaTree;
 29  
 import org.kuali.rice.krms.framework.engine.BasicAgendaTreeEntry;
 30  
 import org.kuali.rice.krms.framework.engine.BasicContext;
 31  
 import org.kuali.rice.krms.framework.engine.BasicRule;
 32  
 import org.kuali.rice.krms.framework.engine.CompoundProposition;
 33  
 import org.kuali.rice.krms.framework.engine.Context;
 34  
 import org.kuali.rice.krms.framework.engine.ContextProvider;
 35  
 import org.kuali.rice.krms.framework.engine.Proposition;
 36  
 import org.kuali.rice.krms.framework.engine.ProviderBasedEngine;
 37  
 import org.kuali.student.common.util.krms.ManualContextProvider;
 38  
 import org.kuali.student.common.util.krms.RulesExecutionConstants;
 39  
 import org.kuali.student.enrollment.acal.dto.TermInfo;
 40  
 import org.kuali.student.enrollment.acal.service.AcademicCalendarService;
 41  
 import org.kuali.student.process.poc.context.CourseRegistrationProcessContextInfo;
 42  
 import org.kuali.student.process.poc.evaluator.ProcessEvaluator;
 43  
 import org.kuali.student.process.poc.krms.proposition.ExemptionAwareProposition;
 44  
 import org.kuali.student.process.poc.krms.proposition.MilestoneDateComparisonProposition;
 45  
 import org.kuali.student.process.poc.krms.proposition.MilestoneDateComparisonProposition.DateComparisonType;
 46  
 import org.kuali.student.process.poc.krms.proposition.PersonLivingProposition;
 47  
 import org.kuali.student.process.poc.krms.proposition.RegistrationHoldProposition;
 48  
 import org.kuali.student.process.poc.krms.proposition.SubProcessProposition;
 49  
 import org.kuali.student.process.poc.krms.proposition.SummerTermProposition;
 50  
 import org.kuali.student.process.poc.util.InstructionComparator;
 51  
 import org.kuali.student.r2.common.dto.ContextInfo;
 52  
 import org.kuali.student.r2.common.dto.ValidationResultInfo;
 53  
 import org.kuali.student.r2.common.exceptions.DoesNotExistException;
 54  
 import org.kuali.student.r2.common.exceptions.InvalidParameterException;
 55  
 import org.kuali.student.r2.common.exceptions.MissingParameterException;
 56  
 import org.kuali.student.r2.common.exceptions.OperationFailedException;
 57  
 import org.kuali.student.r2.common.exceptions.PermissionDeniedException;
 58  
 import org.kuali.student.r2.common.infc.ValidationResult;
 59  
 import org.kuali.student.r2.common.util.constants.ExemptionServiceConstants;
 60  
 import org.kuali.student.r2.common.util.constants.ProcessServiceConstants;
 61  
 import org.kuali.student.r2.core.exemption.dto.ExemptionInfo;
 62  
 import org.kuali.student.r2.core.exemption.infc.DateOverride;
 63  
 import org.kuali.student.r2.core.exemption.service.ExemptionService;
 64  
 import org.kuali.student.r2.core.hold.dto.HoldInfo;
 65  
 import org.kuali.student.r2.core.hold.service.HoldService;
 66  
 import org.kuali.student.r2.core.population.service.PopulationService;
 67  
 import org.kuali.student.r2.core.process.dto.CheckInfo;
 68  
 import org.kuali.student.r2.core.process.dto.InstructionInfo;
 69  
 import org.kuali.student.r2.core.process.service.ProcessService;
 70  
 
 71  
 import java.util.ArrayList;
 72  
 import java.util.Arrays;
 73  
 import java.util.Collections;
 74  
 import java.util.HashMap;
 75  
 import java.util.LinkedHashMap;
 76  
 import java.util.List;
 77  
 import java.util.Map;
 78  
 
 79  
 /**
 80  
  * Utility class to build up a Proposition tree for to evaluate checks from a Process
 81  
  *
 82  
  * @author alubbers
 83  
  */
 84  0
 public class KRMSProcessEvaluator implements ProcessEvaluator<CourseRegistrationProcessContextInfo> {
 85  
 
 86  
     public static final String EXEMPTION_WAS_USED_MESSAGE_SUFFIX = " (exemption applied)";
 87  
 
 88  
     private AcademicCalendarService acalService;
 89  
     private ProcessService processService;
 90  
     private PopulationService populationService;
 91  
     private ExemptionService exemptionService;
 92  
     private HoldService holdService;
 93  
     private List<TermResolver<?>> termResolvers;
 94  
     private ExecutionOptions executionOptions;
 95  
     private SelectionCriteria selectionCriteria;
 96  
 
 97  
     public void setAcalService(AcademicCalendarService acalService) {
 98  0
         this.acalService = acalService;
 99  0
     }
 100  
 
 101  
     public void setProcessService(ProcessService processService) {
 102  0
         this.processService = processService;
 103  0
     }
 104  
 
 105  
     public void setPopulationService(PopulationService populationService) {
 106  0
         this.populationService = populationService;
 107  0
     }
 108  
 
 109  
     public void setExemptionService(ExemptionService exemptionService) {
 110  0
         this.exemptionService = exemptionService;
 111  0
     }
 112  
 
 113  
     public void setHoldService(HoldService holdService) {
 114  0
         this.holdService = holdService;
 115  0
     }
 116  
 
 117  
     @Override
 118  
     public List<ValidationResultInfo> evaluate(CourseRegistrationProcessContextInfo processContext, ContextInfo context)
 119  
             throws OperationFailedException {
 120  
 
 121  
         List<InstructionInfo> instructions;
 122  
         try {
 123  0
             instructions = processService.getInstructionsByProcess(processContext.getProcessKey(), context);
 124  0
         } catch (OperationFailedException ex) {
 125  0
             throw ex;
 126  0
         } catch (Exception ex) {
 127  0
             throw new OperationFailedException("unexpected", ex);
 128  0
         }
 129  
 
 130  0
         TermInfo term = null;
 131  
 
 132  0
         if (processContext.getTermKey() != null) {
 133  
             try {
 134  0
                 term = acalService.getTerm(processContext.getTermKey(), context);
 135  0
             } catch (OperationFailedException ex) {
 136  0
                 throw ex;
 137  0
             } catch (Exception ex) {
 138  0
                 throw new OperationFailedException("unexpected", ex);
 139  0
             }
 140  
         }
 141  
 
 142  0
         List<InstructionInfo> sortedInstructions = new ArrayList<InstructionInfo>();
 143  
 
 144  
         // filter out Instructions that do apply to the context
 145  0
         for (InstructionInfo instruction : instructions) {
 146  
 
 147  
             // filter out by term
 148  0
             if (term != null && !instruction.getAppliedAtpTypeKeys().isEmpty() && !instruction.getAppliedAtpTypeKeys().contains(term.getTypeKey())) {
 149  0
                 continue;
 150  
             }
 151  
 
 152  
             // filter out by applicable Population
 153  0
             boolean skipInstruction = true;
 154  0
             for (String popKey : instruction.getAppliedPopulationKeys()) {
 155  
                 try {
 156  0
                     if (populationService.isMember(processContext.getStudentId(), popKey, context)) {
 157  0
                         skipInstruction = false;
 158  0
                         break;
 159  
                     }
 160  0
                 } catch (OperationFailedException ex) {
 161  0
                     throw ex;
 162  0
                 } catch (Exception ex) {
 163  0
                     throw new OperationFailedException("unexpected", ex);
 164  0
                 }
 165  
             }
 166  
 
 167  
             // check for any direct exemptions the student may have for this check
 168  
             List<ExemptionInfo> exemptions;
 169  
             try {
 170  0
                 exemptions = exemptionService.getActiveExemptionsByTypeProcessAndCheckForPerson(ExemptionServiceConstants.CHECK_EXEMPTION_TYPE_KEY, processContext.getProcessKey(), instruction.getCheckKey(), processContext.getStudentId(), context);
 171  0
             } catch (OperationFailedException ex) {
 172  0
                 throw ex;
 173  0
             } catch (Exception ex) {
 174  0
                 throw new OperationFailedException("unexpected", ex);
 175  0
             }
 176  0
             if (!exemptions.isEmpty()) {
 177  0
                 skipInstruction = true;
 178  
             }
 179  
 
 180  0
             if (skipInstruction) {
 181  0
                 continue;
 182  
             }
 183  
 
 184  0
             sortedInstructions.add(instruction);
 185  0
         }
 186  
 
 187  0
         Collections.sort(sortedInstructions, new InstructionComparator());
 188  
 
 189  
         // build a list of propositions based on the sorted instructions, using a LinkedHashMap to maintain the same sorting order
 190  0
         Map<Proposition, InstructionInfo> propositions = new LinkedHashMap<Proposition, InstructionInfo>(sortedInstructions.size());
 191  0
         for (InstructionInfo instruction : sortedInstructions) {
 192  
 
 193  
             CheckInfo check;
 194  
             try {
 195  0
                 check = processService.getCheck(instruction.getCheckKey(), context);
 196  0
             } catch (OperationFailedException ex) {
 197  0
                 throw ex;
 198  0
             } catch (Exception ex) {
 199  0
                 throw new OperationFailedException("unexpected", ex);
 200  0
             }
 201  
 
 202  
             // if a sub-process is found,
 203  0
             if (check.getTypeKey().equals(ProcessServiceConstants.PROCESS_CHECK_TYPE_KEY)) {
 204  
 
 205  0
                 CourseRegistrationProcessContextInfo checkContext = CourseRegistrationProcessContextInfo.createForRegistrationEligibility(processContext.getStudentId(), processContext.getTermKey());
 206  0
                 checkContext.setProcessKey(check.getProcessKey());
 207  
 
 208  0
                 propositions.put(new SubProcessProposition(checkContext, this), instruction);
 209  
             }
 210  
 
 211  0
             if (check.getTypeKey().equals(ProcessServiceConstants.HOLD_CHECK_TYPE_KEY)) {
 212  0
                 propositions.put(new RegistrationHoldProposition(check.getIssueKey()), instruction);
 213  0
             } else if (check.getKey().equals(ProcessServiceConstants.CHECK_KEY_IS_ALIVE)) {
 214  0
                 propositions.put(new PersonLivingProposition(), instruction);
 215  0
             } else if (check.getKey().equals(ProcessServiceConstants.CHECK_KEY_IS_NOT_SUMMER_TERM)) {
 216  0
                 propositions.put(new SummerTermProposition(term), instruction);
 217  0
             } else if (check.getTypeKey().equals(ProcessServiceConstants.START_DATE_CHECK_TYPE_KEY)) {
 218  0
                 propositions.put(buildMilestoneCheckProposition(check, DateComparisonType.AFTER, processContext, context), instruction);
 219  0
             } else if (check.getTypeKey().equals(ProcessServiceConstants.DEADLINE_CHECK_TYPE_KEY)) {
 220  0
                 propositions.put(buildMilestoneCheckProposition(check, DateComparisonType.BEFORE, processContext, context), instruction);
 221  0
             } else if (check.getTypeKey().equals(ProcessServiceConstants.TIME_PERIOD_CHECK_TYPE_KEY)) {
 222  0
                 propositions.put(buildMilestoneCheckProposition(check, DateComparisonType.BETWEEN, processContext, context), instruction);
 223  
             }
 224  0
         }
 225  
 
 226  
         // if all instructions are skipped, and no propositions were generated, return one "success" message
 227  0
         if(propositions.isEmpty()) {
 228  0
             ValidationResultInfo success = new ValidationResultInfo();
 229  0
             success.setLevel(ValidationResult.ErrorLevel.OK);
 230  0
             return Collections.singletonList(success);
 231  
         }
 232  
 
 233  
         // Build the list of known facts prior to execution
 234  0
         Map<String, Object> executionFacts = buildExecutionFacts(processContext, context);
 235  
 
 236  
         // Combine all propositions into a CompoundProposition and evaluate the results
 237  0
         EngineResults krmsResults = evaluateProposition(new CompoundProposition(LogicalOperator.AND, new ArrayList<Proposition>(propositions.keySet())), executionFacts);
 238  
 
 239  
 
 240  
         List<ValidationResultInfo> results;
 241  
         try {
 242  0
             results = buildValidationResultsFromEngineResults(krmsResults, propositions, context);   
 243  0
         } catch (OperationFailedException ex) {
 244  0
             throw ex;
 245  0
         } catch (Exception ex) {
 246  0
             throw new OperationFailedException("unexpected", ex);
 247  0
         }
 248  
 
 249  0
         return results;
 250  
     }
 251  
 
 252  
     private MilestoneDateComparisonProposition buildMilestoneCheckProposition(CheckInfo check, DateComparisonType comparisonType, CourseRegistrationProcessContextInfo processContext, ContextInfo context)
 253  
             throws OperationFailedException {
 254  
         List<ExemptionInfo> exemptions;
 255  
         try {
 256  0
             exemptions = exemptionService.getActiveExemptionsByTypeProcessAndCheckForPerson(ExemptionServiceConstants.MILESTONE_DATE_EXEMPTION_TYPE_KEY, processContext.getProcessKey(), check.getKey(), processContext.getStudentId(), context);
 257  0
         } catch (OperationFailedException ex) {
 258  0
             throw ex;
 259  0
         } catch (Exception ex) {
 260  0
             throw new OperationFailedException("unexpected", ex);
 261  0
         }
 262  
 
 263  0
         if (exemptions.isEmpty()) {
 264  0
             return new MilestoneDateComparisonProposition(RulesExecutionConstants.CURRENT_DATE_TERM_NAME, comparisonType, check.getMilestoneTypeKey(), processContext.getTermKey(), true, null);
 265  
         }
 266  
 
 267  
         // For now, assume there is only one active Exemption
 268  0
         DateOverride dateOverrideInfo = exemptions.get(0).getDateOverride();
 269  
 
 270  0
         return new MilestoneDateComparisonProposition(RulesExecutionConstants.CURRENT_DATE_TERM_NAME, comparisonType, check.getMilestoneTypeKey(), processContext.getTermKey(), true, dateOverrideInfo);
 271  
     }
 272  
 
 273  
     private EngineResults evaluateProposition(Proposition proposition, Map<String, Object> executionFacts) {
 274  
 
 275  
         // Build the KRMS agenda and other startup objects to execute
 276  0
         List<AgendaTreeEntry> treeEntries = new ArrayList<AgendaTreeEntry>(1);
 277  0
         treeEntries.add(new BasicAgendaTreeEntry(new BasicRule(proposition, null)));
 278  
 
 279  0
         Map<String, String> qualifiers = Collections.emptyMap();
 280  0
         Agenda agenda = new BasicAgenda(qualifiers, new BasicAgendaTree(treeEntries));
 281  
 
 282  0
         Context context = new BasicContext(Arrays.asList(agenda), termResolvers);
 283  0
         ContextProvider contextProvider = new ManualContextProvider(context);
 284  
 
 285  0
         ProviderBasedEngine engine = new ProviderBasedEngine();
 286  0
         engine.setContextProvider(contextProvider);
 287  
 
 288  0
         if (executionOptions == null) {
 289  0
             executionOptions = new ExecutionOptions();
 290  0
             executionOptions.setFlag(ExecutionFlag.LOG_EXECUTION, true);
 291  0
             executionOptions.setFlag(ExecutionFlag.EVALUATE_ALL_PROPOSITIONS, true);
 292  
         }
 293  
 
 294  0
         if (selectionCriteria == null) {
 295  0
             Map<String, String> contextQualifiers = Collections.singletonMap(RulesExecutionConstants.DOCTYPE_CONTEXT_QUALIFIER, RulesExecutionConstants.STUDENT_ELIGIBILITY_DOCTYPE);
 296  
 
 297  0
             Map<String, String> empty = Collections.emptyMap();
 298  0
             selectionCriteria = SelectionCriteria.createCriteria(new DateTime(), contextQualifiers, empty);
 299  
         }
 300  
 
 301  0
         return engine.execute(selectionCriteria, executionFacts, executionOptions);
 302  
     }
 303  
 
 304  
     private List<ValidationResultInfo> buildValidationResultsFromEngineResults(EngineResults engineResults, Map<Proposition, InstructionInfo> propositionInstructionMap, ContextInfo context) throws InvalidParameterException, MissingParameterException, DoesNotExistException, PermissionDeniedException, OperationFailedException {
 305  0
         List<ValidationResultInfo> results = new ArrayList<ValidationResultInfo>();
 306  
 
 307  
         // go through all the results from the Propositions, and build validation results based on any propositions that failed
 308  0
         List<ResultEvent> events = engineResults.getResultsOfType(ResultEvent.PropositionEvaluated);
 309  0
         for (ResultEvent e : events) {
 310  0
             Proposition prop = (Proposition) e.getSource();
 311  0
             InstructionInfo instruction = propositionInstructionMap.get(prop);
 312  0
             ValidationResultInfo result = new ValidationResultInfo();
 313  0
             String message = instruction.getMessage().getPlain();
 314  0
             ExemptionAwareProposition exemptionProp = null;
 315  
 
 316  0
             if(prop instanceof SubProcessProposition) {
 317  0
                 List<ValidationResultInfo> subResults = (List<ValidationResultInfo>) e.getResultDetails().get(RulesExecutionConstants.SUBPROCESS_EVALUATION_RESULTS);
 318  0
                 results.addAll(subResults);
 319  0
             }
 320  
             else {
 321  
                 // if the proposition is could have an exemption, check for the exemption and add a suffix to the message
 322  0
                 if(prop instanceof ExemptionAwareProposition) {
 323  0
                     exemptionProp = (ExemptionAwareProposition)prop;
 324  0
                     if(exemptionProp.isExemptionUsed()) {
 325  0
                         message += EXEMPTION_WAS_USED_MESSAGE_SUFFIX;
 326  
                     }
 327  
                 }
 328  0
                 if (e.getResult()) {
 329  0
                     result.setLevel(ValidationResult.ErrorLevel.OK);
 330  
                     // add a message to an OK result only if an exemption was used
 331  0
                     if(exemptionProp != null && exemptionProp.isExemptionUsed()) {
 332  0
                         result.setMessage(message);
 333  
                     }
 334  
                 }
 335  
                 else {
 336  
 
 337  0
                     if (instruction.getIsWarning()) {
 338  0
                         result.setWarn(message);
 339  
                     } else {
 340  0
                         result.setError(message);
 341  
                     }
 342  
                 }
 343  
 
 344  0
                 results.add(result);
 345  
             }
 346  
 
 347  0
             if (!e.getResult() && !instruction.getContinueOnFail()) {
 348  0
                 break;
 349  
             }
 350  0
         }
 351  
 
 352  
         // Now check if there are any warnings from Holds that are marked as warning only
 353  0
         List<String> warningHoldIds = (List<String>) engineResults.getAttribute(RulesExecutionConstants.REGISTRATION_HOLD_WARNINGS_ATTRIBUTE);
 354  
 
 355  0
         if (warningHoldIds != null && !warningHoldIds.isEmpty()) {
 356  0
             for (String holdId : warningHoldIds) {
 357  0
                 HoldInfo hold = holdService.getHold(holdId, context);
 358  0
                 ValidationResultInfo result = new ValidationResultInfo();
 359  0
                 result.setWarn("The following hold was found on the student's account, but set as a warning: " + hold.getDescr().getPlain());
 360  0
                 results.add(result);
 361  0
             }
 362  
         }
 363  
 
 364  0
         return results;
 365  
     }
 366  
 
 367  
     public List<ValidationResultInfo> evaluateStudentAliveRule(InstructionInfo instruction, CourseRegistrationProcessContextInfo processContext, ContextInfo context) throws InvalidParameterException, MissingParameterException, DoesNotExistException, OperationFailedException, PermissionDeniedException {
 368  0
         List<ValidationResultInfo> results = new ArrayList<ValidationResultInfo>();
 369  
 
 370  
         // Build the list of known facts prior to execution
 371  0
         Map<String, Object> executionFacts = buildExecutionFacts(processContext, context);
 372  
 
 373  
         // evaluate the results for just the one proposition
 374  0
         Proposition prop = new PersonLivingProposition();
 375  0
         EngineResults engineResults = evaluateProposition(prop, executionFacts);
 376  
 
 377  0
         Map<Proposition, InstructionInfo> propInstructionMap = Collections.singletonMap(prop, instruction);
 378  
 
 379  0
         return buildValidationResultsFromEngineResults(engineResults, propInstructionMap, context);
 380  
     }
 381  
 
 382  
     public List<ValidationResultInfo> evaluateSummerTermRule(InstructionInfo instruction, CourseRegistrationProcessContextInfo processContext, ContextInfo context) throws InvalidParameterException, MissingParameterException, DoesNotExistException, OperationFailedException, PermissionDeniedException {
 383  0
         List<ValidationResultInfo> results = new ArrayList<ValidationResultInfo>();
 384  
 
 385  0
         TermInfo term = acalService.getTerm(processContext.getTermKey(), context);
 386  
 
 387  
         // Build the list of known facts prior to execution
 388  0
         Map<String, Object> executionFacts = buildExecutionFacts(processContext, context);
 389  
 
 390  
         // evaluate the results for just the one proposition
 391  0
         Proposition prop = new SummerTermProposition(term);
 392  0
         EngineResults engineResults = evaluateProposition(prop, executionFacts);
 393  
 
 394  0
         Map<Proposition, InstructionInfo> propInstructionMap = Collections.singletonMap(prop, instruction);
 395  
 
 396  0
         return buildValidationResultsFromEngineResults(engineResults, propInstructionMap, context);
 397  
     }
 398  
 
 399  
     private Map<String, Object> buildExecutionFacts(CourseRegistrationProcessContextInfo processContext, ContextInfo context) {
 400  0
         Map<String, Object> executionFacts = new HashMap<String, Object>();
 401  0
         executionFacts.put(RulesExecutionConstants.STUDENT_ID_TERM_NAME, processContext.getStudentId());
 402  0
         executionFacts.put(RulesExecutionConstants.CONTEXT_INFO_TERM_NAME, context);
 403  0
         if (processContext.getTermKey() != null) {
 404  0
             executionFacts.put(RulesExecutionConstants.REGISTRATION_TERM_TERM_NAME, processContext.getTermKey());
 405  
         }
 406  0
         return executionFacts;
 407  
     }
 408  
 
 409  
     public void setTermResolvers(List<TermResolver<?>> termResolvers) {
 410  0
         this.termResolvers = termResolvers;
 411  0
     }
 412  
 
 413  
     public void setExecutionOptions(ExecutionOptions executionOptions) {
 414  0
         this.executionOptions = executionOptions;
 415  0
     }
 416  
 }