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:
- Logger: a custom logger that automatically creates a random ID per service + logs the time at every log call
- try: this is used to remove all redundant error handlers and group them into one place. See statement (11)
- create the map that will hold the meta data needed in the validation process :
- the map key: The key used here is the input parameter name
- the map value (Meta class). Meta is shorthand for ParamMetaData. It holds:
- the Title of the error message (optional: defaults to null)
- the default value. Similar to calling ec.contextStack.getOrDefault() (optional: defaults to null)
- 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
- the validator should always be given the current execution context
- NullPolicy: this describes the action taken when a null/missing parameter is passed to the validator (optional: defaults to NONE)
- NONE: No special action is taken. Test will run normally
- 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)
- 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)).
- when you are done adding the validation tests, call the “build()” method to get an instance of a Validator
- the validation call: this call will:
- get the values in the context stack according to the keys of the passed map/
- log each parameter (name/value/type)
- validate each parameter.
- throw a MoquiException if a parameter is not valid. No extra catches are needed. (See statements 2, 10)
- example getting from the validated parameters map.
- a Custom top level compile-time exception that can hold a log message and a display error
- passing the validated params in a service call
- catch:
- any custom error. similar to statement (8)
- any validation error that will send back both a log error and a user facing error message to be processed
- 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