Logic 2.0 Design

High-level plans

New functionality

A new "rule" module will be authored which has the core set of functionality that is desired. This functionality will be primarily focused on producing a particular piece of data for a single patient or a cohort of patients. This module will use the "org.openmrs" namespace, rather than the conventional "org.openmrs.module" namespace in order to more easily support the goal of eventually incorporating this module into core. By keeping it as a module initially, this allows implementations running older versions of OpenMRS (1.6, 1.8, 1.9, etc) to take advantage of, and test out, this work, without requiring them to upgrade. It also will allow the module to evolve at a different pace than the core code.

TODO: What do we think of the name of this module? rule? patientcalculation? patientdata?

For the purposes of this design, and for brevity, I will use "Rule". If we change the name, we can replace "Rule" throughout as appropriate.

Existing functionality

The existing "org.openmrs.logic" package that exists in core will be removed into the existing logic module. The logic module will be updated such that it requires the module described above, and that it implements the interfaces created and exposed in it. The logic module will no longer be a "core" or "required" module within an OpenMRS distribution. Backwards compatibility will be maintained with a few minor exceptions, including the need to explicitly require the module within any module that uses it, and the need to use Context.getService(LogicService.class) rather than Context.getLogicService().

Requirements for the new functionality

Design for new functionality

Rule / ParameterDefinition

A Rule represents a definition that can be evaluated to produce patient data. A Rule can expose parameters which control the results of it's calculation.

interface Rule {
    Set<ParameterDefinition> getParameterDefinitions();  // Returns all of the parameters supported by this Rule
}

interface ParameterDefinition {
    String key; // "startDate". unique per rule and expected to be a valid java variable name.
    String name; // this is a display label, like "Start Date"
    String description; // "This is the start date for ..."
    String datatype; // The Java class of this parameter
    Boolean required; // If true, this parameter must have a non-null value for evaluation to occur
    // we decided to get rid of allowMultiple, since description can be List<String>, though Burke wants to keep it according to the notes    // Boolean allowMultiple;  // If true, this parameter would accept a Collection of values of the declared "type"}

RuleContext

The RuleContext contains any contextual information that may be shared across one or more Rule evaluations. This includes the "index date" for the evaluation and a cache for storing the results for previously evaluated rules. The index date represents the date on which the evaluation should occur. It should essentially replace any call for "new Date()" in evaluation code, and should return the data that was accurate as of that particular date and time.

interface RuleContext {
    public Date getIndexDate();
    public void setIndexDate(Date date);
    public CohortResult getFromCache(Cohort, Rule, Map<String, Object>);
    // the cache-related methods still need some design (one option is to have a "RuleProvider owner" argument too)
    public void addToCache(String key, Object value);
    public Object getFromCache(String key);
    public void removeFromCache(String key);
}

Result

A Result is the data that is produced from evaluating a Rule for a single patient. Results are strongly typed, but provide a convenience method for casting to other datatypes.

interface Result {
  public Rule getRule(); // Returns the Rule that was evaluated to produce this result
  public RuleContext getRuleContext();  // Returns the RuleContext used when the Rule was evaluated
  public Object getValue(); // Returns the raw object value (eg. a Patient or an Obs)
  public boolean isEmpty(); // Return true if the object value is null, an empty list, or an empty string?
  public T as(Class<T> clazz) throws ConversionException; // Tries to convert to the passed type
}

interface DateBasedResult extends Result {
  public Date getDateOfResult();
}

class EmptyResult extends Result {
  public Object getValue() { return null; }
  public boolean isEmpty() { return true; }
  public T as(Class<T> clazz) { ... }
}

class ObsResult implements DateBasedResult {
  private Obs value;
  public Object getValue() { return value; }
  public boolean isEmpty() { return value == null; }
  public Date getDateOfResult() { return value == null ? null : value.getObsDatetime(); }
  public T as(Class<T> clazz) { ... }
}


class EncounterResult implements DateBasedResult {
  ...
}


class VisitResult implements DateBasedResult {
  ...
}

class ListResult extends Result {
  private List<Result> results;
  public Object getValue() { return results; }
  public boolean isEmpty() { return results == null || results.isEmpty(); }
  public T as(Class<T> clazz) { ... }
  public Result getFirstResult() { return isEmpty() ? new EmptyResult() : results.get(0); }
  public Result getLastResult() { return isEmpty() ? new EmptyResult() : results.get(results.size()-1); }
}

We will likely employ a library of utility methods as well, to support conversion of Result types. For example:

class ResultUtil {
  public static Result first(Result);  // first if a list, or self if a single result
  public static T convert(myResult, Class<T>); // the "as" method on Result may delegate to this
}

CohortResult

A CohortResult is the data that is produced from evaluating a Rule for a Cohort of patients. It is essentially a wrapper of a Map<Integer, Result>, but provides the flexibility to add additional methods and/or data as needed down the road.

class CohortResult implements Map<Integer, Result> {
  public Map<Integer, Result> getAllResults();
}

RuleEvaluator

A RuleEvaluator is responsible for evaluating one or more types of Rules into Results. This is where the bulk of all calculations occur, either by performing these calculations directly within the evaluator, or by calling service methods / DAOs that perform calculations. RuleEvaluators will likely be wired to Rule classes either via a registry or via annotations.

interface RuleEvaluator {
  public CohortResult evaluate(Cohort, Rule, Map<String, Object>, RuleContext); // the Map<String, Object> are parameter values
}

As an implementation detail, we probably want an abstract class to simplify writing RuleEvaluators that evaluate patients one at a time, like:

abstract class PatientAtATimeRuleEvaluator implements RuleEvaluator {
  public abstract void beforeEvaluating(Cohort, Rule, Map<String, Object>, RuleContext);
  public abstract Result evaluateForPatient(Integer ptId, Rule, Map<String, Object>, RuleContext);
  public abstract void afterEvaluation(Cohort, Rule, Map<String, Object>, RuleContext);
  public CohortResult evaluate(Cohort cohort, Rule rule, Map<String, Object> params, RuleContext context) {
    beforeEvaluation(cohort, rule, params, context);
    CohortResult ret = new CohortResult();
    for (Integer ptId : cohort.getMemberIds()) {
      ret.add(ptId, evaluateForPatient(ptId, rule, params, context));
    }
    afterEvaluation(cohort, rule, params, context);
    return ret;
  }
}

RuleProvider

A RuleProvider is responsible for retrieving a Rule instance given a rule name and an optional configuration string. A typical implementation would be such that ruleName is the rule class to instantiate and configuration represents the serialized property values that need to be configured on this rule instance, however it is totally up to the Provider to define this. For example, to retrieve the Rule for "Most Recent Weight":

Rule Name: org.openmrs.rule.definition.MostRecentObsRule
Configuration: concept=<UUID for Weight (KG) concept>

There would then be a RuleProvider registered to handle this type of Rule which would know it needed to first instantiate a new instance of MostRecentObsRule, configure it's properties via the parsed values from the configuration string, and then return configured Rule instance. Like RuleEvaluators, RuleProviders will likely be wired to Rule classes either via a registry or via annotations.

interface RuleProvider {
  Rule getRuleInstance(String ruleName, String configuration);
}

TokenRegistration

A TokenRegistration represents a saved Rule instance in the database, and includes a unique name, the RuleProvider, the ruleName, and the configuration for the rule. The intention is to allow a fully configured Rule instance to be retrieved given a unique name String. This class is a hibernate-managed class.

/**
 * terminology-wise, TokenRegistration.name can be referred to as a "token"
 */
class TokenRegistration extends OpenmrsMetadata {
  // All metadata fields
  private Class<? extends RuleProvider> providerType;
  private String ruleName
  private String configuration;
}

RuleService

The RuleService is the primary mechanism for evaluating Rules and for associating Rule instances with saved tokens.

interface RuleService extends OpenmrsService {

  RuleContext createContext(); // Ensures that RuleContext can be overridden as needed

  public TokenRegistration getTokenRegistration(Integer);
  public TokenRegistration getTokenRegistrationByName(String);  // This is required to be unique
  public List<TokenRegistration> getAllTokenRegistrations();
  public List<TokenRegistration> findTokens(String partialName);
  public TokenRegistration saveTokenRegistration(TokenRegistration); // This enforces uniqueness of name
  public void deleteTokenRegistration(TokenRegistration);

  Rule getRule(String tokenName);

  Result evaluate(Integer patientId, Rule rule, Map<String, Object> parameters, RuleContext context);
  Result evaluate(Integer patientId, Rule rule, RuleContext context);
  Result evaluate(Integer patientId, Rule rule);

  CohortResult evaluate(Cohort cohort, Rule rule, Map<String, Object> parameters, RuleContext context);
  CohortResult evaluate(Cohort cohort, Rule rule, RuleContext context);
  CohortResult evaluate(Cohort cohort, Rule rule);
}