Invoking External Functionality Using Expressions

Expressions can be found in multiple places in a script to define behavior for loops, conditions and so on. See the Expression Syntax appendix in the Authoring Scripts using Intelligent Evidence Gathering(IEG) guide for reference.

These expressions can refer to answers and can combine them using various operators, and they can even call functions (except when used on dynamic conditional clusters as these expressions are evaluated in the browser).

The functions described above are referred to as Custom Functions and are defined using Java code. Depending on their usage, they can be of two types:

Real-world examples that might necessitate the invocation of external functionality are the validation of a US ZIP code that a user has supplied and the population of a state field based on a supplied ZIP code. We will now demonstrate those 2 different usage.

The DS schema will need to be expanded to add the following 2 attributes to the Person entity, as follows:

Figure 1. Additional Person attributes in the DS schema
<xsd:attribute name="state" type="IEG_STRING"/>
<xsd:attribute name="zipCode" type="IEG_STRING"/>

First let's try to validate a ZIP code against a state (this is a naive implementation): a ZIP code must be five digits long and the first 3 digits will indicate the state.

The personal details page mentioned earlier and the corresponding summary page can be modified with 2 extra mandatory questions: state and zipCode:

Figure 2. State and zipCode questions in the script definition
<question id="state" mandatory="true">
    <label id="State.Label">
        State:
    </label>
    <help-text id="State.HelpText">
        The state you live in
    </help-text>
</question>
<question id="zipCode" mandatory="true">
    <label id="ZipCode.Label">
        ZIP Code:
    </label>
    <help-text id="ZipCode.HelpText">
        Your ZIP code
    </help-text>
</question>

Then the custom function that will perform the validation must be created as a Java class in the package curam.rules.functions:

Figure 3. Custom Function to validate the ZIP code
...
public class CustomFunctionValidateZipCode extends CustomFunctor {

  public Adaptor getAdaptorValue(final RulesParameters rp)
  throws AppException, InformationalException {

    final List<Adaptor> parameters = getParameters();
    final String zipCode =
      ((StringAdaptor) parameters.get(0)).getStringValue(rp);
    final String state =
      ((StringAdaptor) parameters.get(1)).getStringValue(rp);
    boolean valid = false;

    if (zipCode.length() == 5) {
      final String prefix = zipCode.substring(0, 3);
      //lookup the state prefixes
      if (prefix.equals("100")
        && state.equalsIgnoreCase("New York")) {
        valid = true;
      }
      if (prefix.equals("900")
        && state.equalsIgnoreCase("California")) {
        valid = true;
      }
    }

    return AdaptorFactory.getBooleanAdaptor(Boolean.valueOf(valid));
  }

}

The following metadata for the custom function must be inserted in <yourcomponent>/rulesets/functions/CustomFunctionMetaData.xml:

Figure 4. Custom Function Metadata
<CustomFunctor name="CustomFunctionValidateZipCode">
  <parameters>
    <parameter>
      curam.util.rules.functor.Adaptor$StringAdaptor
    </parameter>
    <parameter>
      curam.util.rules.functor.Adaptor$StringAdaptor
    </parameter>
  </parameters>
  <returns>curam.util.rules.functor.Adaptor$BooleanAdaptor</returns>
</CustomFunctor>

See the Cúram Rules Codification Guide for more details on the definition of custom functions.

In our example, the custom function ValidateZipCode doesn't access an external database to look-up the corresponding state. Ideally, it should do that look-up and then check the state returned against the state that was entered. For simplification purposes, only two zip code prefixes are hard-coded above.

The validation will then be inserted in the personal details page:

Figure 5. ZIP code validation in the script definition
<validation
  expression="ValidateZipCode(Person.zipCode, Person.state)">
    <message id="InvalidZipCode">
        The ZIP code is invalid.
    </message>
</validation>

When the user clicks Next, the answers to the zipCode and state questions are passed to the custom function, which will return true if the answers are valid. The next page will then be displayed.

If the custom function returns false, the message specified in the validation is displayed at the top of the Person details page, blocking the access to the Next page until valid answers are submitted.

The custom function has no side effect as it doesn't alter anything. It only performs an operation based on the parameters and returns a result.

It would also be possible to remove the mandatory flag on the two new questions and to validate the answers only if they have both been supplied. The validation expression would then need to be changed to the following using the out-of-the-box custom function isNotNull that checks if the given parameter is null:

Figure 6. Alternate validation expression
"not(isNotNull(Person.zipCode) and isNotNull(Person.state))
    or ValidateZipCode(Person.zipCode, Person.state)"

Alternatively, it is possible to populate the state question given the zipCode. To do so, the Person details page will only ask for the zipCode (with the mandatory flag), and the summary page will display both state and zipCode.

The following custom function should be defined:

Figure 7. Custom Function to populate the state
...
public class CustomFunctionpopulateState extends CustomFunctor {

  public Adaptor getAdaptorValue(final RulesParameters rp)
  throws AppException, InformationalException {

    final IEG2Context ieg2Context = (IEG2Context) rp;
    final long rootEntityID = ieg2Context.getRootEntityID();
    String schemaName; 
    //schemaName has to be hard-coded or retrieved outside of IEG
    Datastore ds = null;
    try {
      ds =
        DatastoreFactory.newInstance().openDatastore(
            schemaName);
    } catch (NoSuchSchemaException e) {
      throw new AppException(IEG.ID_SCHEMA_NOT_FOUND);
    }

    Entity applicationEntity = ds.readEntity(rootEntityID);

    Entity personEntity =
      applicationEntity.getChildEntities(
        ds.getEntityType("Person"))[0];
    String zipCode = personEntity.getAttribute("zipCode");
    String state = "Unknown";
    final String prefix = zipCode.substring(0, 3);
    //lookup the state prefixes
    if (prefix.equals("100")) {
      state = "New York";
    }
    if (prefix.equals("900")) {
      state = "California";
    }
    personEntity.setAttribute("state", state);
    personEntity.update();
    return AdaptorFactory.getBooleanAdaptor(new Boolean(true));
  }

}

And its metadata:

Figure 8. Custom Function metadata
<CustomFunctor name="CustomFunctionpopulateState">
  <returns>curam.util.rules.functor.Adaptor$BooleanAdaptor</returns>
</CustomFunctor>

Between the Person details page and the summary page, a callout element must be inserted to call this custom function, as follows:

Figure 9. Callout to populate the sate in the script definition
<callout id="populateAddress" expression="populateState()"/>

This time, the custom function will alter the DS by populating the state on the Person entity. The context contains the root entity ID and executionID, making it easier to update the DS. If the callout is in a loop, the context also contains the current entity ID.