Adding Custom Validation

This section describes how to create custom validators in Atoti Sign-Off.

Overview

We expect users to utilize the ISignOffDefinitionService and IAdjustmentService to perform transactional CRUD operations. These services come with built-in validation constraint handling using Java Bean Validation. However, you can customize the validation performed when creating or updating sign-off definitions and adjustments by adding beans that implement the ISignOffDefinitionValidator and IAdjustmentValidator interfaces, respectively.

info

We expect Sign-Off tasks to be created or updated via the workflow, hence why they are not included in the list of services that can be customized.

How it works

Atoti Sign-Off uses custom constraint annotations to validate the input of the services. For example, when creating a Sign-Off definition, the validation works as follows:

  1. The input parameter of ISignOffDefinitionService::createSignOffDefinition is annotated with @SignOffDefinitionConstraint(type = CREATION).
    This tells Spring: when passing an ISignOffDefinition to this method, use the custom @SignOffDefinitionConstraint annotation with the type CREATION to validate the ISignOffDefinition.
    This means that if you include your own implementation of ISignOffDefinitionService, the constraint will automatically be applied.

  2. @SignOffDefinitionConstraint is annotated with @Constraint(validatedBy = SignOffDefinitionConstraintValidator.class).
    This tells Spring: when you see the @SignOffDefinitionConstraint annotation, use the SignOffDefinitionConstraintValidator to validate the input.

  3. The SignOffDefinitionConstraintValidator class implements Jakarta’s ConstraintValidator interface, which is used to validate the input. The isValid method is called when the input is passed to the method. This method calls the ISignOffDefinitionValidator implementation to actually validate the input.

    note

    You can expose your own implementation of SignOffDefinitionConstraintValidator to override the isValid method, but in order to do so, you must extend the concrete class SignOffDefinitionConstraintValidator so that your implementation can be picked up by @SignOffDefinitionConstraint. The Jakarta validation API requires a concrete class to be used as the validator, so we cannot use an interface.

  4. The ISignOffDefinitionValidator implementation is used to validate the input. If you want to add your own custom validation logic, expose an implementation of this interface.

Default Validation

We perform the following validations out-of-the-box:

Sign-Off Definitions

On Creation

The default ISignOffDefinitionValidator validates the following:

  • The id field is not populated.
  • The scope field does not overlap with another definition’s scope. See the overlapping scopes section for more details.

On Update

The default ISignOffDefinitionValidator validates the following:

  • The id field is populated.
  • The name field has not changed, as it is required as a key for the workflow.
  • The startDate field has not changed, as it is required as a key for the workflow.
  • The scope field does not overlap with another definition’s scope. See the overlapping scopes section for more details.

Sign-Off Adjustments

On Creation

The default ISignOffAdjustmentValidator validates the following:

  • The id field is not populated.

On Update

The default ISignOffAdjustmentValidator validates the following:

  • The id field is populated.

Custom Validation

To implement your own custom validation you must expose an implementation of ISignOffDefinitionValidator or IAdjustmentValidator as a Spring Bean. This will then replace our default validation logic. You may extend our logic by letting your implementation extend SignOffDefinitionValidator or AdjustmentValidator respectively.

Sign-Off Definitions

To provide custom validation for sign-off definitions, create a class that implements the ISignOffDefinitionValidator interface and override its methods. The interface is as follows:

/**
 * <b>ISignOffDefinitionValidator</b>
 *
 * <p> The validator used for validating Atoti Sign-Off Definitions.
 *
 * @author ActiveViam
 */
public interface ISignOffDefinitionValidator {

    /**
     * @param definitionToCreate the definition to be created
     * @return validation errors if the definition is not valid for creation, empty list otherwise
     */
    List<IValidationError> validateSignOffDefinitionCreation(ISignOffDefinition definitionToCreate);

    /**
     * @param definitionToUpdate the definition to be updated
     * @return validation errors if the definition is not valid for update, empty list otherwise
     */
    List<IValidationError> validateSignOffDefinitionUpdate(ISignOffDefinition definitionToUpdate);
}

A sample implementation of ISignOffDefinitionValidator could look like this:

public class MyCustomSignOffDefinitionValidator implements ISignOffDefinitionValidator {

    List<IValidationError> validateSignOffDefinitionCreation(ISignOffDefinition definitionToCreate) {
        if (definitionToCreate.getName() == null){
            return List.of(DefaultValidationError.builder()
                    .source(definitionToCreate)
                    .reason("The definition name cannot be null")
                    .action("Please ensure the name field is populated.")
                    .build());
        } 
        return Collections.emptyList();
    }

    List<IValidationError> validateSignOffDefinitionUpdate(ISignOffDefinition definitionToUpdate) {
        if (definitionToUpdate.getName() == null) {
            return List.of(DefaultValidationError.builder()
                    .source(definitionToUpdate)
                    .reason("The updated definition name cannot be null")
                    .action("Please ensure the name field is populated.")
                    .build());
        }
        return Collections.emptyList();
    }

}

Sign-Off Adjustments

To provide custom validation for sign-off adjustments, create a class that implements the ISignOffAdjustmentValidator interface and override its methods. The interface is as follows:

/**
 * <b>ISignOffAdjustmentValidator</b>
 *
 * <p> The validator used for validating Atoti Sign-Off Adjustments.
 *
 * @author ActiveViam
 */
public interface ISignOffAdjustmentValidator {

    /**
     * @param adjustmentToCreate the adjustment to be created
     * @return validation errors if the adjustment is not valid for creation, empty list otherwise
     */
    List<IValidationError> validateSignOffAdjustmentCreation(IAdjustment adjustmentToCreate);

    /**
     * @param adjustmentToUpdate the adjustment to be updated
     * @return validation errors if the adjustment is not valid for update, empty list otherwise
     */
    List<IValidationError> validateSignOffAdjustmentUpdate(IAdjustment adjustmentToUpdate);
}

A sample implementation of ISignOffAdjustmentValidator could look like this:

public class MyCustomSignOffAdjustmentValidator implements ISignOffAdjustmentValidator {

   List<IValidationError> validateSignOffAdjustmentCreation(IAdjustment adjustmentToCreate) {
        if (adjustmentToCreate.getSignOffTaskId() == null){
            return List.of(DefaultValidationError.builder()
                    .source(adjustmentToCreate)
                    .reason("The adjustment's task ID cannot be null")
                    .action("Please ensure the task ID field is populated.")
                    .build());
        } 
        return Collections.emptyList();
    }

   List<IValidationError> validateSignOffAdjustmentUpdate(IAdjustment adjustmentToUpdate) {
        if (adjustmentToUpdate.getSignOffTaskId() == null) {
            return List.of(DefaultValidationError.builder()
                    .source(adjustmentToUpdate)
                    .reason("The adjustment's task ID cannot be null")
                    .action("Please ensure the task ID field is populated.")
                    .build());
        }
        return Collections.emptyList();
    }

}