This is an internal documentation. There is a good chance you’re looking for something else. See Disclaimer.

Validators

Validators are executed before the commit and are used to ensure data quality. They are executed in java and may throw error messages if something is not quite right.

Warning

Custom java validators must not be used if the requirement can be satisfied using standard entity validators (See Validation)

If a validator is required it can be created by implementing extending the AbstractEntitiesValidator class and implementing the validate method. The java code evaluates all entities of one entity model that were changed in a given transaction (first parameter of the validate method). If the entity validates successfully, nothing should be done in the validator. If it does not validate the validationResults.get(entity).setError can be used to cancel the transaction and show an error message for each faulty entity. The message can contain variables that may be filled in as described in Text-Resources.

Please find below an example that validates entity and throws an error if relTest_status is not null and mandatory_if_status_set is not filled in.

@Validator(filter = "Test_entity")
public class TestValidator extends AbstractEntitiesValidator {
    @Override
    public void validate(List<Entity> entities, Map<Entity, EntityValidationResult> validationResults) {
        entities.stream()
            .filter(entity -> entity.getRelatedEntityOrNull("relTest_status") != null
                && Strings.isNullOrEmpty(entity.getString("mandatory_if_status_set"))
            .forEach(entity -> validationResults.get(entity).setError(new TextMessage("validation.TestValidator.error_message"));
    }
}

Tip

Entity validation runs in the NULL business unit.

EntitiesValidator vs. EntityValidator

To optimise entity validation while importing entities the interface was changed for single entity validation (AbstractEntityValidator) to the current multi entity validation. If entities / data structures need to resolved this can and should be done once per transaction instead of once per entity. See for example the validator below. It checks if a default account, payment condition and currency is configured. This can now be done once per business unit instead of once per entity which reduces the number of queries while importing addresses significantly.

@Validator(filter = "Debitor_information")
public class DebitorInformationValidator extends AbstractEntitiesValidator {

    private static final String ERROR_MESSAGE = "validation.DebitorInformationValidator.missingDefaultMsg";

    private final Context context;
    private final QueryBuilderFactory queryBuilderFactory;
    private final BusinessUnitManager businessUnitManager;

    public DebitorInformationValidator(Context context,
                                       QueryBuilderFactory queryBuilderFactory,
                                       BusinessUnitManager businessUnitManager) {
        this.context = context;
        this.queryBuilderFactory = queryBuilderFactory;
        this.businessUnitManager = businessUnitManager;
    }

    @Override
    public void validate(List<Entity> debitorInformationEntities, Map<Entity, EntityValidationResult> validationResults) {
        Multimap<String, Entity> groupedDebitorInfoEntities = groupByBusinessUnit(debitorInformationEntities);

        for (String buId : groupedDebitorInfoEntities.keySet()) {
            if (defaultAccountNotExists(buId) || defaultPaymentConditionNotExists(buId) || defaultCurrencyNotExists(buId)) {
                groupedDebitorInfoEntities.get(buId).forEach(entity -> validationResults.get(entity).setError(new TextMessage(ERROR_MESSAGE)));
            }
        }
    }

    private Multimap<String, Entity> groupByBusinessUnit(List<Entity> debitorInformationEntities) {
        Multimap<String, Entity> result = ArrayListMultimap.create();
        debitorInformationEntities.forEach(entity -> result.put(businessUnitManager.getBusinessUnit(entity).getId(), entity));
        return result;
    }

    private boolean defaultAccountNotExists(String buId) {
        return defaultNotExists("Account", "default_summary", buId);
    }

    private boolean defaultPaymentConditionNotExists(String buId) {
        return defaultNotExists("Payment_condition", "default_payment_condition", buId);
    }

    private boolean defaultCurrencyNotExists(String buId) {
        return defaultNotExists("Currency", "default_currency", buId);
    }

    private boolean defaultNotExists(String entityName, String defaultBooleanFieldName, String buId) {
        QueryBuilder queryBuilder = queryBuilderFactory.find(entityName)
                .where(field(defaultBooleanFieldName).is(true));
        if (hasBuRelation(entityName)) {
            queryBuilder.where(field("relBusiness_unit.unique_id").is(buId));
        }
        EntityList defaultEntities = queryBuilder.build(context).execute();
        return defaultEntities.isEmpty();
    }

    private boolean hasBuRelation(String entityName) {
        return ((EntityModel) context.getEntityManager(entityName).getModel()).getBusinessUnitType().isLinked();
    }
}

All old validators will still work with the old interface. To make this posisble, a default implementation of the new interface method was added to the old interface. Please do not try to get rid of the old interface by copy-pasting the default implementation to each validator.

default void validate(List<Entity> entities, Map<Entity, EntityValidationResult> validationResults) {
    entities.forEach(entity -> validate(entity, validationResults.get(entity)));
}

Registration

Validators are usually automatically registered when annotated with the @Validator annotation. To define which entities should be validated the filter parameter can be used as seen in the following examples. If a validator should be applied to all entities filter="*" can be used.

Single entity model

@Validator(filter = "User")
public class TestValidator extends AbstractEntitiesValidator {
    // content
}

Mutiple entity models

@Validator(filter = {"User", "Address"})
public class TestValidator extends AbstractEntitiesValidator {
    // content
}

Post flush

Normal validator are executed before the change is flushed to the database. So queries cannot be executed on the database as the change is not persisted. With postFlush = true the validtor is executed after the change is flushed but before the transaction is commited. For example the QueryBuilderFactory can be used to verify something.

@Validator(filter = "User", postFlush = true)
public class TestValidator extends AbstractEntitiesValidator {

    private final QueryBuilderFactory queryBuilderFactory;

    // content
}

Register validator from different module

If you need to register a validator from a different module (e.g. a standard validator) this can be done in your module specific configuration. To reference the validator instance that in most cases is not visible in your module the class name can be used as parameter name. E.g. public class TestValidator extends AbstractEntitiesValidator -> EntitiesValidator testValidator.

@Bean
public EntitiesValidatorContribution testValidatorContribution(EntitiesValidator testValidator) {
    EntitiesValidatorContribution contribution = new EntitiesValidatorContribution();
    contribution.setValidator(testValidator);
    contribution.setFilter("Other_entity");
    return contribution;
}