Java tutorial
/* Educational Online Test Delivery System Copyright (c) 2013 American Institutes for Research Distributed under the AIR Open Source License, Version 1.0 See accompanying file AIR-License-1_0.txt or at http://www.smarterapp.org/documents/American_Institutes_for_Research_Open_Source_Software_License.pdf */ package org.opentestsystem.delivery.testreg.eligibility; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.apache.commons.beanutils.PropertyUtils; import org.joda.time.DateTime; import org.joda.time.format.DateTimeFormat; import org.joda.time.format.DateTimeFormatter; import org.opentestsystem.delivery.testreg.domain.ARTHelpers; import org.opentestsystem.delivery.testreg.domain.Assessment; import org.opentestsystem.delivery.testreg.domain.CacheMap; import org.opentestsystem.delivery.testreg.domain.ExplicitEligibility; import org.opentestsystem.delivery.testreg.domain.HierarchyLevel; import org.opentestsystem.delivery.testreg.domain.ImplicitEligibilityRule; import org.opentestsystem.delivery.testreg.domain.InstitutionEntity; import org.opentestsystem.delivery.testreg.domain.Sb11Entity; import org.opentestsystem.delivery.testreg.domain.Sb11SuperEntity; import org.opentestsystem.delivery.testreg.domain.Student; import org.opentestsystem.delivery.testreg.domain.event.AssessmentModificationEvent; import org.opentestsystem.delivery.testreg.domain.event.BatchStudentModificationEvent; import org.opentestsystem.delivery.testreg.domain.event.ExplicitEligibilityModificationEvent; import org.opentestsystem.delivery.testreg.domain.event.StudentModificationEvent; import org.opentestsystem.delivery.testreg.domain.exception.EligibilityException; import org.opentestsystem.delivery.testreg.persistence.AssessmentRepository; import org.opentestsystem.delivery.testreg.persistence.ExplicitEligibilityRepository; import org.opentestsystem.delivery.testreg.persistence.StudentRepository; import org.opentestsystem.delivery.testreg.persistence.criteria.dependencyresolvers.EligibilityDependencyResolver; import org.opentestsystem.delivery.testreg.service.CacheMapService; import org.opentestsystem.delivery.testreg.service.EligibilityService; import org.opentestsystem.delivery.testreg.service.Sb11EntityRepositoryService; import org.opentestsystem.shared.progman.client.ProgManClient; import org.opentestsystem.shared.progman.client.domain.Tenant; import org.opentestsystem.shared.progman.client.domain.TenantType; import org.opentestsystem.shared.security.domain.SbacEntity; import org.opentestsystem.shared.security.service.TenancyService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.cache.annotation.Cacheable; import org.springframework.integration.annotation.ServiceActivator; import org.springframework.stereotype.Component; import org.springframework.util.CollectionUtils; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Set; @Component class EligibilityEvaluatorCache { private final ProgManClient progmanClient; private final TenancyService tenancyService; private final AssessmentRepository assessmentRepository; private final Sb11EntityRepositoryService sb11EntityService; // Unused but for CGLIB to create proxy objects (for @Cacheable). EligibilityEvaluatorCache() { progmanClient = null; tenancyService = null; assessmentRepository = null; sb11EntityService = null; } @Autowired EligibilityEvaluatorCache(ProgManClient progmanClient, TenancyService tenancyService, AssessmentRepository assessmentRepository, Sb11EntityRepositoryService sb11EntityService) { this.progmanClient = progmanClient; this.tenancyService = tenancyService; this.assessmentRepository = assessmentRepository; this.sb11EntityService = sb11EntityService; } @Cacheable("tenantById") public Tenant getTenantById(String tenantId) { return this.progmanClient.getTenantById(tenantId); } @Cacheable("applicableTenants") public List<String> getApplicableTenantIds(final Set<SbacEntity> possibleTenants) { return Lists.transform(tenancyService.getApplicableTenants(possibleTenants), ARTHelpers.TENANT_ID_TRANSFORMER); } @Cacheable("implicitAssessments") public List<Assessment> getImplicitAssessments(String institutionIdentifier, String stateAbbreviation) { // create the initial set with the institution level final Set<SbacEntity> possibleTenants = Sets .newHashSet(new SbacEntity(TenantType.INSTITUTION, institutionIdentifier, "nothing")); // go up the hierarchy to find all the entities up to the client and add to the hash set // we need to know if any of the levels are tenants so we grab all possible assessments for the hierarchy Sb11Entity entity = sb11EntityService.findByEntityIdAndStateAbbreviation(institutionIdentifier, stateAbbreviation, InstitutionEntity.class); while (entity != null) { if (entity.getParentEntityType() != null) { final TenantType tenantType = convertHierarchyLevelToTenantType(entity.getParentEntityType()); if (tenantType != null) { possibleTenants.add(new SbacEntity(tenantType, entity.getParentEntityId(), "nothing")); } } entity = entity.getParentEntityId() != null ? sb11EntityService.getParentEntity(entity) : null; } // find all assessments for possible tenants List<Assessment> implicitAssessments = assessmentRepository.findByEligibilityTypeAndTenantIdIsIn("IMPLICIT", getApplicableTenantIds(possibleTenants)); return CollectionUtils.isEmpty(implicitAssessments) ? new ArrayList<Assessment>() : implicitAssessments; // repo can return null } private static TenantType convertHierarchyLevelToTenantType(final HierarchyLevel level) { TenantType converted = null; switch (level) { case DISTRICT: converted = TenantType.DISTRICT; break; case GROUPOFDISTRICTS: converted = TenantType.DISTRICT_GROUP; break; case GROUPOFINSTITUTIONS: converted = TenantType.INSTITUTION_GROUP; break; case GROUPOFSTATES: converted = TenantType.STATE_GROUP; break; case STATE: converted = TenantType.STATE; break; default: converted = null; break; } return converted; } } @Component public class EligibilityEvaluatorImpl implements EligibilityEvaluator { private static final Logger LOGGER = LoggerFactory.getLogger(EligibilityEvaluatorImpl.class); private static final long PAGE_SIZE = 5000; @Autowired private EligibilityService eligibilityService; @Autowired private AssessmentRepository assessmentRepository; @Autowired private StudentRepository studentRepository; @Autowired private ExplicitEligibilityRepository explicitEligRepository; @Autowired private EligibilityEvaluatorCache eligibilityEvaluatorCache; @Autowired private Sb11EntityRepositoryService sb11EntityService; @Autowired private CacheMapService cacheMapService; @Autowired @Qualifier("eligibilityDependencyResolver") private EligibilityDependencyResolver eligibilityDependencyResolver; // change this class to be EligibilityEvaluator // use to evaluate explicit eligibility also // those rules don't need to be "run", but we do need to create EligibleAssessment/EligibleStudent from them // and like implicit eligibility, they only count if the assessment is in one of its test windows // implicit eligibility rules need to be evaluated under the following circumstances: // - student modification (upload or UI) // - accommodation modification (upload or UI) // - rule change // - assessment modified (change of eligibility type) // if a student is inserted or modified, the student must be checked against all // implicit rules on all assessments marked as implicit // if a student is deleted, then corresponding EligibleStudent must be deleted // and each EligibleAssessment that includes the student must be modified to remove the student // if accommodations are inserted or modified treat the same as student data insert or modification // if accommodations are deleted treat the same as student data modification // if the implicit eligibility rules for an assessment are changed (any create, update, delete) // then each student in the system must be tested against the rules // if an assessment changes from implicit to explicit eligibility, // the corresponding EligibleAssessment must be removed // and each EligibleStudent that includes the assessment must be modified to remove the assessment // then re-evaluate explicit eligibility to see if any records exist for the assessment // if an assessment changes from explicit to implicit eligibility, // the corresponding EligibleAssessment must be removed // and each EligibleStudent that includes the assessment must be modified to remove the assessment // then each student in the system must be checked against the new implicit rules @Override public void evaluateImplicitRules(final Assessment assessment, final Student student) { evaluateImplicitRules(assessment, true, student); } @ServiceActivator(inputChannel = "studentModificationEventsInbound") @Override public void evaluateStudentModification(final StudentModificationEvent event) { LOGGER.debug("Handling a StudentModificationEvent: " + event); switch (event.getAction()) { case UPD: processImplicitEligibilityForStudent(event.getSource()); processExplicitEligibilityForStudent(event.getSource()); break; case DEL: studentDeleted(event.getSource()); break; default: // unknown, throw error here LOGGER.error("Unknown action: " + event.getAction()); throw new EligibilityException("eligibility.unknown.event"); } } @ServiceActivator(inputChannel = "batchStudentModificationEventsInbound") @Override public void evaluateBatchStudentModification(final BatchStudentModificationEvent event) { LOGGER.debug("Handling a BatchStudentModificationEvent: " + event); final List<Student> studentList = event.getSource(); for (final Student student : studentList) { processImplicitEligibilityForStudent(student); processExplicitEligibilityForStudent(student); } } private void processImplicitEligibilityForStudent(final Student student) { // if a student is inserted or modified, the student must be checked against all // implicit rules on all assessments marked as implicit // remove EligibleStudent for this student // find all EligibleAssessments with this student id and modify to remove student // find all Assessments that use IMPLICIT eligibility and run all rules against this student this.eligibilityService.removeAllAssociationsForStudent(student); final List<Assessment> implicitAssessments = eligibilityEvaluatorCache .getImplicitAssessments(student.getInstitutionIdentifier(), student.getStateAbbreviation()); for (final Assessment assess : implicitAssessments) { evaluateImplicitRules(assess, student); } } private void processExplicitEligibilityForStudent(final Student student) { // lookup explicit eligibility for this student // for all rows that exist, add to eligibleStudent final Student retrievedStudent = this.studentRepository .findByEntityIdAndStateAbbreviation(student.getEntityId(), student.getStateAbbreviation()); final List<ExplicitEligibility> explicitEligibilities = this.explicitEligRepository .findByStudentIdAndStateAbbreviation(student.getEntityId(), student.getStateAbbreviation()); if (!CollectionUtils.isEmpty(explicitEligibilities)) { for (final ExplicitEligibility explicitEligibility : explicitEligibilities) { Assessment assessment = eligibilityDependencyResolver .resolveAssessmentForStudent(explicitEligibility); if (assessment != null) { this.eligibilityService.saveAssociation(assessment, retrievedStudent); } else { LOGGER.error("No assessment found for test name = " + explicitEligibility.getTestName() + " and version = " + explicitEligibility.getTestVersion() + " cannot set explicit eligibility for student id = " + student.getEntityId() + "."); } } } } private void studentDeleted(final Student student) { // if a student is deleted, then corresponding EligibleStudent must be deleted // and each EligibleAssessment that includes the student must be modified to remove the student // remove EligibleStudent for this student // find all EligibleAssessments with this student id and modify to remove student this.eligibilityService.removeAllAssociationsForStudent(student); } @ServiceActivator(inputChannel = "assessmentModificationEventsInbound") @Override public void evaluateAssessmentModification(final AssessmentModificationEvent event) { LOGGER.debug("Handling an AssessmentModificationEvent: " + event); switch (event.getAction()) { case UPD: assessmentModified(event); break; case DEL: assessmentDeleted(event); break; default: // unknown, throw error here LOGGER.error("Unknown action: " + event.getAction()); throw new EligibilityException("eligibility.unknown.event"); } } private void assessmentModified(final AssessmentModificationEvent event) { // if the implicit eligibility rules for an assessment are changed (any create, update, delete) // then each student in the system must be tested against the rules // if an assessment changes from implicit to explicit eligibility, // the corresponding EligibleAssessment must be removed // and each EligibleStudent that includes the assessment must be modified to remove the assessment // then re-evaluate explicit eligibility to see if any records exist for the assessment // if an assessment changes from explicit to implicit eligibility, // the corresponding EligibleAssessment must be removed // and each EligibleStudent that includes the assessment must be modified to remove the assessment // then each student in the system must be checked against the new implicit rules final Assessment source = event.getSource(); LOGGER.debug("Removing all associations for assessment with id: " + source.getId()); this.eligibilityService.removeAllAssociationsForAssessment(source); switch (source.getEligibilityType()) { case IMPLICIT: // this has potential to run really slow, but we should be able to filter by // the tenant on the assessment // find all applicable students // add filtering to find only applicable students, for now this will get ALL students // assessment has a tenant, convert tenant back into possible institution ids that can be used to // filter students final String tenantId = source.getTenantId(); final Tenant tenant = this.eligibilityEvaluatorCache.getTenantById(tenantId); if (tenant == null) { break; } // get the entity that corresponds to the tenant final Sb11SuperEntity entity = this.sb11EntityService.findByEntityId(tenant.getName(), tenant.getType()); if (entity == null) { break; } final CacheMap cacheMap = this.cacheMapService.getCacheMap("UberEntityRelationshipMap"); Map<String, Set<String>> uberMap = null; if (cacheMap != null) { uberMap = cacheMap.getMap(); } final Set<String> validEntityMongoIds = uberMap.get(entity.getId()); validEntityMongoIds.add(entity.getId()); LOGGER.debug("Counting students in the DB"); final long numStudents = this.studentRepository .countByInstitutionFilter(new ArrayList<>(validEntityMongoIds)); LOGGER.debug("Calculating implicit eligibility for " + numStudents + " students"); long numPages = numStudents / PAGE_SIZE; final long remain = numStudents % PAGE_SIZE; if (remain > 0) { numPages++; } LOGGER.debug("Chunking students into " + numPages + " pages of size " + PAGE_SIZE); List<Student> students = null; List<Student> eligStudents = null; String studentMongoId = "000000000000000000000000"; // TODO Another speedup can be obtained by sending each chunk to an @Async method, will have to see how // many executors is a good number for (long curPage = 0; curPage < numPages; curPage++) { final long timestamp = System.currentTimeMillis(); // students = studentRepository.findAll( // new PageRequest((int) curPage, (int) PAGE_SIZE, new Sort("_id"))).getContent(); students = this.studentRepository.findAllByRangeAndLimitWithInstitutionFilter(studentMongoId, (int) PAGE_SIZE, new ArrayList<>(validEntityMongoIds)); LOGGER.debug("Time to get another page of students: " + (System.currentTimeMillis() - timestamp)); LOGGER.debug("Running rule evaluation for page " + curPage + " of " + numPages); LOGGER.debug("Evaluating " + students.size() + " students"); studentMongoId = students.get(students.size() - 1).getId(); final long timestamp2 = System.currentTimeMillis(); eligStudents = evaluateImplicitRules(source, false, Iterables.toArray(students, Student.class)); LOGGER.debug("Time to run rule eval for chunk of students: " + (System.currentTimeMillis() - timestamp2)); LOGGER.debug("Evaluation found " + eligStudents.size() + " eligible students, calling save"); final long timestamp3 = System.currentTimeMillis(); if (!eligStudents.isEmpty()) { this.eligibilityService.saveAssociations(source, eligStudents); } LOGGER.debug("Time to save new EligibleStudents for one chunk of students: " + (System.currentTimeMillis() - timestamp3)); LOGGER.debug("Time to run one chunk of evaluation rules plus save: " + (System.currentTimeMillis() - timestamp)); } break; case EXPLICIT: // search for any explicit eligibility records for this assessment and associate final List<ExplicitEligibility> eligRecs = this.explicitEligRepository .findByTestNameAndTestVersion(source.getTestName(), source.getVersion()); final List<Student> tmpStudents = new ArrayList<>(); for (final ExplicitEligibility explicitElig : eligRecs) { // look up Student tmpStudents.add(this.studentRepository.findByEntityIdAndStateAbbreviation( explicitElig.getStudentId(), explicitElig.getStateAbbreviation())); } if (!tmpStudents.isEmpty()) { this.eligibilityService.saveAssociations(source, tmpStudents); } break; default: LOGGER.error("Unknown eligbility type: " + source.getEligibilityType()); throw new EligibilityException("eligibility.unknown.type"); } } private void assessmentDeleted(final AssessmentModificationEvent event) { this.eligibilityService.removeAllAssociationsForAssessment(event.getSource()); } @ServiceActivator(inputChannel = "explicitEligibilityModificationEventsInbound") @Override public void evaluateExplicitEligibilityModification(final ExplicitEligibilityModificationEvent event) { LOGGER.debug("Handling an ExplicitEligibilityModificationEvent: " + event); switch (event.getAction()) { case UPD: explicitEligibilityModified(event); break; case DEL: explicitEligibilityDeleted(event); break; default: // unknown, throw error here LOGGER.error("Unknown action: " + event.getAction()); throw new EligibilityException("eligibility.unknown.event"); } } private void explicitEligibilityModified(final ExplicitEligibilityModificationEvent event) { // this is really just ADD, there is no update of explicit eligibility // find the student by student id // find the assessment by alternate key final ExplicitEligibility explicitEligibility = event.getSource(); final Student student = this.studentRepository.findByEntityIdAndStateAbbreviation( explicitEligibility.getStudentId(), event.getSource().getStateAbbreviation()); Assessment assessment = eligibilityDependencyResolver.resolveAssessmentForStudent(explicitEligibility); // associate the two this.eligibilityService.saveAssociation(assessment, student); } private void explicitEligibilityDeleted(final ExplicitEligibilityModificationEvent event) { // find the student by student id // find the assessment by alternate key final Student student = this.studentRepository.findByEntityIdAndStateAbbreviation( event.getSource().getStudentId(), event.getSource().getStateAbbreviation()); Assessment assessment = eligibilityDependencyResolver.resolveAssessmentForStudent(event.getSource()); // remove the association this.eligibilityService.removeAssociation(assessment, student); } @Override public void recalculateAllEligibility() { // remove the eligible student collection this.eligibilityService.removeEligibleStudentCollection(); // load all the Assessments in the system and create assessment modified events for each // loop and call assessmentModified() for each final List<Assessment> allAssessments = this.assessmentRepository.findAll(); final List<AssessmentModificationEvent> events = Lists.transform(allAssessments, ARTHelpers.ASSESSMENT_TRANSFORMER); for (final AssessmentModificationEvent event : events) { try { evaluateAssessmentModification(event); } catch (final Exception e) { LOGGER.warn("Caught exception while re-evaluating all eligibility, ignoring: ", e); } } // done! } private List<Student> evaluateImplicitRules(final Assessment assessment, final boolean shouldSave, final Student... students) { final List<Student> eligibleStudents = Lists.newArrayList(); final List<ImplicitEligibilityRule> enablerRules = assessment.getEnablerRules(); final List<ImplicitEligibilityRule> disablerRules = assessment.getDisablerRules(); boolean disabled = false; boolean enabled = false; for (final Student student : students) { disabled = false; enabled = false; if (!CollectionUtils.isEmpty(disablerRules)) { disabled = true; // run disabler rules first for (final ImplicitEligibilityRule rule : disablerRules) { if (!(disabled = applyRule(assessment.getSubjectCode(), rule, student, rule.getField()))) { break; } } } if (!disabled) { // run enabler rules if (!CollectionUtils.isEmpty(enablerRules)) { enabled = true; for (final ImplicitEligibilityRule rule : enablerRules) { if (!(enabled = applyRule(assessment.getSubjectCode(), rule, student, rule.getField()))) { break; } } } } if (!disabled && enabled) { eligibleStudents.add(student); } } if (shouldSave && !CollectionUtils.isEmpty(eligibleStudents)) { // students that are eligible for the assessment, make the associations this.eligibilityService.saveAssociations(assessment, eligibleStudents); } return eligibleStudents; } private boolean applyRule(final String assessmentSubjectCode, final ImplicitEligibilityRule rule, final Student student, final String fieldName) { Object reflectedValue = null; try { reflectedValue = PropertyUtils.getProperty(student, fieldName); if (reflectedValue == null || !evaluateOperator(reflectedValue, rule)) { return false; } } catch (IllegalAccessException | InvocationTargetException | NoSuchMethodException e) { LOGGER.debug("Field '" + fieldName + "' is not on Student, looking at Accommodations"); final Map<String, Object> accommodation = student.getAccommodation(assessmentSubjectCode); if (accommodation != null) { try { reflectedValue = accommodation.get(fieldName); } catch (Exception e1) { LOGGER.error("Failure in eligibility eval can't find field called " + fieldName + " in accommodation, invalid rule", e1); throw new EligibilityException("eligibility.invalid.compare.field", new String[] { fieldName }, e); } } else { LOGGER.debug("Accommodation not found for assessment with subject " + assessmentSubjectCode + " skipping getProperty"); } if (reflectedValue == null) { return false; } else { if (reflectedValue instanceof ArrayList) { ArrayList<?> valueList = (ArrayList<?>) reflectedValue; for (Object value : valueList) { if (evaluateOperator(value, rule) == true) { return true; } } return false; } else { if (!evaluateOperator(reflectedValue, rule)) { return false; } } } } return true; } @SuppressWarnings("unchecked") private boolean evaluateOperator(final Object value, final ImplicitEligibilityRule rule) { if (rule.getOperatorType().isValidFor(value.getClass())) { switch (rule.getOperatorType()) { case EQUALS: return value.equals(convertTo(rule.getValue(), value.getClass())); case GREATER_THAN: return ((Comparable<Object>) value).compareTo(convertTo(rule.getValue(), value.getClass())) > 0; case GREATER_THAN_EQUALS: return ((Comparable<Object>) value).compareTo(convertTo(rule.getValue(), value.getClass())) >= 0; case LESS_THAN: return ((Comparable<Object>) value).compareTo(convertTo(rule.getValue(), value.getClass())) < 0; case LESS_THAN_EQUALS: return ((Comparable<Object>) value).compareTo(convertTo(rule.getValue(), value.getClass())) <= 0; default: LOGGER.error("The class type " + value.getClass().toString() + " cannot be compared using the operator " + rule.getOperatorType().name()); throw new EligibilityException("eligiblity.invalid.operator.forclass", new String[] { value.getClass().toString(), rule.getOperatorType().name() }); } } else { LOGGER.error("The class type " + value.getClass().toString() + " cannot be compared using the operator " + rule.getOperatorType().name()); throw new EligibilityException("eligiblity.invalid.operator.forclass", new String[] { value.getClass().toString(), rule.getOperatorType().name() }); } } @SuppressWarnings("unchecked") private <T> T convertTo(final String value, final Class<T> clazz) { try { if (String.class.isAssignableFrom(clazz)) { return (T) value; } else if (Number.class.isAssignableFrom(clazz)) { return clazz.getConstructor(String.class).newInstance(value); } else if (DateTime.class.isAssignableFrom(clazz)) { DateTimeFormatter formatter = DateTimeFormat.forPattern("yyyy-MM-dd'T'HH:mm:ss.SSSZ"); DateTime dateTime = formatter.parseDateTime(value); return (T) dateTime; } else if (Enum.class.isAssignableFrom(clazz)) { Method getEnum = null; try { getEnum = clazz.getDeclaredMethod("getEnumByValue", String.class); } catch (final NoSuchMethodException me) { getEnum = clazz.getMethod("valueOf", String.class); } return (T) getEnum.invoke(null, value); } else { LOGGER.error("Failure to convert value \"" + value + "\" into class type " + clazz.toString()); throw new EligibilityException("eligibility.value.convert.error", new String[] { value, clazz.toString() }); } } catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException | SecurityException e) { LOGGER.error("Failure to convert value \"" + value + "\" into class type " + clazz.toString(), e); throw new EligibilityException("eligibility.value.convert.error", new String[] { value, clazz.toString() }, e); } } }