New tool: Validator and Logger

The below is useful in XML as well

Dear all,

I am a developer that likes to work with strongly typed statically scoped languages, so I use Java/Kotlin to develop my backend.
While working with java i created some tools that can make my code cleaner and smaller. I want to contribute this code to moqui and I want the community’s opinion before creating the pull request. Please bear with this post as it will be a bit long. I would also like to present the idea more on this Friday’s meeting if you can give me the time.

The below is a very brief example without going into the mechanics and the plans/possibilities of the validator

The idea is to add a validation level between the service input params and the logic of the service. For now, this is done at the start of the java service. Please use the below method as a reference

/**
 * @param ec ec
 * @return the primary keys of the created benefit type
 */
public Map<String, Object> createBenefitType(ExecutionContext ec) {
    EntityFacade ef = ec.getEntity();
    ServiceFacade sf = ec.getService();
    // 1: Logger
    Logger logger = LoggerFactoryAutoId.getLoggerWithStartedTimer(ec, PayrollService.class);
    logger.info("mantle.humanres.employment.createBenefitType... start");
    // 2: try
    try {
        // database validation
        // 3: create the map that will hold the validation process needed meta data
        HashMap<String, ParamMetaData<?>> unvalidated = new HashMap<>();
			// 4: the map key
			// 5: the map value (Meta class).
			// 5.1: the Title of the error message
        // 5.2: the default value
        // 5.3: The validator builder
        unvalidated.put("parentTypeId", new Meta<String>(
                "Parent Type Id", null, Validator.builder(ec, NullPolicy.NONE)
                .isNotBlank().build()));
        unvalidated.put("itemTypeEnumId", new Meta<String>(
                "Item Type Enum", Validator.builder(ec, NullPolicy.ALLOW)
                .existsAsEnumOfType("ItemType").build()));
        unvalidated.put("leaveTypeEnumId", new Meta<String>(
                "Leave Type Enum", Validator.builder(ec, NullPolicy.ALLOW)
                .existsAsEnumOfType("EmploymentLeaveType").build()));
        // string validation
        unvalidated.put("description", new Meta<String>(
                "Description", Validator.builder(ec, NullPolicy.NONE)
                .isNotBlank().build()));
        // number validation
        unvalidated.put("employerPaidPercentage", new Meta<Number>(
                "Employer Paid Percentage", Validator.builder(ec, NullPolicy.ALLOW)
                .isPositive().build()));

        // validate all inputs
        // 6: the validation call
        Map<String, Object> params = Validator.validateAndLogParams(logger, ec, unvalidated);

        // validation for item type and leave type both not to be null
        // 7: getting from the validated parameters map
        String itemTypeEnumId = (String) params.get("itemTypeEnumId");
        String leaveTypeEnumId = (String) params.get("leaveTypeEnumId");
        if (StringUtils.isBlank(itemTypeEnumId) && StringUtils.isBlank(leaveTypeEnumId)) {
            // 8: a Custom top level compile-time exception that can hold a log message and a
            //    display error
            throw new MoquiException("Benefit Type required, neither an item or a leave type was provided");
        }

        // create benefit type
        Map<String, Object> returnMap = sf.sync().name("create#mantle.humanres.employment.BenefitType")
                // 9: passing the validated params in a service call
                .parameters(params)
                .disableAuthz().call();

        // custom class to show a success localised success (LocMes) equivalent to
        // ec.getMessage().addMessage(ec.getL10n().localize(LocMes.SUCCESS), MessageFacade.success);
        MF.success(ec, LocMes.SUCCESS);
        logger.info("mantle.humanres.employment.createBenefitType... done");
        return returnMap;
    }
    // 10: catch:
    // 10.1: any custom error. similar to statement (8)
    // 10.2: any validation error that will send back both a log error and a user facing
    // error message to be processed
    catch (MoquiException e) {
        // a fast way of processing the error. please find the a copy of this method below for
        // your convenience
        return e.process(ec, logger);
    }
}

/**
* @param ec     current context
* @param logger {@link org.slf4j.Logger} or can also be one of the custom children in {@link
*               com.erp.nucleus.utils.log.LoggerFactoryAutoId}
*
* @return a map with the error code and error messages
*/
// 11: error processing
public Map<String, Object> process(ExecutionContext ec, Logger logger) {
    logger.error(this.getMessage(), this);
    userMessageList.forEach(item->
        ec.getMessage().addError(item)
    );
    LinkedHashMap<String, Object> ret = new LinkedHashMap<>();
    ret.put("errorCode", code);
    ret.put("errors", userMessageList);
    return ret;
}

Explanation:

  1. Logger: a custom logger that automatically creates a random ID per service + logs the time at every log call
  2. try: this is used to remove all redundant error handlers and group them into one place. See statement (11)
  3. create the map that will hold the meta data needed in the validation process :
  4. the map key: The key used here is the input parameter name
  5. the map value (Meta class). Meta is shorthand for ParamMetaData. It holds:
    1. the Title of the error message (optional: defaults to null)
    2. the default value. Similar to calling ec.contextStack.getOrDefault() (optional: defaults to null)
    3. The validator. The core component needed. Can be set to null to indicate no validation. This class follows a builder pattern to add the wanted tests
      1. the validator should always be given the current execution context
      2. NullPolicy: this describes the action taken when a null/missing parameter is passed to the validator (optional: defaults to NONE)
        1. NONE: No special action is taken. Test will run normally
        2. ALLOW: The value will be tested for null first and will be considered valid if == null. The key/value pair will be added to the validated parameters map (statement 6)
        3. IGNORE: The value will be tested for null first and will be considered valid if == null. This flag will tell the validation process (statement 6) to not add this key/value to the validated parameters map. This is useful when passing in optional parameters. (e.g. the primary key to a store statement (see statement 9)).
      3. when you are done adding the validation tests, call the “build()” method to get an instance of a Validator
  6. the validation call: this call will:
    1. get the values in the context stack according to the keys of the passed map/
    2. log each parameter (name/value/type)
    3. validate each parameter.
    4. throw a MoquiException if a parameter is not valid. No extra catches are needed. (See statements 2, 10)
  7. example getting from the validated parameters map.
  8. a Custom top level compile-time exception that can hold a log message and a display error
  9. passing the validated params in a service call
  10. catch:
    1. any custom error. similar to statement (8)
    2. any validation error that will send back both a log error and a user facing error message to be processed
  11. error processing

NOTE 1:there is a coding style I use to make the code even smaller and more readable, but it will make this post more complex. We can talk about it on Friday.

NOTE 2: this is not implemented, but i want this validation to be possible in the xml definition of the service to completely remove it from the java code. This will make it more readable, less redundant and self explanatory since it is directly related to the inputs themsenves

This is a list of the currently implemented tests that can be added:

public Validator$Builder {
  public Builder(org.moqui.context.ExecutionContext);
  public Builder(org.moqui.context.ExecutionContext, boolean);
  public Builder(org.moqui.context.ExecutionContext, Validator$NullPolicy);
  public Builder isEqual(java.lang.Object);
  public Builder existsInCollection(java.util.Collection<?>);
  public Builder existsInArray(java.lang.Object...);
  public Builder isNotBlank();
  public Builder matches(java.lang.String);
  public Builder isNumber();
  public Builder isNumber(java.util.Locale);
  public Builder isPositiveInteger();
  public Builder isPositiveInteger(java.util.Locale);
  public Builder isPositive();
  public Builder isPositive(java.util.Locale);
  public Builder isNotZero();
  public Builder isNotZero(java.util.Locale);
  public Builder isInRange(java.lang.Number, java.lang.Number);
  public Builder isInRange(java.lang.Number, java.lang.Number, java.util.Locale);
  public Builder existsAsEnum();
  public Builder existsAsEnumOfType(java.lang.String);
  public Builder existsAsStatusOfType(java.lang.String);
  public Builder existsAsFieldValueOfEntry(java.lang.String, java.lang.String);
  public Builder existsAsPkInEntity(java.lang.String);
  public Builder isValidDate();
  public Builder datesInChronoOrder();
  public Builder dateRangesOverlap();
  public Builder isBeforeNow();
  public Builder isAfterNow();
  public Builder isBoolean();
  public <T> Builder isListOfClass();
  public Builder customTest(Validator$ValidatorTest, java.lang.String);
  public Validator build();
}

I will be talking about the future plans if i get a spot on this Friday meeting

1 Like

A small Update. The Validator now returns any entity-find search result used while validating.
Kotlin Example:

valMap["partyRelationshipId" ] = Meta("Party Relationship",
    null/*this says default = null*/, Validator.Builder(ec, NullPolicy.NONE)
    .isNotBlank
    .existsAsPkInEntity("mantle.party.PartyRelationship")
    .build())
val params = Validator.validateAndLogParams(logger, ec, valMap)
val partyRel = params.getEntity("partyRelationshipId")

the last line will return the entity that was found while making sure that the partyRelationship exists in the Database. Since the code execution reached here, that means that the variable partyRel is never null.

What’s the point of this code?

Is it to validate the data already in the database?

1 Like

it is to validate inputs that the user is putting into a form or received via API.
Example:
Given an “employee update” API, adding this code inside the service will make sure that the employee that we are trying to update (via partyRelationshipId) actually exists in the Database (to prevent null pointers while fetching the entity value), and that the employee name is not blank.

You can obviously do this with if statements and null tests, but for each line in the validator, you have to write, 3-4 lines of code and make the service itself less readable.

The below code:

val cs = ec.context
val partyRelId = cs.getOrDefault("partyRelationshipId", null) as String
if (StringUtils.isBlank(partyRelId)){
	ec.logger.error("The partyRelationshipId given is blank")
    ec.message.addError("The Employee you set is invalid")
    return mapOf()
}

val ef: EntityFacade= ec.entity
val partyRel = ef.find("mantle.party.PartyRelationship")
    .condition("partyRelationshipId", partyRelId)
    .disableAuthz().one()
if (partyRel == null){
    ec.logger.error("The partyRelationship with id $partyRelId doesn't exist in the database")
    ec.message.addError("The Employee you set does not exist")
    return mapOf()
}

val newName = cs.getOrDefault("newName", null) as String
if (StringUtils.isBlank(newName)){
    ec.logger.error("The name given is blank")
    ec.message.addError("The Employee can't have an empty name")
    return mapOf()
}

can be shrunk into

try {
    val valMap:MutableMap<String, Meta<*>> = LinkedHashMap()
    valMap["partyRelationshipId" ] = Meta("Party Relationship",
	    null/*this says default = null*/, Validator.Builder(ec, Validator.NullPolicy.NONE)
		    .isNotBlank
		    .existsAsPkInEntity("mantle.party.PartyRelationship")
		    .build())
    valMap["newName" ] = Meta<String>("EmployeeName", Validator.Builder(ec, Validator.NullPolicy.NONE)
		    .isNotBlank.build())
    val params = Validator.validateAndLogParams(logger, ec, valMap)
    val partyRel = params.getEntity("partyRelationshipId")
    val newName = params["newName"] as String
} catch (e: MoquiException) {
	return e.process(ec, logger)
}