Java tutorial
/** * Licensed to Apereo under one or more contributor license * agreements. See the NOTICE file distributed with this work * for additional information regarding copyright ownership. * Apereo licenses this file to you under the Apache License, * Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a * copy of the License at the following location: * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. */ package org.jasig.ssp.service.impl; import com.google.common.base.Function; import com.google.common.base.Predicate; import com.google.common.base.Splitter; import com.google.common.collect.Iterables; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import org.apache.commons.collections.CollectionUtils; import org.apache.commons.lang.StringUtils; import org.jasig.ssp.model.ObjectStatus; import org.jasig.ssp.model.Person; import org.jasig.ssp.model.external.ExternalStudentFinancialAid; import org.jasig.ssp.model.external.ExternalStudentRiskIndicator; import org.jasig.ssp.model.external.ExternalStudentTranscript; import org.jasig.ssp.model.external.PlanStatus; import org.jasig.ssp.model.external.RegistrationStatusByTerm; import org.jasig.ssp.model.external.Term; import org.jasig.ssp.model.reference.Blurb; import org.jasig.ssp.model.reference.SuccessIndicator; import org.jasig.ssp.service.EarlyAlertService; import org.jasig.ssp.service.EvaluatedSuccessIndicatorService; import org.jasig.ssp.service.MapStatusService; import org.jasig.ssp.service.ObjectNotFoundException; import org.jasig.ssp.service.PersonService; import org.jasig.ssp.service.SecurityService; import org.jasig.ssp.service.TaskService; import org.jasig.ssp.service.external.ExternalStudentFinancialAidService; import org.jasig.ssp.service.external.ExternalStudentRiskIndicatorService; import org.jasig.ssp.service.external.ExternalStudentTranscriptService; import org.jasig.ssp.service.external.RegistrationStatusByTermService; import org.jasig.ssp.service.external.TermService; import org.jasig.ssp.service.reference.BlurbService; import org.jasig.ssp.service.reference.SuccessIndicatorService; import org.jasig.ssp.transferobject.EvaluatedSuccessIndicatorTO; import org.jasig.ssp.transferobject.SuccessIndicatorEvaluation; import org.jasig.ssp.transferobject.external.AbstractPlanStatusReportTO; import org.jasig.ssp.transferobject.jsonserializer.DateOnlyFormatting; import org.jasig.ssp.util.SspStringUtils; import org.jasig.ssp.util.collections.Pair; import org.jasig.ssp.util.sort.PagingWrapper; import org.jasig.ssp.util.sort.SortingAndPaging; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.PlatformTransactionManager; import org.springframework.transaction.TransactionStatus; import org.springframework.transaction.UnexpectedRollbackException; import org.springframework.transaction.support.TransactionCallbackWithoutResult; import org.springframework.transaction.support.TransactionTemplate; import javax.annotation.Nonnull; import javax.annotation.Nullable; import java.math.BigDecimal; import java.math.RoundingMode; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.UUID; import java.util.concurrent.atomic.AtomicReference; import static org.jasig.ssp.util.sort.SortingAndPaging.allActive; @Service public class EvaluatedSuccessIndicatorServiceImpl implements EvaluatedSuccessIndicatorService { private static final Logger LOGGER = LoggerFactory.getLogger(EvaluatedSuccessIndicatorServiceImpl.class); private static final int DECIMAL_SCALE = 2; private static final RoundingMode ROUNDING_MODE = RoundingMode.HALF_UP; private static final Function<String, String> LOWER_CASER = new Function<String, String>() { @Override public String apply(@Nullable String input) { return input.toLowerCase(); } }; private static final Predicate<String> BLANK_STRING_TEST = new Predicate<String>() { @Override public boolean apply(@Nullable String input) { return StringUtils.isBlank(input); } }; private static final Predicate<String> NON_BLANK_STRING_TEST = new Predicate<String>() { @Override public boolean apply(@Nullable String input) { return !(BLANK_STRING_TEST.apply(input)); } }; // Must generate keys corresponding to those generated by RISK_SUCCESS_INDICATOR_MAP_KEY_GENERATOR private static final Function<ExternalStudentRiskIndicator, String> RISK_INDICATOR_MAP_KEY_GENERATOR = new Function<ExternalStudentRiskIndicator, String>() { @Override public String apply(@Nullable ExternalStudentRiskIndicator input) { return new StringBuilder(input.getModelCode()).append("::").append(input.getIndicatorCode()).toString(); } }; // Must generate keys corresponding to those generated by RISK_INDICATOR_MAP_KEY_GENERATOR private static final Function<SuccessIndicator, String> RISK_SUCCESS_INDICATOR_MAP_KEY_GENERATOR = new Function<SuccessIndicator, String>() { @Override public String apply(@Nullable SuccessIndicator input) { return new StringBuilder(input.getModelCode()).append("::").append(input.getCode()).toString(); } }; private static final Function<Blurb, String> BLURB_MAP_KEY_GENERATOR = new Function<Blurb, String>() { @Override public String apply(@Nullable Blurb input) { return input.getCode(); } }; private static final String TRANSCRIPT_INDICATOR_METRIC_KEY = "TRANSCRIPT"; private static final String FINANCIAL_AID_INDICATOR_METRIC_KEY = "FINANCIAL_AID"; private static final String EXTERNAL_RISK_INDICATOR_METRIC_KEY = "EXTERNAL_RISK"; private static final String EVALUATION_DISPLAY_NAMES_KEY = "DISPLAY_NAMES"; private static final String BLURB_SEPARATOR = "."; private static final String EVALUATION_DISPLAY_NAMES_BLURB_PREFIX = "ssp.success.indicator.evaluation"; private static final String EVALUATION_DISPLAY_NAMES_BLURB_QUERY = EVALUATION_DISPLAY_NAMES_BLURB_PREFIX + BLURB_SEPARATOR + "*"; @Autowired private SuccessIndicatorService successIndicatorService; @Autowired private ExternalStudentTranscriptService externalStudentTranscriptService; @Autowired private RegistrationStatusByTermService registrationStatusByTermService; @Autowired private TermService termService; @Autowired private PersonService personService; @Autowired private TaskService taskService; @Autowired private SecurityService securityService; @Autowired private EarlyAlertService earlyAlertService; @Autowired private MapStatusService mapStatusService; @Autowired private ExternalStudentFinancialAidService externalStudentFinancialAidService; @Autowired private ExternalStudentRiskIndicatorService externalStudentRiskIndicatorService; @Autowired private BlurbService blurbService; @Autowired private PlatformTransactionManager platformTransactionManager; private ThreadLocal<Map<String, Object>> evaluationResourceCache = new ThreadLocal<>(); @Override public List<EvaluatedSuccessIndicatorTO> getForPerson(final UUID personId, final ObjectStatus status) throws ObjectNotFoundException { // Elaborate transaction management workaround b/c we can't avoid opening a transaction, but any exception // that crosses a transactional boundary in the code will mark the transaction as rollback only, which is // fine except that if we just tag this method with @Transactional(readOnly=true), the transaction manager // will still attempt a commit if the exception doesn't exit all the way out of this method (which is // what we usually want in this specific case - we want to try to return as many indicators as we can). And // if you attempt to commit a transaction marked as rollback only, you get a // org.springframework.transaction.UnexpectedRollbackException TransactionTemplate transactionTemplate = new TransactionTemplate(platformTransactionManager); transactionTemplate.setReadOnly(true); final AtomicReference<List<EvaluatedSuccessIndicatorTO>> rsltHolder = new AtomicReference<>(); final AtomicReference<ObjectNotFoundException> onfeHolder = new AtomicReference<>(); try { transactionTemplate.execute(new TransactionCallbackWithoutResult() { @Override protected void doInTransactionWithoutResult(TransactionStatus txnStatus) { try { getForPersonInTransaction(personId, status, rsltHolder); } catch (ObjectNotFoundException e) { onfeHolder.set(e); throw new RuntimeException("Rolling back transaction", e); } } }); } catch (UnexpectedRollbackException e) { // nothing to be done, totally normal. see comments above. } catch (RuntimeException e) { if (onfeHolder.get() == null) { throw e; } // otherwise it's just us, rolling back the transaction, nothing to be done, totally normal } if (onfeHolder.get() != null) { throw onfeHolder.get(); } return rsltHolder.get(); } private void getForPersonInTransaction(UUID personId, ObjectStatus status, AtomicReference<List<EvaluatedSuccessIndicatorTO>> rsltHolder) throws ObjectNotFoundException { final Person person = findPersonOrFail(personId); final PagingWrapper<SuccessIndicator> successIndicators = successIndicatorService.getAll(allActive()); if (successIndicators.getResults() <= 0L) { rsltHolder.set(Lists.<EvaluatedSuccessIndicatorTO>newArrayListWithCapacity(0)); return; } final ArrayList<EvaluatedSuccessIndicatorTO> evaluations = Lists .newArrayListWithExpectedSize((int) successIndicators.getResults()); evaluationResourceCache.set(Maps.<String, Object>newLinkedHashMap()); try { for (SuccessIndicator successIndicator : successIndicators) { try { final EvaluatedSuccessIndicatorTO evaluation = evaluate(successIndicator, person); if (evaluation != null) { evaluations.add(evaluation); } } catch (Exception e) { // This rarely happens b/c evaluate() should be catching nearly all problems since the goal // is to aways return an evaluation TO with as much possible info for all active indicators, even // if the eval fails. If we tried to do that here, we wouldn't be able to output metric values in // the TO, for example. So while this is a handled exception, it does indicate a more serious // problem (probably a bad indicator code) than elsewhere in this class, so logging at a higher // level. LOGGER.error("System failure evaluating success indicator [{}] for person [{}]", new Object[] { successIndicatorLoggingId(successIndicator), person.getId(), e }); } } } finally { evaluationResourceCache.set(null); } rsltHolder.set(evaluations); } private EvaluatedSuccessIndicatorTO evaluate(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { // Have struggled w/ the right cohesion level for this method. Currently responsible for building the // full evaluation response *and* calculating the eval itself, including selection of 'no data' and/or 'no // match' evaluations in the event the type specific processing produces or cannot be expected to produce an // evaluation. Previous revisions did split that up but ended up being difficult to name and suprisingly // difficult to understand. So for now it's all clumped up together. String metricDisplay = null; SuccessIndicatorEvaluation evaluation = null; Exception failure = null; try { final Pair<Object, String> metricDescriptor = findMetric(successIndicator, person); final Object metricValue = metricDescriptor.getFirst(); metricDisplay = metricDescriptor.getSecond(); final boolean isMetric = !(isEmptyNonNormalizedMetric(metricValue, successIndicator, person)); if (!(isMetric)) { evaluation = successIndicator.getNoDataExistsEvaluation(); } if (evaluation == null) { // indicator value i.e. metric normalization and existence check at this layer so it can be responsible // centrally for both 'no data' and 'no match' handling switch (successIndicator.getEvaluationType()) { case SCALE: BigDecimal normalizedScaleMetric = null; try { normalizedScaleMetric = normalizeForScaleEvaluation(metricValue, successIndicator); } catch (NumberFormatException e) { // see rationale for debug level logging elsewhere in this class LOGGER.debug( "Failed to find numeric representation of metric [{}] for person [{}] for evaluation against a " + "numeric scale using success indicator [{}]", new Object[] { metricValue, person.getId(), successIndicatorLoggingId(successIndicator), e }); // we have data, we just can't narrow its type for evaluation against a numeric scale, so this is // a non-match, which is handled outside the switch failure = e; break; } // anything else is a programmer error and we can't really distinguish between 'no data' and 'no match' so raise it // shouldn't happen, but just in case, and b/c this layer is responsible for 'no data' evaluations of // normalized indicator metrics if (normalizedScaleMetric == null) { evaluation = successIndicator.getNoDataExistsEvaluation(); } evaluation = evaluateScale(normalizedScaleMetric, successIndicator, person); break; case STRING: // any normalization problem is a programmer error and we can't really distinguish between 'no data' and // 'no match' so raise it final List<String> normalizedStringMetric = normalizeForStringEvaluation(metricValue, successIndicator); if (isEmptyNormalizedString(normalizedStringMetric)) { evaluation = successIndicator.getNoDataExistsEvaluation(); } evaluation = evaluateString(normalizedStringMetric, successIndicator, person); break; default: throw new UnsupportedOperationException("Unrecognized evaluation type [" + successIndicator.getEvaluationType() + "] in success indicator [" + successIndicatorLoggingId(successIndicator) + "]"); } } if (evaluation == null) { evaluation = successIndicator.getNoDataMatchesEvaluation(); } } catch (Exception e) { // debug b/c this is completely a handled exception... if you're wondering why you're not // getting the evaluation statuses you expect, enable debug logging and have a look. LOGGER.debug("Failed to evaluate success indicator [{}] for person [{}]", new Object[] { successIndicatorLoggingId(successIndicator), person.getId(), e }); // TODO would be nice to have a better 'error' eval to return, or at least a field // to set on the evaluated indicator TO to indicate an error occurred so the // eval might be misleading metricDisplay = null; failure = e; evaluation = successIndicator.getNoDataExistsEvaluation(); } // wait to set display value to make sure there wasn't an error during eval final EvaluatedSuccessIndicatorTO indicatorTO = newBaseEvaluation(successIndicator, person); indicatorTO.setDisplayValue( StringUtils.isBlank(metricDisplay) ? (failure == null ? "[NO DATA]" : "[ERROR]") : metricDisplay); indicatorTO.setEvaluation(evaluation); indicatorTO.setEvaluationDisplayName(findEvaluationDisplayName(evaluation)); return indicatorTO; } private Pair<Object, String> findMetric(SuccessIndicator successIndicator, Person person) { switch (successIndicator.getIndicatorGroup()) { case STUDENT: return findMetricInStudentIndicatorGroup(successIndicator, person); case INTERVENTION: return findMetricInInterventionIndicatorGroup(successIndicator, person); case RISK: return findMetricInRiskIndicatorGroup(successIndicator, person); default: throw new IllegalStateException("Unexpected indicator group [" + successIndicator.getIndicatorGroup() + "] for indicator [" + successIndicatorLoggingId(successIndicator) + "]"); } } /** * Just a preliminary check of the non-normalized indicator value, i.e. metric. Has to be re-checked after * normalization, which is specific to SuccessIndicator#evaluationType. */ private boolean isEmptyNonNormalizedMetric(@Nullable Object metric, @Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { return metric == null || ((metric instanceof String) && StringUtils.isBlank((String) metric)); } private SuccessIndicatorEvaluation evaluateScale(@Nonnull BigDecimal normalizedMetric, @Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { SuccessIndicatorEvaluation rslt = null; BigDecimal from = successIndicator.getScaleEvaluationLowFrom(); BigDecimal to = successIndicator.getScaleEvaluationLowTo(); rslt = evaluateScaleInRange(normalizedMetric, from, to, SuccessIndicatorEvaluation.LOW); if (rslt != null) { return rslt; } from = successIndicator.getScaleEvaluationMediumFrom(); to = successIndicator.getScaleEvaluationMediumTo(); rslt = evaluateScaleInRange(normalizedMetric, from, to, SuccessIndicatorEvaluation.MEDIUM); if (rslt != null) { return rslt; } from = successIndicator.getScaleEvaluationHighFrom(); to = successIndicator.getScaleEvaluationHighTo(); rslt = evaluateScaleInRange(normalizedMetric, from, to, SuccessIndicatorEvaluation.HIGH); return rslt; // will be null if no match, which is handled at a higher layer } private SuccessIndicatorEvaluation evaluateScaleInRange(@Nonnull BigDecimal normalizedMetric, @Nullable BigDecimal from, @Nullable BigDecimal to, @Nonnull SuccessIndicatorEvaluation resultOnMatch) { return isInRange(normalizedMetric, from, to) ? resultOnMatch : null; } private boolean isInRange(BigDecimal metric, BigDecimal from, BigDecimal to) { if (from == null && to == null) { return false; } final boolean gteFrom = from == null || from.compareTo(metric) <= 0; return gteFrom && (to == null || to.compareTo(metric) >= 0); } private BigDecimal normalizeForScaleEvaluation(@Nullable Object metric, @Nonnull SuccessIndicator successIndicator) throws NumberFormatException, IllegalArgumentException { if (metric == null) { return null; } if (metric instanceof Boolean) { return ((Boolean) metric).booleanValue() ? BigDecimal.ONE : BigDecimal.ZERO; } if (metric instanceof BigDecimal) { return ((BigDecimal) metric); } if (metric instanceof Number) { return BigDecimal.valueOf(((Number) metric).doubleValue()).setScale(DECIMAL_SCALE); } if (metric instanceof String) { return new BigDecimal((String) metric).setScale(DECIMAL_SCALE); } if (metric instanceof Ratio) { return ((Ratio) metric).ratio(); } throw new NumberFormatException("Cannot interpret evaluation input value [" + metric + "] of type [" + metric.getClass().getName() + "] as a BigDecimal"); } private SuccessIndicatorEvaluation evaluateString(@Nonnull List<String> normalizedMetric, @Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { SuccessIndicatorEvaluation rslt = null; String evalRule = successIndicator.getStringEvaluationLow(); rslt = evaluateStringMatchingRule(normalizedMetric, evalRule, SuccessIndicatorEvaluation.LOW, successIndicator, person); if (rslt != null) { return rslt; } evalRule = successIndicator.getStringEvaluationMedium(); rslt = evaluateStringMatchingRule(normalizedMetric, evalRule, SuccessIndicatorEvaluation.MEDIUM, successIndicator, person); if (rslt != null) { return rslt; } evalRule = successIndicator.getStringEvaluationHigh(); rslt = evaluateStringMatchingRule(normalizedMetric, evalRule, SuccessIndicatorEvaluation.HIGH, successIndicator, person); return rslt; // will be null if no match, which is handled at a higher layer } private SuccessIndicatorEvaluation evaluateStringMatchingRule(@Nonnull List<String> normalizedMetric, @Nullable String rawRule, @Nonnull SuccessIndicatorEvaluation resultOnMatch, @Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { List<String> normalizedRule = null; try { normalizedRule = normalizeForStringEvaluation(rawRule, successIndicator); } catch (RuntimeException e) { // see rationale for debug level logging elsewhere in this class LOGGER.debug("Failed to evaluate metric [{}] for person [{}] for against " + "STRING rule [{}] for result [{}] using success indicator [{}]. String rule is likely malformed.", new Object[] { normalizedMetric, person.getId(), rawRule, resultOnMatch, successIndicatorLoggingId(successIndicator), e }); // rule itself is formatted badly or otherwise non-normalizable, so handle the same way we do for // scale evals and treat this as a 'no match' return null; } if (isEmptyNormalizedString(normalizedRule)) { return null; } return CollectionUtils.containsAny(normalizedMetric, normalizedRule) ? resultOnMatch : null; } private List<String> normalizeForStringEvaluation(@Nullable Object forStringEval, @Nonnull SuccessIndicator successIndicator) throws IllegalArgumentException { if (forStringEval == null) { return Lists.newArrayListWithCapacity(0); } // TODO fix excessive List creation Iterable<String> translated = null; if (forStringEval instanceof Boolean) { translated = Lists.newArrayList(translateForStringEvaluation((Boolean) forStringEval)); } else if (forStringEval instanceof Ratio) { final BigDecimal divided = ((Ratio) forStringEval).ratio(); translated = Lists.newArrayList(divided.toPlainString()); } else if (forStringEval instanceof BigDecimal) { translated = Lists.newArrayList(((BigDecimal) forStringEval).toPlainString()); } else { // Otherwise skipping any sort of numeric value formatting b/c it's hard to know what sort of formatting and // scale people might want e.g. should we *always* format to some number of decimal points, even for int types? // In reality you shouldnt be using string matching for numeric metrics so whatever we do here really isn't all // that important, but at least this way you don't have to memorize any rules about how metric values are // normalized before comparison other than that they're trimmed, canonicalized to lower case, and split on // commas. // // Fully expect this to become more sophisticated in the future, where the SuccessIndicator specifies how // the metric should be normalized, e.g. to turn all the behaviors listed above on/off. // Thanks in part to http://eclipsesource.com/blogs/2012/07/26/having-fun-with-guavas-string-helpers/ translated = Splitter.on(",").omitEmptyStrings().trimResults().split(forStringEval.toString()); } return normalizeCasingForStringEvaluation(translated); } private List<String> normalizeCasingForStringEvaluation(Iterable<String> strings) { return Lists.newArrayList(Iterables.transform(strings, LOWER_CASER)); } private boolean isEmptyNormalizedString(@Nullable List<String> normalizedStrings) { return normalizedStrings == null || normalizedStrings.isEmpty() || !(Iterables.any(normalizedStrings, NON_BLANK_STRING_TEST)); } private String booleanMetricDisplay(@Nullable Boolean booleanMetric) { return SspStringUtils.shortYesNoFromBoolean(booleanMetric); } private String translateForStringEvaluation(@Nullable Boolean bool) { // case canonicalization handled elsewhere to try to keep it DRY. So be sure to treat this exclusively // as a delegate of normalizeForStringEvaluation() return booleanMetricDisplay(bool); } private EvaluatedSuccessIndicatorTO newBaseEvaluation(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { final EvaluatedSuccessIndicatorTO indicatorTO = new EvaluatedSuccessIndicatorTO(); indicatorTO.setPersonId(person.getId()); indicatorTO.setIndicatorId(successIndicator.getId()); indicatorTO.setId(indicatorTO.getIndicatorId().toString() + "::" + indicatorTO.getPersonId()); indicatorTO.setIndicatorGroupCode(successIndicator.getIndicatorGroup()); indicatorTO.setObjectStatus(successIndicator.getObjectStatus()); indicatorTO.setIndicatorName(successIndicator.getName()); indicatorTO.setIndicatorDescription(successIndicator.getDescription()); indicatorTO.setIndicatorCode(successIndicator.getCode()); indicatorTO.setIndicatorSortOrder(successIndicator.getSortOrder()); indicatorTO.setIndicatorModelCode(successIndicator.getModelCode()); indicatorTO.setIndicatorModelName(successIndicator.getModelName()); return indicatorTO; } private static class Ratio { private BigDecimal numerator; private BigDecimal denominator; private Ratio(long numerator, long denominator) { this(new BigDecimal(numerator), new BigDecimal(denominator)); } private Ratio(@Nonnull BigDecimal numerator, @Nonnull BigDecimal denominator) { this.numerator = numerator; this.denominator = denominator; } private BigDecimal ratio() { if (BigDecimal.ZERO.compareTo(denominator) == 0) { return BigDecimal.ZERO; } return numerator.divide(denominator, DECIMAL_SCALE, ROUNDING_MODE); } } // Now all the indicator/metric-specific code... // TODO refactor to externalize per-indicator 'metric' extractions... this class is getting out of hand trying do // them all inline. Should have individual services register 'indicator metric providers' to perform those // extractions private Pair<Object, String> findMetricInStudentIndicatorGroup(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { switch (successIndicator.getCode()) { case "system.student.gpa": return findGpaMetric(successIndicator, person); case "system.student.registration": return findRegistrationMetric(successIndicator, person); case "system.student.creditcompletion": return findCreditCompletionMetric(successIndicator, person); case "system.student.standing": return findStandingMetric(successIndicator, person); case "system.student.sap": return findSapMetric(successIndicator, person); case "system.student.restrictions": return findRestrictionsMetric(successIndicator, person); default: throw new UnsupportedOperationException( "Unrecognized student indicator code [" + successIndicator.getCode() + "] in success indicator [" + successIndicatorLoggingId(successIndicator) + "]"); } } private Pair<Object, String> findGpaMetric(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { final ExternalStudentTranscript transcript = findTranscriptFor(person); BigDecimal gpa = null; if (transcript != null) { gpa = transcript.getGradePointAverage(); } return new Pair<Object, String>(gpa, (gpa == null ? null : gpa.toString())); } private static enum RegistrationStatusMetric { NONE { @Override public RegistrationStatusMetric foundCurrent() { return CURRENT; } @Override public RegistrationStatusMetric foundFuture() { return FUTURE; } }, CURRENT { @Override public RegistrationStatusMetric foundFuture() { return CURRENT_AND_FUTURE; } }, FUTURE { @Override public RegistrationStatusMetric foundCurrent() { return CURRENT_AND_FUTURE; } }, CURRENT_AND_FUTURE { @Override public String screenDisplayName() { return "CURRENT+FUTURE"; } }; public String screenDisplayName() { return this.name(); } public RegistrationStatusMetric foundCurrent() { return this; } public RegistrationStatusMetric foundFuture() { return this; } } private Pair<Object, String> findRegistrationMetric(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { List<Term> currentAndFutureTerms = null; try { currentAndFutureTerms = termService.getCurrentAndFutureTerms(); } catch (ObjectNotFoundException e) { // nothing to be done - missing terms handled below } final boolean areCurrentOrFutureTerms = currentAndFutureTerms != null && !(currentAndFutureTerms.isEmpty()); if (!(areCurrentOrFutureTerms)) { return emptyMetricDescriptor(); } final Map<String, Term> currentAndFutureTermsByCode = Maps.newLinkedHashMap(); for (Term term : currentAndFutureTerms) { currentAndFutureTermsByCode.put(term.getCode(), term); } Term currentTerm = null; try { currentTerm = termService.getCurrentTerm(); } catch (ObjectNotFoundException e) { // nothing to be done - missing terms handled below } final boolean isCurrentTerm = currentTerm != null; List<RegistrationStatusByTerm> regStatuses = null; try { regStatuses = registrationStatusByTermService.getCurrentAndFutureTerms(person); } catch (ObjectNotFoundException e) { // really shouldn't happen, but if it does, indicates all current/future terms have gone missing, so // handle it the same was as in that check above return emptyMetricDescriptor(); } if (regStatuses == null || regStatuses.isEmpty()) { // current/future terms exist, but this person has no registration records in any of them. // or has zeroes in all of them. treat as if they are all zeroes return new Pair<Object, String>(RegistrationStatusMetric.NONE.name(), RegistrationStatusMetric.NONE.screenDisplayName()); } RegistrationStatusMetric regStatusMetric = RegistrationStatusMetric.NONE; for (RegistrationStatusByTerm regStatus : regStatuses) { final int regCnt = regStatus.getRegisteredCourseCount(); if (regCnt > 0) { final String regStatusTermCode = regStatus.getTermCode(); if (isCurrentTerm && currentTerm.getCode().equals(regStatusTermCode)) { regStatusMetric = regStatusMetric.foundCurrent(); } else if (currentAndFutureTermsByCode.containsKey(regStatusTermCode)) { regStatusMetric = regStatusMetric.foundFuture(); } else { // nothing to do... this reg status really shouldnt even be in the list } } else { // no point really in even logging this... it's a just a vanilla "no registrations in this term" record } } return new Pair<Object, String>(regStatusMetric, regStatusMetric.screenDisplayName()); } private Pair<Object, String> findCreditCompletionMetric(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { final ExternalStudentTranscript transcript = findTranscriptFor(person); BigDecimal completionRatio = null; if (transcript != null) { completionRatio = transcript.getCreditCompletionRate(); } return new Pair<Object, String>(completionRatio, (completionRatio == null ? null : completionRatio.toString() + "%")); } private Pair<Object, String> findStandingMetric(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { final ExternalStudentTranscript transcript = findTranscriptFor(person); String standing = null; if (transcript != null) { standing = transcript.getAcademicStanding(); } return new Pair<Object, String>(standing, standing); } private Pair<Object, String> findSapMetric(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { final ExternalStudentFinancialAid fa = findFinancialAidFor(person); String sap = null; if (fa != null) { sap = fa.getSapStatusCode(); } return new Pair<Object, String>(sap, sap); } private Pair<Object, String> findRestrictionsMetric(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { final ExternalStudentTranscript transcript = findTranscriptFor(person); String restrictions = null; if (transcript != null) { restrictions = transcript.getCurrentRestrictions(); } return new Pair<Object, String>(restrictions, restrictions); } private Pair<Object, String> findMetricInInterventionIndicatorGroup(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { switch (successIndicator.getCode()) { case "system.intervention.intakesubmitted": return findIntakeSubmittedMetric(successIndicator, person); case "system.intervention.opentasks": return findOpenTasksMetric(successIndicator, person); case "system.intervention.openalerts": return findOpenAlertsMetric(successIndicator, person); case "system.intervention.mapstatus": return findMapStatusMetric(successIndicator, person); default: throw new UnsupportedOperationException( "Unrecognized intervention indicator code [" + successIndicator.getCode() + "] in success indicator [" + successIndicatorLoggingId(successIndicator) + "]"); } } private Pair<Object, String> findIntakeSubmittedMetric(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { Date intakeSubmissionDate = person.getStudentIntakeCompleteDate(); // Doesn't go for all boolean indicators you could imagine, but for this one there is no conceptual distinction // between 'no data' and 'false' return new Pair<Object, String>(new Boolean(intakeSubmissionDate != null), formatIntakeSubmissionDateForDisplay(intakeSubmissionDate)); } private String formatIntakeSubmissionDateForDisplay(@Nullable Date intakeSubmissionDate) { if (intakeSubmissionDate == null) { return null; } return DateOnlyFormatting.dateFormatter().format(intakeSubmissionDate); } private Pair<Object, String> findOpenTasksMetric(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { // Special service method invented specifically for this use case... without it, pulling back // a list of actual Tasks to filter ends up taking easily half the elapsed time of the entire // getForPerson(). The query generated by a getAllForPerson() is nightmarish. final Pair<Long, Long> openVsClosed = taskService.getOpenVsClosedTaskCountsForPerson(person); // No conceptual difference between 'no data' and 0's here. final long open = openVsClosed.getFirst(); final long closed = openVsClosed.getSecond(); final long total = open + closed; final String display = new StringBuilder().append(open).append("/").append(total).toString(); return new Pair<Object, String>(open, display); } private Pair<Object, String> findOpenAlertsMetric(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { // Would obviously need to change if we have more states in the future, but for now this is the most efficient // solution. final Number activeRaw = person.getActiveAlertsCount(); final Number closedRaw = person.getClosedAlertsCount(); // No conceptual difference between 'no data' and 0's here. final long active = activeRaw == null ? 0L : activeRaw.longValue(); final long closed = closedRaw == null ? 0L : closedRaw.longValue(); final long total = active + closed; final String display = new StringBuilder().append(active).append("/").append(total).toString(); return new Pair<Object, String>(active, display); } private Pair<Object, String> findMapStatusMetric(SuccessIndicator successIndicator, Person person) { AbstractPlanStatusReportTO statusReport = null; try { statusReport = mapStatusService.getByPersonId(person.getId()); } catch (ObjectNotFoundException e) { // actually means 'no such person', which means something terrible has gone wrong, but we'll treat it // as just a missing indicator metric return new Pair<Object, String>(null, null); } // PlanStatus is so domain specific, we don't pass it back into the evaluate(). That function does have // coercion/translation/normalization capabilities, but they're all focused on very low-level types, // e.g. Numeric and BigDecimal, or purpose built but still very generic stuff, e.g. Ratio. So // we translate that enum into a name here, the same way we do it for findRegistrationMetric() final PlanStatus status = statusReport == null ? null : statusReport.getStatus(); return new Pair<Object, String>((status == null ? null : status.name()), (status == null ? null : status.getDisplayName())); } private Pair<Object, String> findMetricInRiskIndicatorGroup(@Nonnull SuccessIndicator successIndicator, @Nonnull Person person) { final Map<String, ExternalStudentRiskIndicator> externalRiskIndicators = findExternalRiskIndicatorsFor( person); if (externalRiskIndicators.isEmpty()) { // we're guaranteed the list is non-null return emptyMetricDescriptor(); } final ExternalStudentRiskIndicator externalRiskIndicator = externalRiskIndicators .get(externalRiskMapKeyFor(successIndicator)); if (externalRiskIndicator == null) { return emptyMetricDescriptor(); } // TODO probably useful to have a way to get at the normalized representation of indicatorValue for display // purposes return new Pair<Object, String>(externalRiskIndicator.getIndicatorValue(), externalRiskIndicator.getIndicatorValue()); } private Person findPersonOrFail(UUID personId) throws ObjectNotFoundException { try { final Person person = personService.get(personId); if (person == null) { throw new ObjectNotFoundException(personId, Person.class.getName()); } return person; } catch (ObjectNotFoundException e) { throw e; } } private ExternalStudentTranscript findTranscriptFor(@Nonnull Person person) { final Map<String, Object> cache = evaluationResourceCache.get(); if (cache == null) { return externalStudentTranscriptService.getRecordsBySchoolId(person.getSchoolId()); } else { if (cache.containsKey(TRANSCRIPT_INDICATOR_METRIC_KEY)) { // yes, even if null final ExternalStudentTranscript transcript = (ExternalStudentTranscript) cache .get(TRANSCRIPT_INDICATOR_METRIC_KEY); // Just in case threadlocal state wasn't cleared properly if (transcript == null || transcript.getSchoolId().equals(person.getSchoolId())) { return transcript; } else { cache.remove(TRANSCRIPT_INDICATOR_METRIC_KEY); // fall through } } final ExternalStudentTranscript transcript = externalStudentTranscriptService .getRecordsBySchoolId(person.getSchoolId()); // yes, even if null cache.put(TRANSCRIPT_INDICATOR_METRIC_KEY, transcript); return transcript; } } private ExternalStudentFinancialAid findFinancialAidFor(@Nonnull Person person) { final Map<String, Object> cache = evaluationResourceCache.get(); if (cache == null) { return externalStudentFinancialAidService.getStudentFinancialAidBySchoolId(person.getSchoolId()); } else { if (cache.containsKey(FINANCIAL_AID_INDICATOR_METRIC_KEY)) { // yes, even if null final ExternalStudentFinancialAid fa = (ExternalStudentFinancialAid) cache .get(FINANCIAL_AID_INDICATOR_METRIC_KEY); // Just in case threadlocal state wasn't cleared properly if (fa == null || fa.getSchoolId().equals(person.getSchoolId())) { return fa; } else { cache.remove(FINANCIAL_AID_INDICATOR_METRIC_KEY); // fall through } } final ExternalStudentFinancialAid fa = externalStudentFinancialAidService .getStudentFinancialAidBySchoolId(person.getSchoolId()); // yes, even if null cache.put(FINANCIAL_AID_INDICATOR_METRIC_KEY, fa); return fa; } } private Map<String, ExternalStudentRiskIndicator> findExternalRiskIndicatorsFor(@Nonnull Person person) { final Map<String, Object> cache = evaluationResourceCache.get(); if (cache == null) { final List<ExternalStudentRiskIndicator> esriList = externalStudentRiskIndicatorService .getBySchoolId(person.getSchoolId()); return mapOf(esriList); } else { if (cache.containsKey(EXTERNAL_RISK_INDICATOR_METRIC_KEY)) { // yes, even if null final Map<String, ExternalStudentRiskIndicator> esri = (Map<String, ExternalStudentRiskIndicator>) cache .get(EXTERNAL_RISK_INDICATOR_METRIC_KEY); // Just in case threadlocal state wasn't cleared properly if (esri == null || esri.isEmpty() || esri.values().iterator().next().getSchoolId().equals(person.getSchoolId())) { return esri; } else { cache.remove(EXTERNAL_RISK_INDICATOR_METRIC_KEY); // fall through } } final List<ExternalStudentRiskIndicator> esriList = externalStudentRiskIndicatorService .getBySchoolId(person.getSchoolId()); final Map<String, ExternalStudentRiskIndicator> esri = mapOf(esriList); cache.put(EXTERNAL_RISK_INDICATOR_METRIC_KEY, esri); return esri; } } private String findEvaluationDisplayName(@Nonnull SuccessIndicatorEvaluation evaluation) { final Map<String, Object> cache = evaluationResourceCache.get(); Map<String, Blurb> blurbMap = null; if (cache == null || !(cache.containsKey(EVALUATION_DISPLAY_NAMES_KEY))) { final PagingWrapper<Blurb> blurbs = blurbService.getAll(allActive(), EVALUATION_DISPLAY_NAMES_BLURB_QUERY); blurbMap = mapOfBlurbs(blurbs.getRows()); if (cache != null) { cache.put(EVALUATION_DISPLAY_NAMES_KEY, blurbMap); } } else { blurbMap = (Map<String, Blurb>) cache.get(EVALUATION_DISPLAY_NAMES_KEY); } return evaluationDisplayNameFor(evaluation, blurbMap); } private String evaluationDisplayNameFor(@Nonnull SuccessIndicatorEvaluation evaluation, @Nonnull Map<String, Blurb> blurbMap) { final String blurbCode = new StringBuilder(EVALUATION_DISPLAY_NAMES_BLURB_PREFIX).append(BLURB_SEPARATOR) .append(evaluation.name().toLowerCase()).toString(); final Blurb blurb = blurbMap.get(blurbCode); return blurb == null ? null : blurb.getValue(); } /** * Must generate keys that match those generated by * {@link #externalRiskMapKeyFor(org.jasig.ssp.model.reference.SuccessIndicator)}. * * @param esriList * @return */ private Map<String, ExternalStudentRiskIndicator> mapOf(@Nullable List<ExternalStudentRiskIndicator> esriList) { return esriList == null ? Collections.EMPTY_MAP : Maps.uniqueIndex(esriList, RISK_INDICATOR_MAP_KEY_GENERATOR); } private Map<String, Blurb> mapOfBlurbs(@Nullable Collection<Blurb> blurbList) { return blurbList == null ? Collections.EMPTY_MAP : Maps.uniqueIndex(blurbList, BLURB_MAP_KEY_GENERATOR); } /** * Must generate keys that match those generated by {@link #mapOf(java.util.List)}. * * @param successIndicator * @return */ private String externalRiskMapKeyFor(SuccessIndicator successIndicator) { return RISK_SUCCESS_INDICATOR_MAP_KEY_GENERATOR.apply(successIndicator); } private String successIndicatorLoggingId(@Nullable SuccessIndicator successIndicator) { return successIndicator == null ? null : (successIndicator.getCode() + "(" + successIndicator.getId() + ")"); } private Pair<Object, String> emptyMetricDescriptor() { return new Pair<Object, String>(null, null); } }