org.jasig.ssp.service.impl.EvaluatedSuccessIndicatorServiceImpl.java Source code

Java tutorial

Introduction

Here is the source code for org.jasig.ssp.service.impl.EvaluatedSuccessIndicatorServiceImpl.java

Source

/**
 * 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);
    }

}