Customizing the Prog Model

This chapter explains the main APIs to extend or alter the programming conventions that Apache Isis understands to build up its metamodel.

Custom validator

Apache Isis' programming model includes a validator component that detects and prevents (by failing fast) a number of situations where the domain model is logically inconsistent.

For example, the validator will detect any orphaned supporting methods (eg hideXxx()) if the corresponding property or action has been renamed or deleted but the supporting method was not also updated. Another example is that a class cannot have a title specified both using title() method and also using @Title annotation.

You can also impose your own application-specific rules by installing your own metamodel validator. To give just one example, you could impose naming standards such as ensuring that a domain-specific abbreviation such as "ISBN" is always consistently capitalized wherever it appears in a class member.

API and Implementation

There are several ways to go about implementing a validator.

MetaModelValidator

Any custom validator must implement Apache Isis' internal MetaModelValidator interface, so the simplest option is just to implement MetaModelValidator directly:

package org.apache.isis.core.metamodel.specloader.validator;

public interface MetaModelValidator {

    default void onFailure(
            @NonNull FacetHolder facetHolder,                       (1)
            @NonNull Identifier deficiencyOrigin,                   (2)
            @NonNull String deficiencyMessageFormat,
            Object ...args) {

        val deficiencyMessage =
            String.format(deficiencyMessageFormat, args);

        DeficiencyFacet.appendTo(                                   (3)
                facetHolder, deficiencyOrigin, deficiencyMessage);
    }
}
1 represents an element of the metamodel, either an ObjectSpecification (domain class or mixin), or an ObjectMember (property, collection or action), or an ObjectActionParameter.
2 identifier of the element
3 appends the message into the DeficiencyFacet associated with the element.

If the onFailure is called, then a message explaining the deficiency is stored.

The framework also provides a MetaModelValidatorAbstract that implements this interface. However, it is the responsibility of the validator itself to figure out how to iterate over the entire model.

Since this is a common use case, the framework provides a more convenient and fine-grained "Visitor" API, discussed next.

Visitor

More often than not, you’ll want to visit every element in the metamodel, and so for this you can instead subclass from MetaModelValidatorVisiting.Visitor:

package org.apache.isis.core.metamodel.specloader.validator;

public final class MetaModelValidatorVisiting
                        extends MetaModelValidatorAbstract {

    public interface Visitor {
        public boolean visit(                       (1)
            ObjectSpecification objectSpec,         (2)
            ValidationFailures validationFailures); (3)
    }

    // ...
}
1 return true continue visiting specs.
2 ObjectSpecification is the internal API representing a class
3 add any metamodel violations to the ValidationFailures parameter

If you have more than one rule then each can live in its own visitor.

SummarizingVisitor

As a slight refinement, you can also subclass from MetaModelValidatorVisiting.SummarizingVisitor:

package org.apache.isis.core.metamodel.specloader.validator;

public final class MetaModelValidatorVisiting
                        extends MetaModelValidatorAbstract {

    public interface SummarizingVisitor extends Visitor {
        public void summarize(ValidationFailures validationFailures);
    }

    // ...
}

A SummarizingVisitor will be called once after every element in the metamodel has been visited. This is great for performing checks on the metamodel as a whole.

Configuration

Once you have implemented your validator, you must register it with the framework. This is most easily done by implementing service that implements MetaModelRefiner service.

For example, some folk advocate that pattern names such as "Repository" or "Factory" should not appear in class names because they are not part of the ubiquitous language. Such a rule could be verified using this implementation:

@Service
public static class NoRepositorySuffixRefiner implements MetaModelRefiner {
    @Override
    public void refineProgrammingModel(ProgrammingModel programmingModel) {
        programmingModel.addValidator(new MetaModelValidatorVisiting.Visitor() {
            @Override
            public boolean visit(ObjectSpecification objectSpec, MetaModelValidator validator) {
                if(objectSpec.getSingularName().endsWith("Repository")) {
                    validator.onFailure(objectSpec, objectSpec.getIdentifier(), "Domain services may not have the suffix 'Repository'");
                }
                return true;
            }
        });
    }
}

Finetuning

The core metamodel defines APIs and implementations for building the Apache Isis metamodel: a description of the set of entities, domain services and values that make up the domain model.

The description of each domain class consists of a number of elements:

ObjectSpecification

Analogous to java.lang.Class; holds information about the class itself and holds collections of each of the three types of class' members (below);

OneToOneAssociation

Represents a class member that is a single-valued property of the class. The property’s type is either a reference to another entity, or is a value type.

OneToManyAssociation

Represents a class member that is a collection of references to other entities. Note that Apache Isis does not currently support collections of values.

ObjectAction

Represents a class member that is an operation that can be performed on the action. Returns either a single value, a collection of entities, or is void.

The metamodel is built up through the ProgrammingModel, which defines an API for registering a set of FacetFactorys. Two special FacetFactory implementations - PropertyAccessorFacetFactory and CollectionAccessorFacetFactory - are used to identify the class members. Pretty much all the other FacetFactorys are responsible for installing Facets onto the metamodel elements.

There are many many such Facets, and are used to do such things get values from properties or collections, modify properties or collections, invoke action, hide or disable class members, provide UI hints for class members, and so on. In short, the FacetFactorys registered defines the Apache Isis programming conventions.

Modifying the Prog. Model

Creating the ProgrammingModel is the responsibility of the ProgrammingModelService.

The default implementation, ProgrammingModelServiceDefault, creates as the ProgrammingModel the concrete implementation ProgrammingModelFacetsJava8, which registers a large number of FacetFactorys. This programming model can then be added to because the service call every known implementation of MetaModelRefiner.

The programming model can also be filtered using the ProgrammingModelInitFilter interface. The default implementation of this interface, ProgrammingModelInitFilterDefault accepts all facet factories though allows deprecated facet factories to be excluded through a configuration property.

The diagram below shows how these classes relate:

Programming Model classes
Figure 1. Programming Model classes

To summarise:

  • To add to the programming model (new facet factories, validators or post processors), create a @Service implementing the MetaModelRefiner interface

  • to remove from the programming model, create a @Service implementing a ProgrammingModelInitFilter` (with an earlier precedence than the default implementation).