Feature
Validation logic that can not be expressed by using the Kernel rule language, can be achieved in A12 by using custom conditions.
These custom conditions can be specified in the validation rules in the Document Model, but the logic is then implemented in the application and registered to the Kernel.
With this approach, for example it is possible to validate that all documents in the database are unique based on a unique identifier field (e.g. an email address).
For the MMH on Development & Modeling, an example of this has been implemented for Persons with an unique identifier field consisting of their full names, birthdays and place of birth.
For more information on custom conditions, you can refer to the Kernel documentation.
Implementation
The full code will be available with the recording of the session on the elearning plattform.
The custom condition needs to be implemented both on client-side as well as server-side. Kernel offers analogue API for this in Java and Typescript. Therefore, the implementation steps are the same for both cases:
- Create a class for the custom condition by implementing the
ICustomConditioninterface from Kernel. With thecheckmethod of that interface, you can provide the logic for what should be evaluated. - Implement the
ICustomConditionFactoryinterface from Kernel. This class is used to get the names of all supported custom conditions and to create a new instance of a custom condition for a given name. - Register the previously implemented
CustomConditionFactoryclass, so that Kernel is aware of it and can find the relevant custom conditions. TheDocumentRtCustomExtensionServicefrom Kernel can be used for this.
In the frontend, these steps can be implemented as follows to do the uniqueness checks for the person documents:
Step 1:
To simplify and for a faster validation, we will just check the documents in the state. This means that only the Person documents on the page will be validated.
If the overview has multiple pages or uses infinite scrolling, not all documents will be checked in the client-side custom condition. In this case the validation in the backend, will close this gap. However please be aware that the error will be displayed differently, if only the serevr-side validation fails. Please refer to the Pitfalls section below for more details.
File: client/src/modules/person/validation/customCondition.ts
//...
interface People {
UniqueIdentifier: string;
}
const documentService = new DocumentServiceFactory().getDocumentService();
export class CustomCheckForUniquenessCustomCondition implements ICustomCondition {
check(
document: Document,
//...
errorEntityInstance: EntityInstancePath
): boolean {
const fieldValue = documentService.getAssignedObject(document, errorEntityInstance)?.valueOf();
const state = store.getState();
const formActivity: Activity | undefined = ActivitySelectors.latestActivity()(state);
const overviewActivity: Activity | undefined = ActivitySelectors.activitiesByDescriptor({
module: "PersonModule",
engine: "overview"
})(state).pop();
if (formActivity && overviewActivity) {
const overviewDocuments: DocumentListData = ActivitySelectors.data(overviewActivity.id)(state) as DocumentListData;
for (const overviewDocument of overviewDocuments.documents) {
const person = overviewDocument?.People as People;
if (document.id !== overviewDocument?.id && person.UniqueIdentifier === fieldValue) {
return true;
}
}
}
return false;
}
}
Step 2:
File: client/src/modules/person/validation/customConditionFactory.ts
//...
export class CustomConditionFactory implements ICustomConditionFactory {
createCustomCondition(customConditionName: string): ICustomCondition {
if (customConditionName === "CustomCheckForUniqueness") {
return new CustomCheckForUniquenessCustomCondition();
}
throw new Error("Custom condition '" + customConditionName + "' not supported");
}
getSupportedConditionNames(): Set<string> {
return new Set<string>(["CustomCheckForUniqueness"]);
}
}
Step 3:
File: client/src/appsetup.ts
//...
export function setup(): {
config: ApplicationSetup;
initialStoreActions(): Promise<void>;
} {
//...
DocumentRtCustomExtensionService.registerCustomConditions(new CustomConditionFactory());
//....
}
The steps to implement this in the backend on the other hand look as follows:
Step 1:
File: server/app/src/main/java/com/mgmtp/a12/template/server/validation/CustomCheckForUniquenessCustomCondition.java
//...
@Component
public class CustomCheckForUniquenessCustomCondition implements ICustomCondition {
private final IDocumentRepository documentRepository;
private final DocumentServiceFactory documentServiceFactory;
public CustomCheckForUniquenessCustomCondition(IDocumentRepository documentRepository,
DocumentServiceFactory documentServiceFactory) {
this.documentRepository = documentRepository;
this.documentServiceFactory = documentServiceFactory;
}
@Override
public boolean check(IDocument currentDocument, Set<IEntityInstance> relevantEntityInstances, Set<IEntityInstance> formallyIncorrectEntityInstances, IEntityInstance errorEntityInstance) {
IDocumentSearchService currentDocumentSearchService = documentServiceFactory.createDocumentSearchService(currentDocument);
String fieldValue = getFieldValueForOptional(currentDocumentSearchService.getFieldInstance(
errorEntityInstance.getPath(),
errorEntityInstance.getRepetitions()
)
);
List<DocumentReference> docRefs = documentRepository.findAllDocRefsForModel(currentDocument.getDocumentModelId());
List<DataServicesDocument> persistedDocuments = documentRepository.findDocumentsByDocRefs(docRefs);
for (DataServicesDocument persistedDocument : persistedDocuments) {
IDocumentSearchService persistedDocumentSearchService = documentServiceFactory.createDocumentSearchService(persistedDocument.getKernelDocument());
String persistedFieldValue = getFieldValueForOptional(persistedDocumentSearchService.getFieldInstance(
errorEntityInstance.getPath(),
errorEntityInstance.getRepetitions()
)
);
if (!currentDocument.getId().equals(Optional.of(persistedDocument.getDocRef().getDocumentId()))
&& fieldValue.equals(persistedFieldValue)) {
return true;
}
}
return false;
}
private String getFieldValueForOptional(Optional<IFieldInstance> instance) {
return instance.isPresent() ? instance.get().getValue().get().toString() : null;
}
}
Please note that the implementation above has one problem: In the case of updating an existing document, the id in the current document that we would be updating is not set by Data Services.
We however need this to check that we are not comparing the document with itself and causing a false positive.
Therefore, we need to set the id manually by using an event listener:
File: server/app/src/main/java/com/mgmtp/a12/template/server/validation/DocumentEventListener.java
//...
@Component
public class DocumentEventListener {
@EventListener
public void beforeUpdateDocument(DocumentBeforeUpdateEvent event) {
event.getUpdatedDocument().setId(event.getDocumentReference().getDocumentId());
}
}
Step 2:
File: server/app/src/main/java/com/mgmtp/a12/template/server/validation/CustomConditionFactory.java
//...
@Component
public class CustomConditionFactory implements ICustomConditionFactory {
@Inject
IDocumentRepository documentRepository;
@Inject
DocumentServiceFactory documentServiceFactory;
@Override
public Set<String> getSupportedConditionNames() {
return Set.of("CustomCheckForUniqueness");
}
@Override
public ICustomCondition createCustomCondition(String customConditionName) {
if (customConditionName.equals("CustomCheckForUniqueness")) {
return new CustomCheckForUniquenessCustomCondition(documentRepository, documentServiceFactory);
}
throw new IllegalArgumentException("Integration error: Unsupported custom conditon: " + customConditionName);
}
}
Step 3:
When using Data Services it is not necessary to manually register the CustomConditionFactory, as they already register every custom condition factory that they can find during initialization.
This is done in dataservices-core/src/main/java/com/mgmtp/a12/dataservices/initialization/DataServicesInitializationService.java.
Pitfalls
In this implementation, the custom condition in the frontend covers the currently loaded documents in the overview. The checks on the other documents in the database are only performed by the server.
If the validation rule is triggered only server-side, the user will see a different style of error and a different error message. The error message would need to be customized additionally via localization for example, but the styling can currently not be adapted so easily. Improvements for this are requested in A12-15373.