Customizing the Prog Model
Introduction
Apache Causeway works by building a metamodel of the domain objects: entities, domain services, view models and mixins. Dependent on the sort of domain object, the class methods represent both state — (single-valued) properties and (multi-valued) collections — and behaviour — actions.
More specifically, both entities and view models can have properties, collections and actions, while domain services have just actions. Mixins also define only actions, though depending on their semantics they may be rendered as derived properties or collections on the domain object to which they contribute.
In the automatically generated UI a property is rendered as a field. This can be either of a value type (a string, number, date, boolean etc) or can be a reference to another entity. A collection is generally rendered as a table.
Additional business rules semantics are inferred both imperatively from supporting methods (such as disableXxx()
) and declaratively from annotations.
Taken together this set of conventions are what we call the Apache Causeway Programming Model.
In essence, these conventions are just an extension of the pojo / JavaBean standard of yesteryear: properties and collections are getters/setters, while actions are simply any remaining public
methods.
In fact, the Apache Causeway programming model is extensible; you can teach Apache Causeway new programming conventions and you can remove existing ones; ultimately they amount to syntax. The only real fundamental that can’t be changed is the notion that objects consist of properties, collections and actions. You can learn more about extending Apache Causeway programming model here. |
OIDs
As well as defining a metamodel of the structure (domain classes) of its domain objects, Apache Causeway also manages the runtime instances of said domain objects.
When a domain entity is recreated from the database, the framework keeps track of its identity through an "OID": an object identifier. Fundamentally this is a combination of its type (domain class), along with an identifier. You can think of it as its "primary key", except across all domain entity types.
For portability and resilience, though, the object type is generally an alias for the actual domain class: thus "customers.CUS", say, rather than "com.mycompany.myapp.customers.Customer". This is derived from an annotation. The identifier meanwhile is always converted to a string.
Although simple, the OID is an enormously powerful concept: it represents a URI to any domain object managed by a given Apache Causeway application. With it, we have the ability to lookup any arbitrary domain objects.
Some examples:
-
an OID allows sharing of information between users, eg as a deep link to be pasted into an email.
-
the information within an OID could be converted into a barcode, and stamped onto a PDF form. When the PDF is scanned by the mail room, the barcode could be read to attach the correspondence to the relevant domain object.
-
as a handle to any object in an audit record, as used by EntityPropertyChangeSubscriber;
-
similarly within implementations of CommandSubscriber to persist Command objects
-
similarly within implementations of ExecutionSubscriber to persist published action invocations
-
and of course both the RestfulObjects viewer and Web UI (Wicket viewer) use the oid tuple to look up, render and allow the user to interact with domain objects.
Although the exact content of an OID should be considered opaque by domain objects, it is possible for domain objects to obtain OIDs.
These are represented as Bookmark
s, obtained from the BookmarkService.
Deep links meanwhile can be obtained from the DeepLinkService.
OIDs can also be converted into XML format, useful for integration scenarios.
The common schema XSD defines the oidDto
complex type for precisely this purpose.
Custom validator
Apache Causeway' 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 Causeway' internal MetaModelValidator
interface, so the simplest option is just to implement MetaModelValidator
directly:
package org.apache.causeway.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.causeway.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.causeway.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 Causeway 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 Causeway 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 FacetFactory
s are responsible for installing Facets onto the metamodel elements.
There are many many such Facet
s, 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 FacetFactory
s registered defines the Apache Causeway 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 FacetFactory
s.
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:
To summarise:
-
To add to the programming model (new facet factories, validators or post processors), create a
@Service
implementing theMetaModelRefiner
interface -
to remove from the programming model, create a
@Service
implementing aProgrammingModelInitFilter
(with an earlier precedence than the default implementation).