Invoking external functionality by using expressions and custom functions

Expressions can be defined in multiple places in scripts to define behavior such as loops or conditions. These expressions can refer to answers and can combine them using various operators, and they can call external functions, which are called custom functions.

Expressions cannot call custom functions when used on dynamic conditional clusters as these expressions are evaluated in the browser.

Custom functions are defined in Java™ code and depending on their usage, they can be one of two types:

  • Custom functions that can take parameters, possibly making a call to external functionality, and return a value. They do not change the content of the datastore. They are used in most expressions.
  • Custom functions where you want to update the content of the datastore use the stand-alone callout element. The returned value is irrelevant but it must be a Boolean value. These custom functions must not update values that were answered before the callout. The IEG Engine is unaware of updates that are made outside the context of the script, and cannot take any actions that might be required by the updates.

For example, on a personal details page, you might need to call external functionality to validate a US ZIP code that a user has supplied or to populate a US State field based on a supplied ZIP code. Let's look at how you might implement these two use cases.

Update the datastore schema to add two extra attributes to the Person entity, as follows:

<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 three digits indicate the state.

The personal details page and the corresponding summary page can be modified with two extra mandatory questions in the script definition: state and zipCode:

<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>

Create the custom function that performs the validation as a Java class in the curam.rules.functions package. The following custom function validates 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));
  }

}

Insert the following metadata for the custom function in <yourcomponent>/rulesets/functions/CustomFunctionMetaData.xml:

<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>

In the example, the ValidateZipCode custom function doesn't access an external database to look up the corresponding state. Ideally, the custom function would look up and check the returned state against the entered state. For simplicity, two ZIP code prefixes were hardcoded.

The validation is then inserted in the script definition for the personal details page.

<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 returns true if the answers are valid. The next page is then displayed.

If the custom function returns false, the message that is 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 performs an operation based on the parameters and returns a result.

You can also remove the mandatory flag on the two new questions and validate the answers only if they have both been supplied. Change the validation expression to use the isNotNull custom function that is provided in the application, which checks whether the parameter is null:

"not(isNotNull(Person.zipCode) and isNotNull(Person.state))
    or ValidateZipCode(Person.zipCode, Person.state)"

Alternatively, you can populate the state question by using the zipCode. On the Person details page, ask for the zipCode only, with a mandatory flag, and the summary page then displays both state and zipCode.

You can define the following custom function:

...
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:

<CustomFunctor name="CustomFunctionpopulateState">
  <returns>curam.util.rules.functor.Adaptor$BooleanAdaptor</returns>
</CustomFunctor>

Between the Person details page and the summary page, insert a callout element in the script definition to call this custom function, as follows:

<callout id="populateAddress" expression="populateState()"/>

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