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.

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.

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 must be registered as services and contributed to the nice2.model.entity.EntityValidators contribution.

<service-point id="TestValidator" interface="ch.tocco.nice2.model.entity.entityvalidators.EntitiesValidator">
  <invoke-factory>
    <construct class="ch.tocco.nice2.optional.test.impl.validator.TestValidator"/>
  </invoke-factory>
</service-point>

<contribution configuration-id="nice2.model.entity.EntityValidators">
  <validator validator="service:TestValidator" filter="User, Address"/>
</contribution>

The filter attribute defines which entities should be validated by this validator. If it is applicable for multiple entity models, the entity model names may be listed seperated by commas. If a validator should be applied to all entities filter="*" can be used.

Dependencies

hivemodule.xml

In the hivemodule.xml of the module containing the validator, two ClassLoader imports are required to run validators.

<contribution configuration-id="hiveapp.ClassLoader">
  <import feature="ch.tocco.nice2.model.entity" version="*"/>
  <import feature="ch.tocco.nice2.persist" version="*"/>
  <import feature="ch.tocco.nice2.textresources" version="*"/>
  <import feature="ch.tocco.nice2.validate" version="*"/>
</contribution>

pom.xml

In the impl pom.xml of the module containing the validator, two dependencies are required to compile validators.

<dependency>
  <groupId>ch.tocco.nice2.model.entity</groupId>
  <artifactId>nice2-model-entity-api</artifactId>
  <version>${project.version}</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>ch.tocco.nice2.persist.core</groupId>
  <artifactId>nice2-persist-core-api</artifactId>
  <version>${project.version}</version>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>ch.tocco.nice2.textresources</groupId>
  <artifactId>nice2-textresources-api</artifactId>
  <version>${project.version}</version>
  <type>jar</type>
  <scope>provided</scope>
</dependency>
<dependency>
  <groupId>ch.tocco.nice2.validate</groupId>
  <artifactId>nice2-validate-api</artifactId>
  <version>${project.version}</version>
  <type>jar</type>
  <scope>provided</scope>
</dependency>