Authors

Arne Vandamme, Steven Gentens

About

The EntityModule is responsible for automatically generating an administration UI of entities built. It provides infrastructure to define an entity model and to generate default web pages based on that model. By default all Spring Data repositories are introspected and corresponding entity models are built for every repository.

What’s new in this version?

2.2.0.RELEASE

  • added message codes for admin menu items

    • of the entity management section

    • of a group of entities \(defaults to the module name\)

    • of a single entity type

  • EntityQuery Language allows specifying an order by clause or a Sort specifier

  • OptionIterableBuilder can return a sorted specification by implementing the isSorted\(\) method

    • if the OptionGenerator has no explicit sorting parameter set, it now only sorts if the configured OptionIterableBuilder is not sorted

    • if you specify option values using an EQL statement, the sort specifier of your EntityQuery will be taken into account

  • EntityQuery filtering now supports a basic and advanced mode to support the use of configured property filters and the use of eql statements

2.1.0.RELEASE

  • improve the ability to customize page titles and layouts

    • all entity views now set a page \(sub\) title if a matching message code returns a non-empty string

      • there is a default title for all views except the list view

    • list views now also publish an EntityPageStructureRenderedEvent

  • select option controls now support SelectFormElementConfiguration to render more advanced bootstrap-select controls

  • added ILIKE operator to the EntityQuery Language for case insensitive matching on String columns

    • an EntityQueryConditionTranslator attribute can be registered on entity properties to ensure regular equal and like lookups are always converted to the case insensitive equivalent

2.0.0.RELEASE

This release has a lot of breaking changes compared with the previous release. The code has been heavily rewritten and optimized. The public API modified accordingly with a focus on simplification and future extensibility.

  • requires Across 2.0.0+

  • massive overhaul of the EntitiesConfigurationBuilder system - removed the and() concatenating of builder calls

  • massive overhaul of EntityViewFactory, EntityViewProcessor and the default administration controllers

    • nested builder consumers are used instead - this greatly simplified the class hierarchy involved

    • externalized the entire ViewElement infrastructure to BootstrapUiModule

    • if BootstrapUiModule is not present, default views will not be created

  • compatibility update with Spring 4.2 which replaces CrudInvoker with RepositoryInvoker from spring-data-commons.

  • principal names on Auditable entities are now pretty printed using the SecurityPrincipalLabelResolverStrategy from the SpringSecurityModule

  • EntityModule now supports deleting of entities

  • the EntityModel of an EntityConfiguration can now be customized using the EntityConfigurer builders

  • extension of the EntityQuery infrastructure

    • addition of the EntityQuery Language \(EQL\) providing SQL-like syntax for building an EntityQuery

    • provide a default EQL-based filter for list views

  • addition of the entity browser in the Developer tools section of AdminWebModule

    • allows seeing all registered entities along with their attributes, properties, views and associations

    • the entity browser is only activate if development mode is active

  • streamlined the message code hierarchy for view rendering, see appendix for details

  • a list view can now have a default predicate assigned using an EQL statement

    • this can be used to ensure a list result always has a default filter applied

  • default entity views support transactions, allowing multiple processors to modify data in a single transaction

    • transactions are enabled by default for state modifying HTTP methods of all form views \(create, update, delete and custom form views\)

  • option controls \(select, multi-checkbox\) can be easily customized through a number of attributes

    • making it easier to specify the option values that can be selected

General information

Artifact

    <dependencies>
        <dependency>
            <groupId>com.foreach.across.modules</groupId>
            <artifactId>entity-module</artifactId>
            <version>{entity-module-version}</version>
        </dependency>
    </dependencies>

Module dependencies

Module Type Description

AdminWebModule

optional

Enables auto generated forms for managing the registered entities.

Module settings

Property Description Default

entityModule.entityValidator.registerForMvc

Should the entity Validator instance be registered as the default validator for MVC databinding.

true

How EntityModule works

1. Entity views

EntityModule has the ability to generate automatic views for entities. When AdminWebModule is present, automatic CRUD pages for entities will be generated using these views.

A view is an abstraction of web controller that allows you to take advantage of the EntityModule configuration system. Usually views are a rendering of one or more entities of a particular entity type.

Both EntityConfiguration and EntityAssociation can have their own views defined. They do so by implementing the EntityViewRegistry interface. Every view has a unique name within an EntityViewRegistry.

1.1. Default views

By default CRUD views will be created for every EntityConfiguration with a CrudRepository or an EntityQueryExecutor registered. Likewise CRUD views will be created for every EntityAssociation with an AssociatedEntityQueryExecutor attribute.

Query executor attributes should be registered automatically in most cases. So you usually don’t need to worry about those. See the chapter on EntityQueryExecutor for more information.

The following default views will be created for an entity type:

View name View type Description

listView (EntityView.LIST_VIEW_NAME)

list view

Shows the list of entities.

createView (EntityView.CREATE_VIEW_NAME)

form view

Renders and executes the form for creating a new entity.

updateView (EntityView.UPDATE_VIEW_NAME)

form view

Renders and executes the form for updating an entity.

deleteView (EntityView.DELETE_VIEW_NAME)

form view

Shows the confirmation page and performs the delete if allowed.

The default views all render markup using the ViewElement infrastructure and the implementations provided by the BootstrapUiModule.

1.1.1. How entity views work

An EntityView is created by an EntityViewFactory based on an EntityViewRequest. A controller is still responsible for executing (usually rendering) the actual EntityView.

The GenericEntityViewController is responsible for loading and rendering all default views in the AdminWebModule UI.

The path of the web request to GenericEntityViewController will determine:

  • the name of the view that should be rendered

  • which EntityViewRegistry will be used for loading the view (EntityConfiguration or EntityAssociation)

You can force the view being rendered by simply providing the view name as a request parameter with key view.

The controller creates an EntityViewRequest, retrieves the EntityViewFactory and builds the corresponding EntityViewContext and EntityViewCommand.

1.1.2. EntityViewRequest

Wraps together all the information of a specific entity view request:

  • the web request that is creating the view

  • the contextual information of the entity being viewed (represented by the EntityViewContext)

  • the command that should be executed on the EntityViewFactory (represented by the EntityViewCommand)

  • the name of the view being requested and the EntityViewFactory being used

  • the PageContentStructure of the page being rendered (if applicable)

Please refer to the EntityViewRequest javadoc for a description of all available properties.

1.1.3. EntityViewContext

The EntityViewContext contains contextual information and provides access to components that should be used for building a view. Contextual information is for example the entity type, specific entity or entity association being viewed. Components are things like the EntityLinkBuilder, EntityMessages or AllowableActions

Please refer to the EntityViewContext javadoc for a description of all available properties.

EntityViewContext is available as a request-scoped bean but only in case of a request passing through the GenericEntityViewController will the data be loaded. Loading the EntityViewContext attributes manually can be done by using the EntityViewContextLoader bean. You should use the ConfigurableEntityViewContext interface to modify a view context.

1.1.4. EntityViewCommand

The EntityViewCommand represents the command that should be executed on the EntityViewFactory to create the corresponding EntityView. Most often the command represents the form of a view where the request parameters are bound to.

An EntityViewCommand has 2 attributes: the (optional) entity DTO being bound and a map of named extensions. Customizing view processing can be done by registering additional extensions that will be rendered and validated upon binding.

The EntityViewCommand will be validated for every state modifying request: web requests using POST, PUT, PATCH or DELETE.

Please refer to the EntityViewCommand javadoc for a description of all available properties.

1.1.5. EntityViewFactory

Every view is built by an EntityViewFactory. An EntityViewRegistry (like EntityConfiguration or EntiyAssociation) is nothing but a map of EntityViewFactory by view name.

An EntityViewFactory is a stateless controller with 4 controller methods that are expected to be called in a specific order. Please refer to the javadoc for an overview of the different controller methods and their purpose.

DispatchingEntityViewFactory and EntityViewProcessor

EntityModule has only a single implementation of EntityViewFactory in the form of the DefaultEntityViewFactory. The DefaultEntityViewFactory:

  • supports ViewElement based rendering model - heavily used by EntityModule for building the HTML markup

  • dispatches its controller methods in a more fine-grained manner to a list of registered and ordered EntityViewProcessor instances.

The default builders when configuring views only register EntityViewProcessor beans on the factory. When an EntityViewFactory controller method is being called, it will be translated into one or more processor methods and those method calls will be dispatched to all processors in order.

Two adapter EntityViewProcessor classes are available:

  • use EntityViewProcessorAdapter for most use cases, it provides several adapter methods that also allow you to hook into the ViewElement generation lifecycle

  • use SimpleEntityViewProcessorAdapter if you do not need to hook into the ViewElement generation lifecycle

The DefaultEntityViewFactory will not execute any of the rendering related methods if any of the previous methods has marked the EntityView as being a redirect.

Please refer to the EntityViewProcessor, EntityViewProcessorAdapter and SimpleEntityViewProcessorAdapter for more details on the available processor methods. The appendix also provides a list of all available general purpose processors.

Transaction support

The DefaultEntityViewFactory uses a TransactionalEntityViewProcessorRegistry and enables transactions on all state modifying HTTP methods: POST, PUT, PATCH or DELETE. If a transaction manager bean name is available on the EntityConfiguration, transactions will be enabled by default for all form views: create, update, delete and custom form views. This means that all calls in state modifying doControl() methods of all EntityViewProcessor instances will happen in a single transaction.

The transaction manager bean name is registered as an attribute EntityAttributes.TRANSACTION_MANAGER_NAME and is detected automatically for every Spring Data repository based entity.

Manually enabling transactions on a view

You can enable transactions manually on the EntityViewFactoryBuilder by specifying either the PlatformTransactionManager to use, the name of the transaction manager bean or a TransactionTemplate if you need more fine-grained control.

1.1.6. Model attributes

The GenericEntityViewController exposes the following model attributes to the Spring MVC view:

Attribute name Value

entityViewRequest

EntityViewRequest

entityViewCommand

EntityViewCommand

entityViewContext

EntityViewContext

1.1.7. Default view types

EntityModule supports 3 view types by default.

When defining a new view (see the next section) it will be one of these types. The view type determines the base template that will be used to setup the EntityViewFactory.

The following view types are defined:

View type Template name Description

list view

listView (EntityView.LIST_VIEW_NAME)

Base configuration for rendering a list of entities.

form view

updateView (EntityView.UPDATE_VIEW_NAME)

Base configuration for rendering a form for a single entity.

generic view

genericView (EntityView.GENERIC_VIEW_NAME)

Barebone configuration for visualizing a single entity.

The template name can be used to replace the initializer for the EntityViewFactoryBuilder. See the chapter on the EntityViewFactoryBuilderInitializer.

See also the next chapters for more information on list view, form view and generic view.

1.1.8. Configuring views

Existing views can be modified or new ones registered using an EntityViewFactoryBuilder or EntityListViewFactoryBuilder. You usually don’t create these manually but get a builder for the corresponding view from the configuration or association builder.

The builders provide common properties that will configure one or more EntityViewProcessor instances on the view factory. They also allow you to modify the processor collection directly by adding or removing processors, or by post-processing the entire EntityViewProcessorRegistry.

Example adding an EntityViewProcessor to the default list view
configuration.withType( MyEntity.class )
             .listView( lvb -> lvb.viewProcessor( myProcessor ) );

The following chapters provide some more details on how to configure the default view types.

1.2. List view

default settings of a list view:

  • user must have read allowable action

  • renders values as LIST_VALUE

  • shows all readable properties

  • adds update/delete buttons for every item if the user as update and delete action respectively

  • supports paging and sorting

  • allows configuring sortable properties and the default sort

  • includes a form at the top that can be used for adding filters

  • a create button for the entity if the user has create action allowed

  • supports global feedback messages set with the EntityViewPageHelper

default processors

1.2.1. Adding a list view

TODO

1.2.2. Customizing a list view

TODO

Custom sorting

Customizing the sort behaviour of a specific property can be done by setting a Sort.Order.class attribute on the EntityPropertyDescriptor. You can use this to sort on a different property instead, or to specify behaviour for null handling and case sensitivity.

1.2.3. Filtering a list view

By default a list view is not filtered. If your entity has an EntityQueryExecutor available you can easily activate a simple yet powerful EQL-based filter. Activating EntityQuery based filtering can be done through the builders:

// Activate the EQL-based filter on every entity that has an EntityQueryExecutor
configuration.matching( c -> c.hasAttribute( EntityQueryExecutor.class ) )
             .listView( lvb -> lvb.entityQueryFilter( true ) );

See this chapter for information on how to configure a custom filter for a list view.

1.2.4. Adding a custom filter to a list view

A filter on a list view usually consists of 2 parts:

  • an EntityViewProcessor that provides the filtering options on the list view

  • an EntityViewProcessor that fetches the items using the filtering options

    • a custom form is usually added to the EntityViewCommand.addExtension() for both postback and optional validation

Both parts can easily be combined in a single EntityViewProcessor.

Custom filter example

The following code illustrates adding a simple filter to a view. The filter uses a separate repository method to lookup entities by name. The filter options are added as a form on top of the list view, the form in this case rendered via a custom Thymeleaf template.

Implementation of a single class that holds all filter logic
private static class GroupFilteringProcessor extends EntityViewProcessorAdapter
{
        @Autowired
        private GroupRepository groupRepository;

        @Override
    public void initializeCommandObject( EntityViewRequest entityViewRequest,
                                         EntityViewCommand command,
                                         WebDataBinder dataBinder ) {
        command.addExtension( "filter", "" );
    }

    @Override
    protected void doControl( EntityViewRequest entityViewRequest,
                              EntityView entityView,
                              EntityViewCommand command,
                              BindingResult bindingResult,
                              HttpMethod httpMethod ) {
        String filter = command.getExtension( "filter", String.class );
        Pageable pageable = command.getExtension( PageableExtensionViewProcessor.DEFAULT_EXTENSION_NAME, Pageable.class );

        if ( !StringUtils.isBlank( filter ) ) {
            entityView.addAttribute( "items", groupRepository.findByNameContaining( filter, pageable ) );
        }
        else {
            entityView.addAttribute( "items", groupRepository.findAll( pageable ) );
        }
    }

    @Override
    protected void postRender( EntityViewRequest entityViewRequest,
                               EntityView entityView,
                               ContainerViewElement container,
                               ViewElementBuilderContext builderContext ) {
        Optional<ContainerViewElement> header = find( container, "entityListForm-header", ContainerViewElement.class );
        header.ifPresent(
                h -> {
                    Optional<NodeViewElement> actions
                            = find( h, "entityListForm-header-actions", NodeViewElement.class );
                    actions.ifPresent( a -> a.addCssClass( "pull-right" ) );

                    h.addChild( new TemplateViewElement( "th/entityModuleTest/filters :: filterForm" ) );
                }
        );
    }
}
Custom Thymeleaf template that builds the form
<fragments xmlns:th="http://www.w3.org/1999/xhtml">
    <div class="list-header form form-inline" th:fragment="filterForm">
        <div class="form-group">
            <label for="group-name-filter">Filter by name:</label>
            <input id="group-name-filter" name="extensions[filter]" th:value="${entityViewCommand.extensions['filter']}" type="text" class="form-control" />
        </div>
        <input type="submit" class="btn btn-default" value="Apply filter" />
    </div>
</fragments>
Registration of the custom filter on the list view
entities.withType( Group.class )
        .listView( lvb -> lvb
            .entityQueryFilter( false )           // optional - disable the previously activated entity query filter
            .filter( groupFilteringProcessor() )  // register the custom filter
                );

1.2.5. List summary view

It is possible to activate a detail view inline in a list view. If the EntityConfiguration or EntityAssociation has a view named listSummaryView a summary pane will automatically become available when clicking on the item row in the table. The summary pane is called using AJAX and only the content fragment of the page will be rendered.

// Activate a summary view in the main user results table using a custom Thymeleaf template
configuration.withType( User.class )
             .view( EntityView.SUMMARY_VIEW_NAME, vb -> vb.template( "th/myModule/userSummary" ) );

1.3. Form view

1.3.1. create and update view

default settings of a form view:

  • user must have update allowable action

  • renders values as LIST_VALUE

  • shows all readable properties

  • adds update/delete buttons for every item if the user as update and delete action respectively

  • supports paging and sorting

  • allows configuring sortable properties and the default sort

  • includes a form at the top that can be used for adding filters

  • a create button for the entity if the user has create action allowed

  • supports global feedback messages set with the EntityViewPageHelper

default processors

1.4. Delete view

default settings

default processors

A delete action will be available for all entities where AllowableAction.DELETE is present, this is the default unless more explicit permissions are configured. A delete will always redirect to a confirmation page by default. Because the possibility to delete an entity often depends on other factors (usually associations), the default EntityDeleteViewFactory publishes an event that allows customizing said confirmation page.

By catching the BuildEntityDeleteViewEvent your code can:

  • suppress the ability to delete (by hiding the delete button)

  • add associations to the form

  • add custom feedback messages to the form (and optionally remove the associations block)

This should be sufficient for most use cases without having to revert to custom EntityViewProcessor implementations. Of course the latter would work as well.

Entity associations

The initial BuildEntityDeleteViewEvent is configured based on the EntityAssociation list of the entity. If associated items are detected, they influence the form settings depending on the parentDeleteMode property of the EntityAssociation:

  • ParentDeleteMode.IGNORE: item information is not printed nor influences the ability to delete

  • ParentDeleteMode.WARN: item information is printed on the form but does not influence the ability to delete

  • ParentDeleteMode.SUPPRESS: item information is printed on the form and disables the ability to delete, this is the default setting

The event is published after the initial association information has been set.

Performing the delete

The EntityModule simply calls the delete method of the EntityModel, usually a direct call to a repository delete(). You will have to take care yourself of complex delete scenarios - like deleting the associations - by either modifying the EntityModel or using another mechanism like the EntityInterceptor.

1.4.1. creating an additional form view

1.5. custom views

1.6. AdminWebModule JQuery plugins

The default EntityModule web resources add some JQuery based javascript plugins.

1.6.1. EntityModule object

All EntityModule and BootstrapUiModule javascript can be initialized by calling EntityModule.initializeFormElements(). This method optionally takes an argument that is the node in which the form elements should be initialized.

This is automatically done on document load, but when using AJAX fragment rendering, you usually want to re-initialize the DOM element that was updated.

Custom initializers

You can easily add a custom initializer function by adding it with EntityModule.registerInitializer( callback ). There is no need to manually execute your callback on document load, as that will happen automatically by the EntityModule.

Don’t execute your callback on document load and then add it to the initializers. Execution will happen automatically when calling registerInitializer().
Example registering a custom initializer that configures a sortable table to use AJAX loading
EntityModule.registerInitializer( function( node ) {
    $( '[data-tbl-type="paged"]', node )
        .on( "emSortableTable:prepareData", function( e, params ) {
            console.log( "enhancing the data", params );
            params['_partial'] = 'content';
        } )
        .on( "emSortableTable:loadData", function( e, params ) {
            console.log( "performing ajax load" );
            e.preventDefault();

            $.get( '#', $.param( params, true ), function( data ) {
                   $( '.pcs' ).replaceWith( data );
                   // initialize the form elements in the element just updated
                   EntityModule.initializeFormElements( $('.pcs') );
               }
            );
        } )
} );

1.6.2. Sortable tables

The default list views support paging and sorting of the pages, where client-side code is used to trigger reloading of the page.

Every table matching the selector [data-tbl-type="paged"] will be initialized for sorting and paging. A sortable table considers 3 default parameters:

  • page: page number

  • size: number of results a single page should have

  • sort: array of sort strings: field,direction (eg. name,ASC)

If a table is bound to a form (specified by the data-tbl-form attribute), paging or sorting will result in that form being submitted with the parameters being added as hidden form elements. If no form is bound, the current URL will be reloaded and the parameters added to the query string.

Customizing behaviour

You can hook into the default behaviour by using the events a sortable table emits or listens to.

Event Description Argument

emSortableTable:moveToPage

Trigger this event if you want to reload the data for a specific page.

Page number.

emSortableTable:sort

Trigger this event if you want to reload the data with different sorting. If the data is already sorted on the field specified, the sort order will be reversed.

Name of the field to sort on. Usually value of the data-tbl-field attribute.

emSortableTable:prepareData

Called after determining page number, result size and fields to sort on. Subscribe to this event if you want to expand or modify the parameters that should be submitted.

Parameter map containing: page,size and sort keys.

emSortableTable:loadData

Called after the parameters for the data have been prepared. Subscribe to this event if you want to provide a custom method of fetching the data (eg AJAX based). Note that you have to prevent the default execution if you provide your own mechanism.

Parameters that should be used for fetching the data.

Manually creating sortable tables

If you want to manually initialize a sortable table you can directly call the JQuery plugin emSortableTable() on any element.

You can easily create the valid structure for a sortable table using the EntityViewElementBuilderHelper. This allows you to create a SortableTableBuilder that builds a ViewElement that renders the right markup including all data attributes.

A valid sortable table requires several data attributes to be present on the DOM element:

Attribute Description

data-tbl

Unique id of the data table. Also used on column headings and pager control elements to specify the table they belong to.

data-tbl-form

(Optional) Name of the form that should be submitted when reloading the table data.

data-tbl-total-pages

Total number of pages in the result set.

data-tbl-size

Single page size.

data-tbl-current-page

Current page number (0 based).

data-tbl-sort

Current sort value. This is a JSON object structure containing the actual sort fields and their order. Depending on the the presence of custom Sort.Order.class attributes on the EntityPropertyDescriptor these field names will be the same as the property names.

data-tbl-field

Only used on heading cells that should be sortable. Specifies the field this column represents.

data-tbl-sort-property

Only used on heading cells that should be sortable. Contains the actual property name that is being sorted on. Usually the same as the field name.

data-tbl-page-selector

Used on a pager text field that contains the page number.

data-tbl-page

Used on any element that should navigate to a page on a click event. Contains the value of the page that should be navigated to.

1.7. EntityLinkBuilder

An EntityConfiguration or EntityAssociation can have one or more EntityLinkBuilder instances registered in its attributes. An EntityLinkBuilder is used to create application links to management controllers for the entity. By default the EntityModule will create an EntityLinkBuilder for the management pages in admin web if AdminWebModule is present, and this link builder will be registered as the attribute with EntityLinkBuilder class as key.

You can use the EntityLinkBuilder directly for example in redirects, often the specific EntityLinkBuilder is overridable per view. All links the EntityLinkBuilder generates are entirely configurable, please refer to the javadoc for all possible settings.

EntityLinkBuilder linkBuilder = entityConfiguration.getAttribute( EntityLinkBuilder.class );

// Will create a link of the form "/entities/{parent}/{parentId}/update"
String path = linkBuilder.update( parent );

1.7.1. EntityLinkBuilder for associations

Associations usually also have an EntityLinkBuilder registered, it is possible to create links to items that are an association from a parent entity. To achieve this you must scope the EntityLinkBuilder to the parent entity it belongs to.

EntityLinkBuilder linkBuilder = entityConfiguration.getAttribute( EntityLinkBuilder.class );

EntityConfiguration associated = association.getTargetEntityConfiguration();
EntityLinkBuilder associatedLinkBuilder = association.getAttribute( EntityLinkBuilder.class )
                                                     .asAssociationFor( linkBuilder, parent );

// Will create a link of the form "/entities/{parent}/{parentId}/associations/{associationName}/{childId}/update"
String path = associatedLinkBuilder.update( child );

2. Entity associations

The EntityModule attempts to automatically detect related entities and creates associations mainly to facilitate UI generation. Currently @OneToMany, @ManyToMany and @ManyToOne annotations from javax.persistence API are all scanned and used to build EntityAssociation entries.

In the administrative UI the management of related entities can often be done either through the property or the association. This is especially the case for @ManyToMany and @OneToMany associations that are mapped through a property with collection type. By default related entity management will be done through the property and the association will be generated but hidden.

If you want to enable management through the association interface, you should manipulate the hidden property of both the association and the property using an EntityConfigurer.
@Override
public void configure( EntitiesConfigurationBuilder configuration ) {
    // Groups should be managed through the association instead of the property
    configuration.withType( MachinePrincipal.class )
                 .properties( props -> props.property( "groups" ).hidden( true ) )
                 .association( ab -> ab.name( "machinePrincipal.groups" ).show() );
}

2.1. Association type

Every EntityAssociation is of a specific type, configured through the associationType property. The association type determines how the associated values can be managed through the user interface.

The following association types are possible:

Association type Behaviour

LINKED

The related entities are only linked to. The tab of the parent entity shows the list of related entities, but any modify action will navigate away from the parent entity.

This is the appropriate type if your related entities can in turn have other related entities. Usually this also means the related entity type has a main menu item.

EMBEDDED

The related entities will be managed through the tab of their parent entity. Modifying a related entity will be displayed as modifying the parent entity, no action will leave the context of the parent entity.

Use this type if the related entities only exist if their parent exists. Usually the related entity type does not have any menu item, nor sub tabs (the latter would not be displayed).

By default an association is of type LINKED.

2.2. ParentDeleteMode

An EntityAssociation has a parentDeleteMode property that determines how associated items will influence the ability to delete in the user interface. The default value is SUPPRESS but can be set through the EntitiesConfigurationBuilder.

For more information see the delete view chapter.

2.3. Association naming and location

Associations are added to the EntityConfiguration for which it makes most sense to manage them from a UI perspective. The association naming however is done according to the entity class and property names.

Example:

  • entity Group

  • entity User has a one to many with Group on property groups

  • association user.groups will be created on the entity configuration of Group

2.4. Customize associated entity creation

You can customize the creation of an associated entity in the form views, by setting a custom EntityFactory. This is especially useful for manually creating associations.

An EntityAssociation can have an EntityFactory.class attribute set that contains the EntityFactory that should be used for creating associated items. If no factory is set as attribute on the association, the default EntityFactory of the target configuration will be used.

If there is an EntityFactory attribute set on the association, that factory will be used when creating a new associated entity instance. The createNew() factory method will get called with the parent entity (for whom an associated item is being created) as single parameter.

3. Integration with other modules

3.1. AdminWebModule

If the AdminWebModule is present entity management controllers will be created for all registered entity configurations. If you want to avoid the automatic registration of entity management controllers for a particular entity type, you should set the EntityConfiguration as hidden. This will effectively disable the default entity controllers for that type, and hide the existence of the entity type from the administration interface.

You can also hide one or more associations. By default an association will not be shown if one of the participating entities is hidden. If you specify the hidden property of an EntityAssociation explicitly, that value will take precedence of the entity configurations. This way it is possible to generate management pages for associated entities, but not for the main entity type.

3.2. AdminWebModule: developer tools

When integrated with AdminWebModule and development mode is active, an entity registry browser will be added to the Developer tools section of the administration ui. The browser allows you to inspect the registered entities along with their views, associations and properties.

3.3. Auditable

If SpringSecurityModule is present, EntityModule adapts the default views for Auditable entities. The createdBy and lastModifiedBy properties are rendered using an AuditablePrincipalPropertyViewElementBuilder which uses the SecurityPrincipalLabelResolverStrategy to generate a pretty label for a principal (eg. full name instead of username). The default properties are removed from default views, but an aggregated property created and lastModified is added. The aggregated properties combine both the timestamp and the principal in a single property using the AuditablePropertyViewElementBuilder.

See the AuditableEntityUiConfiguration for full customization.

Customizing generated Entity views

The following section gives an overview of common customizations for generated entity views.

1.1. Customizing the EntityViewFactoryBuilderInitializer

1.2. Changing entity names, property names or other labels

All entity names, property names and labels can be customized using message sources. For an explanation of the different message codes used, see the relevant appendix.

1.3. Setting page title or changing page layout

Setting a page title can be done by adding the corresponding message code. All default views automatically add a page title (optionally with sub text) if the corresponding message code resolves a non-empty string.

See the message codes appendix for a list of relevant message codes.

Changing the page layout

Entity views use a PageContentStructure for the base structure of the web page. The PageContentStructure is available as a request scoped bean, but can also be retrieved from the EntityViewRequest.

See the AdminWebModule reference documentation for a basic explanation of PageContentStructure.

Modifying the page layout for all (or a selection of) views

If you want to modify page layout for multiple views at runtime, you can subscribe to the EntityPageStructureRenderedEvent. This event is published during the postRender() phase and gives you context of the view that is being rendered, allowing you to make changes outside regular EntityViewProcessor implementations.

SingleEntityPageStructureViewProcessor and ListPageStructureViewProcessor are the view processors responsible for building the basic page structure and publishing the event.

1.4. Specifying a custom template

Every default view uses a specific (Thymeleaf) template that renders the ViewElement list created by the view. If you want control over the rendering through a separate template you can specify a different template using the template() method on the EntityViewFactoryBuilder.

1.5. EntityViewProcessor

Modifying a default view can be done by registering an EntityViewProcessor for that view. This API allows you to modify the ViewElement collection that should be generated. This is a useful hook to add for example custom form elements that you wish to add and process. If can also be used to reorganize the layout of the form from backend code using the ContainerViewElementUtils.

1.6. Using a custom EntityViewFactory

Full control can be done by registering a custom EntityViewFactory implementation.

1.7. Selecting properties

EntityPropertySelector, incremental builders, keep current, select all, select all without default filter, exclude

1.8. Configuring property view types

You can configure a property using the EntityPropertyDescriptorBuilder. This builder also contains some methods to influence the ViewElement that should be built for that property for a given mode.

By default a ViewElement will be built based on the property and some of its annotations. There are 3 ways you can influence the default behaviour:

  • specify a custom viewElementType() for a given mode

    • a default builder of that type will be created for that mode

  • specify one or more viewElementPostProcessor() for a given mode

    • these ViewElementPostProcessor instances will be added to the default builder, in the order they were registered

  • specify a custom viewElementBuilder() for a given mode

    • the default building will be ignored and only your custom builder will be used

1.9. Fieldset properties

A fieldset is a visual grouping of other properties, inside a block that has a title (legend) and optional description. Fieldsets are rendered as a FieldsetFormElement. You can postprocess a group of ViewElement instances and move them manually to a FieldsetFormElement, or you can set a fieldset as the ViewElementMode for a property.

In the latter, because a fieldset is a collection of other properties, you will need to specify which properties make up the fieldset. Specifying the properties of fieldset is done by setting the EntityAttributes.FIELDSET_PROPERTY_SELECTOR to a valid EntityPropertySelector.

The following is an example of manually adding a fieldset property to a form, and moving some properties to it:

entities.withType( WebPage.class )
        .createOrUpdateFormView( fvb -> fvb
                /**
                 * First create a new property that is a fieldset
                 * of the existing url and urlGenerated properties.
                 * We add this property only to the scope of the
                 * create or update form view.
                 */
                .properties( props -> props
                        .property( "url-settings" )
                        .displayName( "URL settings" )
                        .viewElementType( ViewElementMode.FORM_WRITE, BootstrapUiElements.FIELDSET )
                        .attribute(
                                EntityAttributes.FIELDSET_PROPERTY_SELECTOR,
                                EntityPropertySelector.of( "url", "urlGenerated" )
                        )
                )
                /**
                 * Because url and urlGenerated are direct members
                 * of WebPage, we need to ensure they are not rendered
                 * directly anymore, so we remove them from the form view.
                 * The new url-settings property will be selected by default
                 * and in turn will render the url and urlGenerated properties.
                 *
                 * If we were to configure the url-settings property as hidden,
                 * we would have to explicitly include it in the form view as well.
                 * That would probably be a preferred approach if we have defined
                 * url-settings in the global property registry for WebPage.
                 */
                .showProperties( "*", "~url", "~urlGenerated" )
        )
Properties mapped to an @Embedded type will automatically be mapped as a fieldset type.

1.10. Customizing entity validation

By default annotation validation is performed on all entities. Customizing validation can be done by simply specifying a Validator bean that supports the specific entity type. You can use the EntityValidatorSupport as a base class to extend the default annotation based entity validation.

If more than one Validator could be applied, you will manually have to set the Validator.class attribute on the EntityConfiguration to the correct one.

1.11. Customizing VALUE mode elements

The ViewElementMode.VALUE and ViewElementMode.LIST_VALUE are the defaults to provide the output of a property for readonly views. Unless a specific ViewElement is configured, this will always be a String output of the property. By default the mvcConversionService will be used to convert the property value if no type specific builder is provided.

Apart from providing a custom ViewElement you can also modify the rendered output by providing attributes on the EntityPropertyDescriptor. If you provide a org.springframework.format.Printer.class attribute, that implementation will be used for printing the text value. Alternatively you can provide a java.text.Format.class attribute to be used. Note that most default Format implementations are not thread-safe, in that case you should wrap them in a SynchronizedFormat instance.

All standard view elements will use the Printer or Format attribute if one of them is present, instead of the default. A Printer attribute takes precedence over a Format.

1.12. Customizing textbox elements

TextboxFormElement.Type can be set as an attribute on the EntityPropertyDescriptor. If set and the property is generated as a TextboxFormElement, that type will be used.

You can add default post processors to the TextboxFormElementBuilderFactory to customize the autodetection.

1.13. Customizing numeric elements

By default all Number type properties will result in a NumericFormElement being used which is rendered as a textbox. The behavior can be customized by providing a NumericFormElementConfiguration. A default configuration will only be created for properties annotated with a Spring @NumberFormat for type CURRENCY or PERCENT, if no NumericFormElementConfiguration.class or NumericFormElementConfiguration.Format.class attribute is present.

If a NumericFormElementConfiguration is present a more advanced javascript control will be used in the front-end for value input. The same configuration will also be used for rendering the VALUE mode elements, formatting the output according to the properties configured.

Manually configuring percent

Put a format attribute with value PERCENT on the EntityPropertyDescriptor. This will create a locale specific percentage format with 2 decimals (unless the property type is integer). Alternatively use the static NumericFormElementConfiguration.percent() factory method to quickly create a localizable format suitable for percentages.

If you use Spring number format for PERCENT then 1 is expected to match 100%. If you manually create a NumericFormElementConfiguration it expects 100 to match with 100%. You can modify this behavior by setting the multiplier property on the configuration.
Manually configuring currency

The easiest way to configure a currency is to set a Currency.class attribute for the property. In that case a locale specific format for that currency will be created. Alternatively the same options as for percentages can be used and there is a NumericFormElementConfiguration.currency() factory method available.

1.14. Customizing datetime picker elements

By default all Date properties will result in a DateTimeFormElement which is rendered as a date time picker. The form element can be customized through the DateTimeFormElementConfiguration class. The default configuration is determined based on the presence of @Temporal annotations on the property. The date picker supports 3 major modes: date, time and timestamp (date + time) with minutes being the maximum resolution. The presence of @Past and @Future validation annotations will additionally restrict the dates that are selectable.

A specific date picker format can easily be specified by putting a DateTimeFormElementConfiguration.Format attribute. Advanced customization can be done by setting a complete DateTimeFormElementConfiguration as attribute. Dynamic configuration (for example setting the first selectable date relative to the current date) can only be done by specifying a DateTimeFormElementBuilder manually and adding a custom post processor that modifies the DateTimeFormElementConfiguration. A DateTimeFormElementConfiguration is always duplicated when creating an element so it is safe for post processors to modify the instance.

Using dates with TemporalType.TIME and JPA

A property of type java.util.Date but annotated with @Temporal(TemporalType.TIME) will result in only time selection being available (hours and minutes). However the @Temporal annotation also influences how JPA will persist the data type. If your type was created as a timestamp in the database schema, this might result in conversion errors. With Hibernate you can resolve this by additionally specifying a @Type annotation forcing the type to be persisted as timestamp.

Example of a required time property that is written as a date relative to start of epoch time in the database
@NotNull
@Column(name = "arrival_time")
@Temporal(TemporalType.TIME)
@Type( type = "timestamp")
private Date arrivalTime;

1.15. Customizing selectable options

Any entity or enum property will by default be rendered via an OptionsFormElementBuilder resulting in either a select box or list of checkboxes being rendered.

Set the type of options control

You can customize the type of options control to be generated by setting the viewElementType for a property.

entities.withType( WebPage.class )
    .createOrUpdateFormView( fvb -> fvb
        /**
         * Render the state as radio buttons instead of a select box.
         */
        .properties( props -> props
            .property( "state" )
            .viewElementType( ViewElementMode.CONTROL, BootstrapUiElements.RADIO )
        )
    );

If no viewElementType has been specified, a default type will be determined: a select box will be used in case of a single value, a checkbox list in case of multiple values.

Advanced select box configuration

A select control being generated will be a bootstrap-select with default configuration. You can customize the select box configuration by manually setting a SelectFormElementConfiguration attributes.

See the BootstrapUiModule documentation for all configurable properties.

If no viewElementType has been specified, but a SelectFormElementConfiguration attribute is present, the resulting control will be a select box.

Configuring options that can be selected

You can manipulate the options that can be selected in several ways by setting either EntityConfiguration or EntityPropertyDescriptor attributes.

If your property is another entity type, by default the selectable options will be all entities of that type. If you want to change this for all properties of that type, you can set either an OptionGenerator.class, OptionIterableBuilder.class or EntityAttributes.OPTIONS_ENTITY_QUERY attribute on the target EntityConfiguration. If you want to change it only for a single property, you can configure the same attributes on the EntityPropertyDescriptor of that property.

entities.withType( WebCmsArticle.class )
    .createOrUpdateFormView( fvb -> fvb
        /**
         * Only allow published sections to be selectable,
         * by specifying an EQL statement to fetch them.
         */
        .properties( props -> props
            .property( "section" )
            .attribute( EntityAttributes.OPTIONS_ENTITY_QUERY, "published = TRUE ORDER BY name ASC" )
        )
    );

When dealing with an enum type, you can also configure the EntityAttributes.OPTIONS_ALLOWED_VALUES with the `EnumSet`of selectable options.

/**
 * Limit the selectable enum HTTP status.
 */
entities.withType( WebCmsUrl.class )
    .properties(
        props -> props
            .property( "httpStatus" )
            .attribute(
                EntityAttributes.OPTIONS_ALLOWED_VALUES,
                EnumSet.of( HttpStatus.OK, HttpStatus.NOT_FOUND )
            )
    );
Depending on the attribute you will change more of the default behaviour and will have to provide custom implementations. Use the most appropriate attribute for your use case. See the appendix for more information on the different attributes. :!sectnums:

Configuring entity types

1.1. Using builders

Entities are usually automatically added to the EntityRegistry through the use of one or more EntityRegistrar beans. The registrars will apply a default configuration, usually consisting of all properties, associations and views.

Customizing the EntityRegistry is done by implementing one or more EntityConfigurer beans in your modules. These receive an EntitiesConfigurationBuilder that effectively allows you to customize all registered EntityConfiguration instances. Multiple EntityConfigurer beans can modify the same EntityConfiguration, the order in which they are applied will determine the last value if they modify the same properties.

Investigate the javadoc of the EntitiesConfigurationBuilder and child builders to discover all possible configuration options.

@AcrossDepends(required = "EntityModule")
@Configuration
public class UserEntitiesConfiguration implements EntityConfigurer
{
        @Override
        public void configure( EntitiesConfigurationBuilder configuration ) {
                // By default permissions cannot be managed through the user interface
                configuration.withType( Permission.class ).hide();
        }
}

1.2. Configuring properties

Properties for an entity can be configured through the builders as well. New properties can be added or the default properties can modified. How properties are configured determines how they will be rendered on the generated forms.

hidden

A hidden property will by default not be returned when requesting all properties from an EntityPropertyRegistry. You can still get this property directly however, the hidden state means a property will not advertise itself, you must know of its existence.

readable

Any readable property can be rendered in all views. This state means that a form control can always be generated, even though it might very well be readonly if the property is not writable.

writable

A writable property can be rendered in form views. In case a property is writable but not readable, the property can only be included in forms but not in other views.

The hidden state has no correlation with a hidden form control. Setting a property to be rendered as a hidden form control can only be done through configuring the right ViewElement information for that property.

1.2.1. Configuring a label

An entity with a corresponding EntityConfiguration always has a label, this is a textual representation of the entity in for example lists. This could be the name or the * title* property for example. By default the label corresponds to a custom generated property #label that defaults to calling toString() on the entity.

You can configure the label using the label() method on a PropertyDescriptorBuilder. This is equivalent to calling property("#label"). If you want to use another property as the base for label generation, you can configure this on the EntityConfigurationBuilder by calling label("propertyName"). This will copy all settings from the source property to the #label property, but keep in mind it still is a separate property that can be customized.

@Override
public void configure( EntitiesConfigurationBuilder entities ) {
    // Configure the username to be used as label for a User entity
    entities.withType( User.class ).label( "username" );

    // Configure the group name to be used as base label, but modify the value fetcher so
    // the label is prefixed with Group
    entities.withType( Group.class )
            .properties( props -> props.label( "name" ).spelValueFetcher( "'Group: ' + name" ) );
}

If you do not wish to use the #label property at all as default entity label, you can customize the Printer used for label generation by modifying the EntityModel.

As #label is a generated property, sorting is not enabled by default. If you configure the label using an existing property, the sortable attribute will be copied as well and sorting on label will be possible.

1.3. Creating an EntityConfiguration manually

1.3.1. Attributes to configure

Some attributes are mandatory, others are optional but will often impact how much functionality is available out of the box. You can configure any attribute you like, see the section on automatic registration for a list of common attributes provided by other registrars.

1.3.2. EntityQueryExecutor

In order for generated views to work automatically, an EntityConfiguration should have an EntityQueryExecutor attribute. The EntityQueryExecutor is a generic interface that supports the simple EntityQuery abstraction for fetching entities from the backing repository. Default implementations exist for JpaSpecificationExecutor and QueryDslPredicateExecutor.

1.3.3. Registering an ENUM as entity

EntityModule supports registration of enums as EntityConfiguration. When creating an EntityConfiguration for an enum, a basic EntityModule will get built and all enum properties will be configurable.

Registering enums as entity is mainly useful for configuration of display properties in related entity views.

Example registering an enum as entity
// Enum class
public enum Country
{
    BE( "Belgium" ),
    UK( "United Kingdom" ),
    NL( "Netherlands" );

    private String name;

    Country( String name ) {
        this.name = name;
    }

    public String getName() {
        return name;
    }
}

// Create the EntityConfiguration
@Override
public void configure( EntitiesConfigurationBuilder entities ) {
    entities.create().entityType( Country.class, true ).label( "name" );
}

1.4. Automatic registration of entity types

1.4.1. Spring Data repositories

JPA repositories

DOCUMENTATION TODO:

  • Embedded ID

  • register conversion service

  • add json serializer

Automatic attribute registration

EntityQuery infrastructure

EntityModule provides an abstraction layer for querying entities. This abstraction is built around the concept of an EntityQuery.

This chapter gives some insight in the setup and how to customize if so wanted.

1. EntityQueryExecutor

The EntityQueryExecutor is responsible for executing an EntityQuery and returning the entities requested. An EntityConfiguration can have a single EntityQueryExecutor.class attribute holding the executor instance.

Default implementations exist for JpaSpecificationExecutor and QueryDslPredicateExecutor. This means that any entity configurations having a repository of this type will get an EntityQueryExecutor created automatically.

Since the EntityQueryExecutor is backed by a specific repository implementation, supported functionality also depends on the actual backing repository.

The presence of the EntityQueryExecutor is a requirement for the default entity views.

1.1. AssociatedEntityQueryExecutor

Like EntityQueryExecutor that is registered on the EntityConfiguration, every EntityAssociation can have an AssociatedEntityQueryExecutor registered. The AssociatedEntityQueryExecutor allows executing queries in the context of a single parent object.

Like the EntityQueryExecutor, the AssociatedEntityQueryExecutor is usually added automatically.

The presence of the AssociatedEntityQueryExecutor is a requirement for the default association entity views.

2. EntityQuery Language (EQL)

Apart from building an EntityQuery in code, one can be specified using the EntityQuery Language (EQL). EQL provides an SQL-like syntax for building queries. Some examples could be:

  • name = 'john'

  • order by birthday asc

  • name = 'john' order by birthday desc, registrationDate asc

  • name = 'john' and birthday >= '1980-01-01'

  • (name in ('john', 'jane') and birthday = today()) or birthday is EMPTY order by name asc

  • children contains 'john' or children is EMPTY

A single clause of a query consists of a field, followed by an operator, followed by one or more values or functions. Multiple values for one field are grouped together inside parentheses. Clauses can be combined using either and or or. Operator precedence can be enforced by using parentheses around a clause.

A field of a query clause usually corresponds with a single property you want to query.

A SQL-like order by clause specifying one or more properties is also supported. An EQL statement with only an ordering clause will select all items in that order.

Special characters in string literals should be escaped using the \\ (backslash) character. In case of like conditions, they should be double-escaped.

2.1. Supported operators

2.1.1. Equality operators

Usable on most property types.

Operator Description

=

equals

!=

not equals

2.1.2. IN/NOT IN

Usable on most property types. These are the only operators that take a group of values as argument.

Operator Description

IN

equal to any of the argument values

NOT IN

not equal to any of the argument values

2.1.3. Comparing operators

Usable on numeric and date property types.

> greater than

>=

greater than or equal to

<

less than

less than or equal to

2.1.4. LIKE operators

Usable on string property types. The argument specifies the pattern that property value should match. The pattern can contain simple wildcard specified using %.

Operator Description

LIKE

should match the pattern specified as argument

NOT LIKE

should not match the pattern specified as argument

ILIKE

case insensitive match of the pattern specified as argument

NOT ILIKE

should not match the pattern specified as argument (case insensitive)

NOTE Special characters in LIKE statements should be double-escaped using the \ (backslash) character. This includes the literal % (percentage) character.

Case insensitive property matching When using ILIKE conditions, a case insensitive lookup will be performed in the datastore. If you want to force a property to always have a case insensitive lookup, you can do so by configuring a EntityQueryConditionTranslator attribute on that property.

configuration
 .withType( Group.class )
 .properties(
    props -> props.property( "name" ).attribute( EntityQueryConditionTranslator.class, EntityQueryConditionTranslator.ignoreCase() )
 )

This will convert any equality or like operator to the equivalent case insensitive lookup.

WARNING Whenever possible, it is probably best to use collation settings of your datastore to ensure case insensitive querying of properties. Configuring it on a datastore level will almost certainly give you much better performance. If collation is not possible, investigate the option of using function indices on the relevant columns.

2.1.5. CONTAINS/NOT CONTAINS

Usable on collection or text property types. In case of text the contains statement is translated to a like statement with wildcards before and after.

Operator Description

CONTAINS

argument should be present in the collection or string should be present in the text

NOT CONTAINS

argument should not be present in the collection or string should not be present in the text

2.1.6. IS NULL/IS NOT NULL

Usable on single value properties only. These operators to not take any additional arguments.

Operator Description

IS NULL

property should not be set (null)

IS NOT NULL

property should be set (not null)

2.1.7. IS EMPTY/IS NOT EMPTY

Preferred for collection type properties, altough usually will work as an alternative for IS NULL/IS NOT NULL on single value properties. These operators to not take any additional arguments.

Operator Description

IS EMPTY

property should not have any members (in case of collection) or should not be set (if single value property)

IS NOT EMPTY

property should have at least one member (in case of collection) or should be set (if single value property)

2.2. Default EQL functions

Security related functions

Function Description

currentUser()

returns the name of the current authenticated principal

Date and time functions

Function Description

now()

returns current timestamp

today()

returns date of today

2.3. EntityQueryParser

The EntityQueryParser is responsible for converting an EQL statement into a valid EntityQuery. Any entity configuration with an EntityQueryExecutor registered will have an EntityQueryParser created automatically.

The parser will validate the EQL statement and convert it to a strongly typed EntityQuery. The default EntityQueryParser uses the entity related EntityPropertyRegistry to validate the query clauses.

3. EntityQuery filtering on list view

Any entity configuration having an EntityQueryParser and EntityQueryExecutor can enable an EntityQuery based filter on its list views. This can be done through the entityQueryFilter() method on the `EntityListViewFactoryBuilder.

An EntityQuery filter supports 2 different modes:

  • basic mode allows you to select the property values to filter on using form controls

  • advanced mode give you a textbox for entering any EQL statement to use as filter

By default only advanced mode is active. Basic mode is activated if you configure the properties that should be shown in the filter. You do so by modifying the EntityQueryFilterConfiguration that is being used.

Activating the default (advanced mode) EntityQuery filter

entities.withType( Group.class )
        .listView( lvb -> lvb.entityQueryFilter( true ) );

Activating basic mode + advanced mode EntityQuery filter

entities.withType( Group.class )
        .listView( lvb -> lvb.entityQueryFilter( true )
                             .showProperties( "name", "active" ) );

By default both basic and advanced mode are available, and the UI allows switching between them. All options are configurable on the EntityQueryFilterConfiguration.

3.1. Basic mode

Basic mode enables the use of controls to filter by and will parse the content of the property controls to a valid EQL statement which will then be submitted.

By default the following controls will be created - depending on property type: * textbox controls * select controls

For select controls, you can specify if multiple values can be selected on the EntityQueryFilterConfiguration.

Text controls will by default use the EntityQueryOps.CONTAINS operand, multi value controls will use the EntityQueryOps.IN operand and otherwise the EntityQueryOps.EQ operand will be used if none was specified on the property directly.

For easier switching between basic and advanced mode, it is also possible to define an EntityAttribute.OPTIONS_ENHANCER on the property, which allows to define the value to be used for the object (e.g. instead of the id of a group, i’d like to see the name of the group whilst filtering). An EntityQueryValueEnhancer however merely defines a label to use. For the statement to be parsed successfully you will also need to register a corresponding Converter on the ConversionService.

The values of the filter controls will be set using the EntityQueryRequest and EntityQueryRequestValueFetcher.

3.2. Advanced mode

Advanced mode enables the use of EQL to filter the current view using a simple textbox. If both advanced and basic mode are allowed, and the EQL statement that was last executed is not convertible to basic mode, basic mode will be disabled.

Example EntityQuery filter configuration

entities.withType( WebCmsArticle.class )
        .listview(
            lvb -> lvb.entityQueryFilter(
              eqf -> eqf.showProperties("title", "articleType") // create a control for title and articleType
                        .multiValue("articleType") // It should be possible to filter on multiple article types
            )
        );

4. EQL predicate on list view

List views also support a base predicate to be configured as an EQL statement. This base predicate will always be applied to the query being executed if it uses the DefaultEntityFetchingViewProcessor or the EntityQueryFilterProcessor.

Ensure deleted (flag) items are never shown

entities.withType( Group.class )
        .listView( lvb -> lvb.entityQueryPredicate( "deleted = false" )        );

Like EQL based filtering, this requires the entity configuration to have a valid EntityQueryExecutor infrastructure.

5. Extending EQL

The EntityQuery infrastructure provides some hooks you can use to extend the EQL support with application specific methods.

5.1. Custom value conversion

When converting an EQL query all value arguments are first converted to an EQType representation before being converted into their respective Java type. Actual type conversion is then done via the Spring ConversionService. To create a custom conversion you can simply register a Converter that converts from the relevant EQType to the property type.

The following table shows how EQL arguments will be converted to their respective EQType:

Argument value EQType

name

EQValue: name

'name'

EQString: name

(name, 'name')

EQGroup

- EQValue: name

- EQString: name

users(name, 'name')

EQFunction: users

[arguments]

- EQValue: name

- EQString: name

By default EntityModule registers id-based lookups for all its registered entities. So supposing you have an entity User with id 1 and you want to query on a property creator of type User, the following query would work: creator = 1.

When building the EntityQuery the value 1 would be used as the id to find the User instance, and the latter would be used as the argument for the final query. If we want to replace the custom behavior and allow the user to be specified by username instead, we could easily register a custom converter.

public class EQValueToUserConverter implements Converter<EQValue, User>
{
    ...

    @Override
    public User convert( EQValue source ) {
        return userRepository.findByUsername( source.getValue() );
    }
}

...

converterRegistry.addConverter( new EQValueToUserConverter(...) );

This would allow us to execute the queries like creator = john or creator in (john, jane). Any type-specific converter will take precedence over the defaults.

NOTE The example above would only work if the username can never contain any whitespace. If it can, then we would have to specify it as a String instead and write a converter for EQString instead of EQValue.

5.2. Adding custom functions

An EQL function is represented by a unique name and can optionally take a number of arguments for its execution. Adding custom functions is as easy as simply defining a @Component that implements the EntityQueryFunctionHandler interface. All components of this type will be detected and checked when executing an EQL query.

The handler will be called with the required contextual data for the return type requested. If you want to use a function to compare a property that has a Date type, your function should return a Date instance as well.

A single handler can support multiple functions and requested return types.

Simple EntityQuery function that always returns the String hello

/**
 * Simple EntityQuery function that always returns the String 'hello'.
 * Example eql: name = hello() or name in (hello(), 'goodbye')
 */
@Component
public class HelloFunction implements EntityQueryFunctionHandler
{
        @Override
        public boolean accepts( String functionName, TypeDescriptor desiredType ) {
                return "hello".equals( functionName );
        }

        @Override
        public Object apply( String functionName,
                             EQType[] arguments,
                             TypeDescriptor desiredType,
                             EQTypeConverter argumentConverter ) {
                return "hello";
        }
}

5.3. Custom EQL translation

You can register an EntityQueryConditionTranslator attribute on any entity property. If a translator instance is present, it will be called during the parsing phase of an EQL statement into an EntityQuery.

Ensuring a field search is always case insensitive

configuration
 .withType( Group.class )
 .properties(
    props -> props.property( "name" ).attribute( EntityQueryConditionTranslator.class, EntityQueryConditionTranslator.ignoreCase() )
 )

Define a search text property that actually searches on other fields

configuration.withType( Note.class )
             .properties( props -> props.property( "text" )
                                        .valueFetcher( entity -> "" )
                                        .propertyType( TypeDescriptor.valueOf( String.class ) )
                                        .viewElementType( ViewElementMode.CONTROL, BootstrapUiElements.TEXTAREA )
                                        .attribute( EntityQueryConditionTranslator.class,
                                                    EntityQueryConditionTranslator.expandingOr( "name", "content" ) )
                                        .hidden( true )
                             )

Appendix

1. Attributes overview

EntityConfiguration attributes

The following table lists commonly present attributes on an EntityConfiguration.

Key Value

Repository.class

In case of an entity registered through a Spring Data repository.

RepositoryFactoryInformation.class

In case of an entity registered through a Spring Data repository.

PersistentEntity.class

In case of an entity registered through a Spring Data repository that exposed PersistentEntity information.

EntityQueryExecutor.class

Holds the EntityQueryExecutor that can be used for custom EntityQuery execution and will be used by default for fetching entities. Available if the Repository was supported by one of the default EntityQueryExecutor implementations.

EntityQueryParser.class

Holds the EntityQueryParser that should be used for parsing EQL statements into a valid EntityQuery. Available if the EntityQueryExecutor.class attribute is present.

EntityAttributes.TRANSACTION_MANAGER_NAME

Optionally holds the name of the PlatformTransactionManager that the repository for this entity uses. EntityModule attempts to detect the transaction manager automatically for every Spring Data repository. When set, this will enable transaction management for the default create, update and delete views.

OptionGenerator.class

When set on an EntityConfiguration, this will be the default generator used to create the set of options that can be selected for a property that points to the entity configuration. If all you want to configure is the list of possible options, set the OptionIterableBuilder.class attribute instead.

OptionIterableBuilder.class

When set on an EntityConfiguration, this will be the default builder used to create the set of options that can be selected for a property that points to the entity configuration.

EntityAttributes.OPTIONS_ENTITY_QUERY

When set on an EntityConfiguration, contains the EQL statement or EntityQuery that should be used to fetch the selectable options for a property that points to the entity configuration. Will only be used if there is no OptionGenerator.class or OptionIterableBuilder.class attribute set.

EntityPropertyDescriptor attributes

The following table lists commonly present attributes on an EntityPropertyDescriptor.

Key Value

PersistentProperty.class

In case of a property of a PersistentEntity registered through a Spring Data repository.

Sort.Order.class

Contains the default Sort.Order if sorting is enabled on this property. By default strings have an order that ignores case.

EntityAttributes.CONTROL_NAME

Optional: required to be a String value. When present this value will be used as the form control name instead of the descriptor name.

EntityAttributes.NATIVE_PROPERTY_DESCRIPTOR

When present holds the Java beans property descriptor that was used to create the EntityPropertyDescriptor. The presence of this attribute indicates that the property is not artificial but corresponds to an actual Java class property.

OptionGenerator.class

When set on an EntityPropertyDescriptor, this will be the generator used to create the set of options that can be selected for that property. If all you want to configure is the list of possible options, set the OptionIterableBuilder.class attribute instead.

OptionIterableBuilder.class

When set on an EntityPropertyDescriptor, this will be the builder used to create the set of options that can be selected for that property.

EntityAttributes.OPTIONS_ENTITY_QUERY

When set on an EntityPropertyDescriptor, contains the EQL statement or EntityQuery that should be used to fetch the selectable options for that property. Will only be used if there is no OptionGenerator.class or OptionIterableBuilder.class attribute set.

EntityAttributes.OPTIONS_ALLOWED_VALUES

Only applicable if the property is of an enum type. When set, the attribute holds the EnumSet of selectable values. If you want to customize selection of a non-enum type, see the other option related attributes. Will only be used if there is no OptionGenerator.class or OptionIterableBuilder.class attribute set.

SelectFormElementConfiguration.class

Can hold the configuration instance that should be used when generating a select control for this propery. Unless a specific ViewElement type has been specified, this will force the control type generated to be a select as well.

2. Message codes

Labels are resolved using a message code hierarchy. Simply define one or more message sources specifying the properties you want. Unless custom EntityMessageCodeResolver instances are being used, message codes are generated as follows:

Message code Description

EntityModule.adminMenu

Title of the root menu group for entity management.

ModuleName.adminMenu

Title of the menu group for the entities of that module.

enums.EnumName.EnumValue

Message code for a single enum value label.

Example: enums.Numbers.ONE

EntityPrefix.name.singular

Label for an entity in singular form, for use outside or at the beginning of a sentence.

Example: UserModule.entities.user.name.singular

EntityPrefix.name.plural

Label for an entity in plural form, for use outside or at the beginning of a sentence.

Example: UserModule.entities.user.name.plural

EntityPrefix.name.singular.inline

Label for an entity in singular form, for use within a sentence. If not explicitly specified, the label is generated based by lower-casing the non-inline version.

Example: UserModule.entities.user.name.singular.inline_

EntityPrefix.name.plural.inline

Label for an entity in plural form, for use within a sentence. If not explicitly specified, the label is generated based by lower-casing the non-inline version.

Example: UserModule.entities.user.name.plural.inline

EntityPrefix.properties.propertyName

Label for a single entity property.

Example: UserModule.entities.user.properties.username

EntityPrefix.properties.propertyName[description]

Description text for a property. If not empty this will be rendered in a help block on forms.

Example: UserModule.entities.user.properties.username[description]

EntityPrefix.properties.propertyName[placeholder]

Placeholder text for a property. Will be used for certain controle like textbox.

Example: UserModule.entities.user.properties.username[placeholder]

EntityPrefix.validation.validatorKey

Description text for a validation error message. Optionally can be suffixed with the specific property name.

Example: UserModule.entities.user.validation.NotBlank, UserModule.entities.user.validation.alreadyExists.username

You would then use errors.rejectValue( "username", "alreadyExists" ); after creating the above message code in your message sources.

EntityPrefix.adminMenu

Title of the admin menu item for this entity. Defaults to the singular name of the entity.

EntityPrefix.adminMenu.general

Name of the General tab. Usually the first tab that is also opened when creating a new entity.

EntityPrefix.adminMenu.associationName

Name of the tab for that association.

Example: UserModule.entities.group.adminMenu.user.groups

EntityPrefix.actions.actionName

Name of the actions, usually the buttons or links on a page. Often you just want to replace these on a global level.

Example: EntityModule.entities.actions.save, UserModule.entities.group.actions.cancel

EntityPrefix.pageTitle.pageName

Title of the page. Supports message code parameters.

Example: UserModule.entities.user.pageTitle.update=Updating {1}: {2}

EntityPrefix.pageTitle.pageName.subText

Additional text that should be added as sub text (small) to the page header. Supports message code parameters.

EntityPrefix.feedback.feedbackType

Feedback message shown for the given feedback type.

Example: UserModule.entities.user.feedback.validationErrors

EntityPrefix.sortableTable.*

Sortable table results and pager text keys.

Example: UserModule.entities.user.sortableTable.resultsFound

EntityPrefix.delete.*

Delete view specific messages.

Example: UserModule.entities.user.delete.confirmation

EntityPrefix.entityQueryFilter.linkToAdvancedMode

The label for the button to navigate from basic to advanced mode.

EntityPrefix.entityQueryFilter.linkToBasicMode

The label for the button to navigate from advanced to basic mode.

EntityPrefix.entityQueryFilter.eqlPlaceholder

The placeholder for the eql statement filter.

EntityPrefix.entityQueryFilter.searchButton

The label for the entity query filter on the search button.

EntityPrefix.entityQueryFilter.eqlDescription

An additional description for the eql statement filter.

EntityPrefix.entityQueryFilter.convertibleToBasicMode[helpText]

The descriptive text that should be shown when hovering over the "basic" mode button when the query is not convertible to basic mode.

EntityPrefix.properties.propertyName[filterNotSelected]

Label for the empty option in a filter control.

EntityPrefix.properties.propertyName.value[empty]

Label for the empty option of an entity property.

EntityPrefix.properties.propertyName.value[true]

Label that should be used instead of true for a boolean property.

EntityPrefix.properties.propertyName.value[false]

Label that should be used instead of false for a boolean property.

EntityPrefix.properties.propertyName.value[notSet]

Label that should be used for the null option in a filter control.

Entity codes are camel cased, eg. CarBrand would become carBrand

EntityPrefix

Every code requested results in several codes being tried with a number of prefixes: The following prefixes are tried in oder:

  1. (If association view) ModuleName.entities.sourceEntityName.associations[associationName]

  2. ModuleName.entities.entityName

  3. EntityModule.entities.entityName

  4. EntityModule.entities

When rendering a view, the default prefix will be appended with a view type prefix as well. Usually of the form views[viewType].

Example lookup of property "name" on the default list view for entity "user":

  1. MyModule.entities.user.views[listView].properties.name

  2. MyModule.entities.user.properties.name

  3. MyModule.entities.views[listView].properties.name

  4. MyModule.entities.properties.name

  5. EntityModule.entities.views[listView].properties.name

  6. EntityModule.entities.properties.name

TIP: To get a better insight in the message codes generated, use the entity browser in the developer tools.

Message code parameters

Some message codes support parameters, if so, the following could be available:

  • {0}: entity name

  • {1}: entity name inline

  • {2}: label of the entity being modified (if known)

Debugging message code lookups

You can trace the message codes being resolved by setting the logger named com.foreach.across.modules.entity.support.EntityMessageCodeResolver to TRACE level.

Default message codes

The following is a copy of EntityModule.properties which contains the default message codes for EntityModule.

EntityModule.adminMenu=Entity management

# Default actions
EntityModule.entities.actions.create=Create a new {1}
EntityModule.entities.actions.view=View {1} details
EntityModule.entities.actions.update=Modify {1}
EntityModule.entities.actions.delete=Delete {1}
EntityModule.entities.actions.save=Save
EntityModule.entities.actions.cancel=Cancel

EntityModule.entities.menu.delete=Delete
EntityModule.entities.menu.advanced=Advanced options

EntityModule.entities.buttons.delete=Delete

EntityModule.entities.feedback.entityCreated=New {1} has been created.
EntityModule.entities.feedback.entityUpdated={0} has been updated.
EntityModule.entities.feedback.entityDeleted={0} has been deleted.
EntityModule.entities.feedback.entityDeleteFailed=Exception deleting {1}: {3}.
EntityModule.entities.feedback.validationErrors=Unable to save, please check the form for one or more errors.
EntityModule.entities.feedback.entitySaveFailed=Something went wrong when saving the {1}.  <br />Error code: <strong>{4}</strong> ({3}).

EntityModule.entities.pageTitle.create=Create a new {1}
EntityModule.entities.pageTitle.update=Modify {1}: {2}
EntityModule.entities.pageTitle.view=View {1} details: {2}
EntityModule.entities.pageTitle.delete=Delete {1}: {2}

EntityModule.entities.sortableTable.resultsFound={0,choice, 0#No {2}| 1#1 {1}| 1<{0} {2}} found.
EntityModule.entities.sortableTable.pager=Showing page {0,number,#} of {1,number,#}
EntityModule.entities.sortableTable.pager.page=page
EntityModule.entities.sortableTable.pager.ofPages=of
EntityModule.entities.sortableTable.pager.nextPage=next page
EntityModule.entities.sortableTable.pager.previousPage=previous page

EntityModule.entities.delete.confirmation=Are you sure you want to delete this {1} and all its associations?
EntityModule.entities.delete.deleteDisabled=Not possible to delete this {1}.
EntityModule.entities.delete.associations=The following items are associated with this {1}:
EntityModule.entities.delete.associatedResults={2} {1}


# Default validation messages

EntityModule.entities.validation.Size=Length should be between {2} and {1} characters.
EntityModule.entities.validation.Length=Length should be between {2} and {1} characters.
EntityModule.entities.validation.NotBlank=A value is required.
EntityModule.entities.validation.NotNull=A value is required.
EntityModule.entities.validation.NotEmpty=A value is required.
EntityModule.entities.validation.Email=Email address is not well-formed.
EntityModule.entities.validation.Min=Value should be greater than or equal to {1}.
EntityModule.entities.validation.Max=Value should be less than or equal to {1}.

EntityModule.entities.validation.alreadyExists=Another entity already has this value.

# Default control messages
BootstrapUiModule.SelectFormElementConfiguration.noneSelectedText=

3. Default EntityViewProcessors

This chapter lists the general purpose EntityViewProcessor classes that are provided by EntityModule. The default entity views are all built on these processors.

You can use these manually to assemble a DefaultEntityViewFactory, though some will be configured automatically if you use one of the builders.

The class javadoc gives more detailed information. For a full list of all processors available, see the package summary.

Name Purpose

ActionAllowedAuthorizationViewProcessor

Verifies an the entity configuration or association being requested is not hidden, and a configured AllowableAction is present.

DefaultEntityFetchingViewProcessor

Uses the default repository or query fetcher to fetch all items for the current entity view context.

DefaultValidationViewProcessor

Registers the default EntityViewCommandValidator and validates the command object if state changing web request (POST, PUT, DELETE, PATCH) is performed.

DelegatingEntityFetchingViewProcessor

Fetches items using a configured Function or BiFunction.

EntityPropertyRegistryViewProcessor

Registers a custom EntityPropertyRegistry on the view context.

EntityQueryFilterProcessor

Adds an EntityQuery language based filter to a list view. Adds both the form with textbox and fetches the items based on the form values.

GlobalPageFeedbackViewProcessor

Renders global feedback on a PageContentStructure (admin page). Global feedback is usually added using EntityViewPageHelper.

ListFormViewProcessor

Adds a default form at the top of a list view. Optionally add a create button.

ListPageStructureViewProcessor

Generates the page structure for an entity list view. Add a page title and publishes the EntityPageStructureRenderedEvent.

MessagePrefixingViewProcessor

Configures custom prefixes that should be used for message code resolving.

PageableExtensionViewProcessor

Creates a Pageable from request parameters and binds it to an EntityViewCommand extension.

PropertyRenderingViewProcessor

Renders a list of properties: allows the properties to be configured as well as the ViewElementMode for rendering.

SingleEntityFormViewProcessor

Creates a form-based layout for an entity view. Supports configuring the form grid (defaults to 2 columns), adding default actions (save/cancel) and adding global binding error messages.

SingleEntityPageStructureViewProcessor

Generates the page structure for a single entity. Adds a page title, builds the entity specific menu (renders it as tabs) and publishes the EntityPageStructureRenderedEvent.

SortableTableRenderingViewProcessor

Generates a sortable table for a list of entities. Allows several configuration options like properties to render, sorting options etc.

TemplateViewProcessor

Configures the name of the template that should be rendered as the result of the controller.

How-tos

1. Adding a custom property to an entity

To add a custom property to an entity, all we need to do is add the property on the EntityPropertyRegistryBuilder of the entity.

In the following example we will add a property named customProperty of the type String, which always returns myCustomString.

@Override
public void configure( EntitiesConfigurationBuilder configuration ) {
       configuration.withType(MyEntity.class)
                .properties(props -> props.property("customProperty")
                           .propertyType(TypeDescriptor.valueOf(String.class))
                           .valueFetcher(entity -> "myCustomString")

                     )
}

Configuring the custom property to expand it’s search to other properties

To expand the search to other properties, we need to modify the query defined by the custom property. To do this we can add an a custom EntityQueryConditionTranslator under the EntityQueryConditionTranslator.class attribute.

In the following example, we will perform the query of the customProperty on the name and content properties.

@Override
public void configure( EntitiesConfigurationBuilder configuration ) {
       configuration.withType(MyEntity.class)
                .properties(props -> props.property("customProperty")
                           .propertyType(TypeDescriptor.valueOf(String.class))
                           .valueFetcher((entity) -> "myCustomString")
                           .attribute( EntityQueryConditionTranslator.class,
                                                            condition -> {
                                                                EntityQuery entityQuery = new EntityQuery();
                                                                entityQuery.setOperand( EntityQueryOps.OR );
                                                                entityQuery.add(
                                                                        new EntityQueryCondition( "name",
                                                                                                  condition.getOperand(),
                                                                                                  condition.getArguments() ) );
                                                                entityQuery.add(
                                                                        new EntityQueryCondition( "content", condition.getOperand(),
                                                                                                  condition.getArguments() ) );
                                                                return entityQuery;
                                                            } )

                     )
}

2. Customizing the listview of an entity

Adding a custom action to each item

In the case where we want to add a custom action to each instance of our entity on a listview, we have to register a viewprocessor (usually an EntityViewProcessorAdapter) on the listview of its entityconfiguration. Then in either the createViewElementBuilders `or `render method, we can edit the configured columns of each instance on the listview using the ViewElementBuilderMap.

From the ViewElementBuilderMap we can retrieve the SortableTableBuilder, registered under SortableTableRenderingViewProcessor.TABLE_BUILDER, on which we can register additional row processors to edit the content of a row. Each row represents a single instance of the entity we are configuring.

To apply changes to the actions of an instance, we will need to find the actions cell in our row, which is registered under EntityListActionsProcessor.CELL_NAME.

In this example, we will add a search button to the actions, which redirects us to google, with the query parameter being the property `name `of the entity instance found on the row.

    @Override
    protected void createViewElementBuilders(EntityViewRequest entityViewRequest, EntityView entityView, ViewElementBuilderMap builderMap) {
        SortableTableBuilder sortableTableBuilder = builderMap.get(SortableTableRenderingViewProcessor.TABLE_BUILDER, SortableTableBuilder.class);
        sortableTableBuilder.valueRowProcessor((ctx, row) -> {
            ContainerViewElementUtils.find(row, EntityListActionsProcessor.CELL_NAME, TableViewElement.Cell.class).ifPresent(
                    actions -> {
                        MyEntity entity = EntityViewElementUtils.currentEntity(ctx, MyEntity.class);

                        String searchUrl= UriComponentsBuilder.fromUriString("https://www.google.com/")
                                .queryParam("q", entity.getName())
                                .toUriString();

                        actions.addFirstChild(
                                BootstrapUiBuilders.button().link()
                                        .iconOnly(new GlyphIcon(GlyphIcon.SEARCH))
                                        .url(searchUrl)
                                        .build(ctx)
                        );
                    }
            );

        });

    }