Appendix for Recommended Practices for Business Rules

This appendix contains some guidelines and code examples that can help users to write business rules.

Comparison of Objects

The comparison operators are used to compare two values in a Boolean fashion. The standard available comparators in JavaScript are:

Operator Name Example Result
== Equal x == y True, if X is equal to Y
=== Identical x === y True, if X is equal to Y, and they are of the same type
!= Not Equal x != y True, if X is not equal to Y
!== Not Identical x !== y True, if X is not equal to Y, or they are not of the type
< Less Than x < y True, if X is less than Y
> Greater Than x > y True, if X is greater than Y
<= Less Than or Equal To x <= y True, if X is less than or equal to Y
>= Greater Than or Equal To x >= y True, if X is greater than or equal to Y

Note: Since business rules are running on a Java runtime environment, there can be differences in what is otherwise perceived as being the same.

The following example uses String objects. Two strings are compared, with one being in Java while the other is JavaScript.

var someJSString = “xyz”;
var comparison = someJSString == product.getValue(”<ID>”).getSimpleValue();

This comparison will always produce 'False' no matter if the value on the product was 'xyz.' In instances when comparing a String, it is therefore recommended to always use the .equals() method available on both JavaScript and Java:

var someJSString = “xyz”;
var comparison = someJSString.equals(product.getValue(”<ID>”).getSimpleValue());

Looping Children of an Object

When looping through the children of an object, the user should refrain from calling the getChildren() method as it consumes memory, and in cases when not all children need to be evaluated, the getChildren() is ineffective. Instead the queryChildren() method should be used since this method only picks the next object when the previous object has been processed.

This code snippet demonstrates a process of looping through children using an anonymous inline implementation of the queryChildren() consume method.

var childrenQuery = node.queryChildren();
childrenQuery.forEach(function(child) {
    logger.info(child.getTitle());   
    return false; // break the "forEach" on the query
});

Conversely, this example shows a process of looping through children using an explicit implementation of the queryChildren() consume method, which in this use case is printTitle.

function printTitle(child) {
    logger.info(child.getTitle());
    return true; // continue the "forEach" on the query
}
var childrenQuery = node.queryChildren();
childrenQuery.forEach(printTitle);

Searches

While it is possible to do searches as part of the Scripting API, one should be careful about using this approach as they can easily lead to very long running times of the scripts.

Important: The following examples are not valid with all platform release versions.

For example, in a system that is running on a version earlier than 9.0, the following code snippet will perform a single attribute search on Product object types.

function singleAttributeSearchProduct(manager, attribute, value, maxResult)
{
var config = new com.stibo.core.domain.singleattributequery.SingleAttributeQueryHome.SingleAttributeQuerySpecification(com.stibo.core.domain.Product, attribute,value);
    var home = manager.getHome(com.stibo.core.domain.singleattributequery.SingleAttributeQueryHome);
    return home.querySingleAttribute(config).asList(maxResult);
}

Note: No matter what is chosen as maxResult, the result list cannot be more than 100 values as the query will simply be cut of at this time.

In this example using a 9.0 or greater system, a single attribute is returned for all Product object types.

function breakingQueryConsumer(node) {
  logger.info(node.getTitle());
  logger.info("Breaking...");
  return false; // break the "forEach" on the query
}
 
function continuingQueryConsumer(node) { 
  logger.info(node.getTitle());
  logger.info("Continuing...");
  return true; // continue the "forEach" on the query
}
 
var singleAttributeQueryHome =
manager.getHome(com.stibo.core.domain.singleattributequery.SingleAttributeQueryHome);
var conditions = new com.stibo.core.domain.singleattributequery.SingleAttributeQueryHome.SingleAttributeQuerySpecification(com.stibo.core.domain.Product,descriptionAttribute, "test");
var query = singleAttributeQueryHome.querySingleAttribute(conditions);
 
query.forEach(breakingQueryConsumer);
query.forEach(continuingQueryConsumer);

In a 9.0 or greater system, users may use the queryAPI to search.

Note: To use the queryAPI, the query add-on component needs to be installed. For on-premises systems, instructions for installing components can be found in the 'SPOT Program' topic in the System Administration documentation found in 'Downloadable Documentation'. For Stibo Systems SaaS environments, contact Stibo Systems Support.

var conditions = com.stibo.query.condition.Conditions;
 
// create a below condition
var isBelowCondition = conditions.hierarchy().simpleBelow(productsRoot);
 
// create an attribute value condition
var hasValueTestCondition = conditions.valueOf(descriptionAttribute).eq("test");
 
var queryHome = manager.getHome(com.stibo.query.home.QueryHome);
 
// query where both conditions are met (and).
var querySpecification = queryHome.queryFor(com.stibo.core.domain.Product).where(isBelowCondition.and(hasValueTestCondition));
 
var result = querySpecification.execute();
result.forEach(showTitle);
 
function showTitle(node) {
  logger.info(node.getTitle());
  return true;
}

The following example also leverages the queryAPI on 9.0 or newer systems.

Note: To use the queryAPI, the query add-on component needs to be installed. For on-premises systems, instructions for installing components can be found in the 'SPOT Program' topic in the System Administration documentation found in 'Downloadable Documentation'. For Stibo Systems SaaS environments, contact Stibo Systems Support.

function searchProductByAttributeValueAndObjectType(manager, objectTypeID, attrID, value) {
  var conditions = com.stibo.query.condition.Conditions; 
  var hasValueTestCondition = conditions.valueOf(manager.getAttributeHome().getAttributeByID("" + attrID)).eq("" + value);
  var hasObjectTypeCondition = conditions.objectType(manager.getObjectTypeHome().getObjectTypeByID(objectTypeID));
  var queryHome = manager.getHome(com.stibo.query.home.QueryHome);
  var querySpecification = queryHome.queryFor(com.stibo.core.domain.Product).where(hasValueTestCondition.and(hasObjectTypeCondition));
  var result = querySpecification.execute();
  return result;
}

Date Handling

While comparing dates can introduce complications, Java is offers some tools to compare dates against each other. This application allows users, for instance, to determined if a date is before or after another date.

In the following example, the code snippet determines if a date has passed already.

function hasISODateBeenExceeded(dateString) {
  return hasDateBeenExceeded(dateString, "yyyy-MM-dd");
}
function hasDateBeenExceeded(dateString, pattern) {
  var now = java.time.LocalDate.now();
  var parsed = java.time.LocalDate.parse(dateString, java.time.format.DateTimeFormatter.ofPattern(pattern)); 
  return now.isAfter(parsed);
}
// hasISODateBeenExceeded("2017-08-17") will return true as the date has been exceeded (compared to now)

Additional Use Cases

The following JavaScript examples are some of the scenarios that business rules may leverage JavaScript. Where applicable, assume that the 'node' is a bind to the 'Current Object' while 'workflow' is a bind to the 'Current Workflow.'

Setting a Static Value

node.getValue("AttributeID").setSimpleValue("No");
//OR do the same thing by using the LOV value ID
node.getValue("AttributeID").setLOVValueByID("N");

Creating a Child of Current Object

var newChild = node.createProduct(""/*ID (optional for object types with auto ID)*/, "ObjectTypeID");

Creating a Reference that is intended for Reference Types that Allow Multiple References

//An error will occur if you try to create a reference between two objects if a reference of that reference type already exists
//This function returns the existing reference if it already exists instead of producing the error
function createReferenceOrGetExisting(source, target, referenceType) {
  for (var references = source.getReferences(referenceType).iterator(); references.hasNext(); ) {
    var reference = references.next();
    if (reference.getTarget().equals(target)) {
      return reference;
    }
  }  
  return source.createReference(target, referenceType);
}
                               			
//A similar function to the previous one except it deletes the existing reference before creating a new reference (namely for reference types that only allow one reference)
function replaceSingleReference(source, target, referenceType) {
  for (var references = source.getReferences(referenceType).iterator(); references.hasNext(); ) {
    var reference = references.next();
    if (reference.getTarget().equals(target)) {
      return reference;//Do nothing if it exists
    } else {
      reference.delete();
      break;
    }
  }
  return source.createReference(target, referenceType);
}
var anAsset = node.getManager().getAssetHome().getAssetByID("AssetID");
var referenceType = node.getManager().getReferenceTypeHome().getReferenceTypeByID("ReferenceTypeID");
var reference = createReferenceOrGetExisting(node, anAsset, referenceType);

Auto-submit Node to Next State

//A rule like this would be configured On Entry for a state
var task = node.getTaskByID(currentWorkflowBind.getID(), "StateID");
task.triggerLaterByID("TransitionID", "A message viewable in the state log");

Partial Approval

//This rule approves this object's location in the hierarchy.
//If this object has never been approved, your set of part objects would have to contain a ParentPartObject to run any partial approval successfully.
var changesToApprove = new java.util.HashSet();
for (var partObjects = node.getNonApprovedObjects().iterator(); partObjects.hasNext(); ) {
  var partObject = partObjects.next();
  if (partObject instanceof com.stibo.core.domain.partobject.ParentPartObject) {
    changesToApprove.add(partObject);
    break;  
  }
}
node.approve(changesToApprove);

Starting a Workflow and Setting a Variable

var workflowInstance = node.startWorkflowByID("WorkflowID", "An optional message to show in the state log");
if (workflowInstance) {//It may have failed to start the workflow due to a start condition 
  workflowInstance.setSimpleVariable("WorkflowVariableID", "A text value");
}

Units with LOVs

Values in the system are stored as both the value and the unit. For example, if viewing an attribute with a value of '10g' in Web UI, the following example code will give access to that value, that value without the unit, and only the unit.

logger.info(node.getValue("SingleValueWithUnit").getSimpleValue());//10 g
logger.info(node.getValue("SingleValueWithUnit").getValue());//10
logger.info(node.getValue("SingleValueWithUnit").getUnit().getTitle());//g
logger.info(node.getValue("YesNoAttribute").getSimpleValue());//Yes
logger.info(node.getValue("YesNoAttribute").getID());//Y

Reading a Multi-value Attribute and Adding a Value

function getMultiValueAsStrings(valueObject) {
  var simpleValue = valueObject.getSimpleValue();
  return simpleValue ? simpleValue.split("<multisep/>") : [];
}
var value = node.getValue("MultiValuedAttribute");
var strings = getMultiValueAsStrings(value);
if (strings.indexOf("New Value") == -1) {//Ordinarily, a duplicate value does not make sense  
  value.addValue("New Value");
}

Setting a Deadline Relative to Current Date

This example is run on a workflow set the deadline that is not a precise one but related to the current date.

var calendar = java.util.Calendar.getInstance();
calendar.add(java.util.Calendar.DATE, 1);//Set to tomorrow
workflow.getTaskByID("WorkflowID", "StateID").setDeadline(calendar.getTime());

Setting a Deadline to a Particular Date

This example is also run on a workflow and set an specific date.

var dateFormat = new java.text.SimpleDateFormat("dd/MM/yyyy");
workflow.getTaskByID("WorkflowID", "StateID").setDeadline(dateFormat.parse("20/01/2021"));

Reading Asset Content

if (asset.hasContent()) {
  try {
    var outputStream = new java.io.ByteArrayOutputStream();//it is worth considering writing to a file to save memory for large assets
    asset.download(outputStream);
    var inputStream = new java.io.ByteArrayInputStream(outputStream.toByteArray());
    var bufferedReader = new java.io.BufferedReader(new java.io.InputStreamReader(inputStream));
    for (var line = bufferedReader.readLine(); line; line = bufferedReader.readLine()) {
      logger.info(line);
    }
} finally {
    if (bufferedReader) {
      bufferedReader.close();//If we were using something other than ByteArray streams, closing becomes important
    }
  }
}

Writing an E-mail

mailHome.mail() 
  .addTo("toAddress@acme.com", "Optional name")
  .from("fromNoReplyAddress@acme.com", "Optional name")//Uses Mail.DefaultFromMailAddress property by default
  .subject("Subject of Email")
  .htmlMessage("<div>The body of the email</div>")//As opposed to plainMessage
  .attachment()//This returns an Attachment object, not the Mail
    .fromAsset(asset)
    .name("TheAttachment.jpg")//Optional
    .attach()//Calling attach finishes the creation of the attachment and returns the mail object again
.send();

Creating a Classification Product Link

var classification = manager.getClassificationHome().getClassificationByID("AClassificationID");
var linkType = manager.getHome(com.stibo.core.domain.classificationproductlinktype.ClassificationProductLinkTypeHome).getLinkTypeByID("LinkTypeID");
try {
  var classificationProductLink = node.createClassificationProductLink(classification, linkType);//classification.createClassificationProductLink(node, linkType); works too
  //Do things with new link, like write metadata
} catch (e) {
  if (e.javaException instanceof com.stibo.core.domain.UniqueConstraintException) {
    logger.info("Link already exists.");
  } else if (e.javaException instanceof com.stibo.core.domain.LinkTypeNotValidException) {
    logger.info("Link type not valid for this product and classification");
} else {
    throw(e);
  }
}