Copyright © 2014-2018
Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.
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.
Module website: https://across.foreach.be/modules/entitymodule
What’s new in this version?
3.0.1.RELEASE
-
the module dependencies for EntityModule have been optimized for re-use
-
as a result EntityModule no longer transitively pulls in BootstrapUiModule or AdminWebModule
-
when used without BootstrapUiModule, no default
ViewElement
rendering infrastructure will be available -
when used with BootstrapUiModule but without AdminWebModule, the default views for an entity will never get created and view support (
EntityViewFactory
) will be disabled
-
-
added support for
LocalDate
,LocalTime
andLocalDateTime
to be rendered usingDateTimeFormElement
-
it is now possible to configure default view element modes (eg. control or readonly rendering) on an
EntityConfiguration
-
these will be used in all cases where no specific configuration has been configured on property level
-
-
configuration & view builders support
AttributeRegistrar
for registering or removing attributes-
using
AttributeRegistrar
is useful if you want to use the owner of the attribute collection (eg. theEntityConfiguration
) -
common default registrars can be found in the
EntityAttributeRegistrars
utility class
-
-
entity views can now have a collection of configuration attributes
-
attributes can be used to influence or extend default behaviour, new attributes are available for permission checking and admin menu rendering
-
during view rendering attributes are accessible (and can be modified) using
EntityViewRequest.getConfigurationAttributes()
-
-
improvements to view configuration
-
EntityViewFactoryAttributes.ADMIN_MENU
attribute can be used to specify if a view should have an admin menu item added -
EntityViewFactoryAttributes.ACCESS_VALIDATOR
attribute can be used to determine how access to the view should be validated
-
-
added an
ExtensionViewProcessorAdapter
base class for easily creating a view for a custom extension class (see how-to) -
added
EntityViewCustomizers
utility class providing some helpers for customizingEntityViewFactoryBuilder
in a chainable fashion -
EntityModule no longer creates its own
Validator
instance, theregisterForMvc
related settings have been removed-
the validator used by EntityModule is the default MVC validator
-
-
it’s now possible to define a different message code prefix for module entities using properties
-
you can now force the required status of a control by setting the
EntityAttributes.REQUIRED_PROPERTY
attribute totrue
orfalse
on anEntityPropertyDescriptor
-
message codes for form groups and fieldsets have been extended, apart from
[description]
, there is now also built-in support for[help]
and[tooltip]
-
this constitutes a minor breaking change in that
[description]
content is now always rendered above the control of a form group. Previously this could be different depending on the type of control inside the form group. -
see the section configuring form controls text for a full explanation of the new message codes
-
-
the behaviour of when controls are prefixed with
entity.
has been changed-
when using
EntityViewCommand
all property controls of the base entity will should be prefixed withentity.
in order to map on theEntityViewCommand.entity
values -
previously this was done always when an
EntityViewCommand
was found on theViewElementBuilderContext
-
in the new version this is only done if there is also an attribute
EntityPropertyControlNamePostProcessor.PREFIX_CONTROL_NAMES
explicitly set totrue
on the builder context-
the latter is done automatically by the
PropertyRenderingViewProcessor
when building the initial controls
-
-
though not intentionally breaking, this change can have side effects with controls no longer being prefixed, developers are encouraged to test the custom forms they have
-
-
new components for linking to entity views have been introduced
-
the old
EntityLinkBuilder
interface and attributes are deprecated, but should still work as before -
see the chapter on linking to entity views for an overview of the new components
-
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 theisSorted()
method-
if the
OptionGenerator
has no explicit sorting parameter set, it now only sorts if the configuredOptionIterableBuilder
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 theand()
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
withRepositoryInvoker
from spring-data-commons. -
principal names on
Auditable
entities are now pretty printed using theSecurityPrincipalLabelResolverStrategy
from the SpringSecurityModule -
EntityModule now supports deleting of entities
-
the
EntityModel
of anEntityConfiguration
can now be customized using theEntityConfigurer
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 generating and customizing forms (views) for managing the registered entities. |
BootstrapUiModule |
optional |
Activates support for default Bootstrap based |
Module settings
EntityModule supports the following configuration properties:
# Customize the message code prefix that should be used for entity messages
# for all entities from that module
entityModule.message-codes[MODULE_NAME] = PREFIX
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
( |
list view |
Shows the list of entities. |
createView
( |
form view |
Renders and executes the form for creating a new entity. |
updateView
( |
form view |
Renders and executes the form for updating an entity. |
deleteView
( |
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
orEntityAssociation
)
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 theEntityViewCommand
) -
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.
Several adapter EntityViewProcessor
classes are available:
-
use
EntityViewProcessorAdapter
for most use cases, it provides several adapter methods that also allow you to hook into theViewElement
generation lifecycle -
use
SimpleEntityViewProcessorAdapter
if you do not need to hook into theViewElement
generation lifecycle -
use
ExtensionViewProcessorAdapter
if you want to create a custom view/form that represents a so-called extension object-
the view is part of the entity configuration but often does not manage properties of the actual entity type
-
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.
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 |
|
entityViewCommand |
|
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
( |
Base configuration for rendering a list of entities. |
form view |
updateView
( |
Base configuration for rendering a form for a single entity. |
generic view |
genericView
( |
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
.
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.2. Customizing a list view
TODO
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.
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" ) );
}
);
}
}
<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>
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.
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.
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.2. Configuring form controls text
A form view usually renders properties in either ViewElementMode.FORM_WRITE
or ViewElementMode.FORM_READ
, depending on the fact if a property can be edited or not.
By default, a property like this would be rendered as a form group (FormGroupElement
that is usually a combination of the label and the control for the property) or a fieldset (FieldsetFormElement
).
In FORM_READ
(readonly) mode, the default form renders only the label of a property.
You can customize the label value by setting the corresponding message code, for example: UserModule.entities.user.properties.username=Name of the user
.
In FORM_WRITE
mode several other message codes will be resolved as well, and if they return values, additional content will be shown on the form.
A description provides additional context for the property being shown. It is rendered above the control of a form group, or above the content of a fieldset.
UserModule.entities.user.properties.username[description]=The username must be unique.
Help text is rendered below the control of a form group, or below the content of a fieldset. It usually provides a (less important) hint for updating the value.
UserModule.entities.user.properties.username[help]=Try to pick something you will remember.
Tooltip text is added as a separate icon (question mark) that will only show the actual tooltip when you hover over it with the mouse cursor. Tooltips are often used as an alternative for help text. The difference is that help text is always visible, whereas to see the tooltip a used will need to take an extra action.
The tooltip icon is added to the label of a form group or to the legend of a fieldset.
UserModule.entities.user.properties.username[tooltip]=You will receive an errror when saving if your username is already taken.
By default all message codes allow HTML entities, so you can add additional links or markup to them.
In case of a form group you can also manually set the different text components from code. Values set from code will take precedence and will never be replaced by the values resolved from message codes. |
A more detailed explanation of how message codes are resolved and which codes are possible can be found in the appendix.
1.5. custom views
TODO: register menu item, register access validator === AdminWebModule JQuery plugins :chapter-number: 0 The default EntityModule web resources add some JQuery based javascript plugins.
1.5.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.
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() .
|
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.5.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.
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. |
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 |
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.6. Linking to entity views
1.6.1. Default views
If AdminWebModul is enabled and you have an auto-detected entity type (eg. from a Spring Data repository), the default views are usually enabled. Every view has a base URL path, see the chapter default views for more information on the view types.
View name | Path |
---|---|
|
|
|
|
|
|
|
|
|
|
Not all paths work in all situations. The path structure is fixed, but it is only active if the corresponding view is also registered. |
The default view path for an association depends on the type of association.
A LINKED
association will use the same path as the target entity type, usually with the exception of the listView
.
An EMBEDDED
association has a path structure nested below the source entity path.
View name | Path |
---|---|
|
|
|
|
|
|
|
|
|
|
You can easily register a custom view for an entity type using an EntityConfigurer
.
Rendering that view can be done by calling the base entity type or association url, and specifying the view
query string parameter.
The path segments past the entity type root do not actually matter, you can use any of the default view paths as a base. The base type of view being rendered (eg. list view, form view) depends solely on how you defined the view itself.
Suppose you created a custom update form view with name myCustomView
.
The following paths would render the exact same view:
-
@adminWeb:/entities/{entityName}/{id}?view=myCustomView
-
@adminWeb:/entities/{entityName}/{id}/update?view=myCustomView
1.6.2. Building view links
An EntityViewLinks
component is available to generate links to entity views from anywhere in your code.
This component will inspect the EntityRegistry
to find corresponding configurations and determine how links should be built.
@Autowired
EntityViewLinks links;
// link to a list view
links.linkTo( MyEntity.class ).toUriString();
// Get the separate link builder
EntityViewLinkBuilder linkBuilder = links.linkTo( MyEntity.class );
Apart from using the EntityViewLinks
component globally to build links, the relevant EntityViewLinkBuilder
for a particular entity type can often be accessed directly:
-
It is registered as an
EntityConfiguration
attribute of that entity type. You can retrieve it usingentityConfiguration.getAttribute( EntityViewLinkBuilder.class )
. -
When rendering an entity view, the builder is available as the
linkBuilder
property on the currentEntityViewContext
. -
The
EntityAdminMenuEvent
exposes the builder for the current entity type as alinkBuilder
property, for direct access when building menus.
String url;
EntityViewContext entityViewContext;
MyEntity entity;
// -- Retrieving the link builder for the entity type
EntityViewLinkBuilder linkBuilder = entityViewContext.getLinkBuilder();
// -- Linking to the list view
// url: /entities/myEntity
url = linkBuilder.listView().toUriString();
// -- Linking to the create view
// url: /entities/myEntity/create
url = linkBuilder.createView().toUriString();
// -- Linking to the update view
// url: /entities/myEntity/1/update
url = linkBuilder.forInstance( entity ).updateView().toUriString();
// -- Linking to a custom view
// url: /entities/myEntity/1?view=customViewName
url = linkBuilder.forInstance( entity )
.withViewName( "customViewName" )
.toUriString();
// -- Linking to an association list view
// url: /entities/myEntity/1/associations/associatedItems/
url = linkBuilder.forInstance( entity )
.association( AssociatedItem.class )
.toUriString();
// -- Linking to a single associated item update view
// url: /entities/myEntity/1/associations/associatedItems/2/update
url = linkBuilder.forInstance( entity )
.association( associatedItem )
.updateView()
.toUriString();
The EntityViewLinkBuilder
has a fluent API that allows customizing the URL before converting it to a String
.
It also has some short-hand methods for commonly used entity view related parameters.
Every method call results in a new instance being created, so you will not make inadvertent changes to an existing link builder.
// append a custom path segment
.slash( String path )
// append query parameter
.withQueryParam( String param, Object... values )
// set a from URL ('from' query parameter)
.withFromUrl( String url )
// set a partial fragment ('_partial' query parameter)
.withPartial( String fragment )
// set a custom view name ('view' query parameter)
.withViewName( String viewName )
// return the unprocessed URI (eg. '@adminWeb:/entities/myEntity')
.toString()
// return the processed URI (eg. '/admin/entities/myEntity')
.toUriString()
// create a new UriComponentsBuilder with the current settings
.toUriComponentsBuilder()
// return as URI
.toUri()
// return as UriComponents
.toUriComponents()
// return the original EntityViewLinks
.root()
1.6.3. Common URL parameters
The following is a list of query string parameters often used with entity views:
from
-
Can hold a URL that should be used as a target when the new operation completes. Most often this is the target of the cancel link on a form view. See also
EntityViewLinkBuilder#withFromUrl(String)
.When building association links, a default
from
value to navigate back to the original entity will usually be added. _partial
-
This can be the identifier of the only fragment of a page that should be rendered. Partial view rendering is part of the Across Web features. See also
EntityViewLinkBuilder#withPartial(String)
. view
-
Name of the specific custom view that should be rendered. See also
EntityViewLinkBuilder#withViewName(String)
.
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 |
---|---|
|
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. |
|
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 withGroup
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
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.
See also the section on configuring form controls text for common customizations.
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.
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
.
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.
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.
|
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.
TemporalType.TIME
and JPAA 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.
@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.
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.
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.
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, thehidden
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.
// 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
Automatic attribute registration
See the appendix for an overview of commonly registered attributes.
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 |
---|---|
|
equal to any of the argument values |
|
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 |
---|---|
|
should match the pattern specified as argument |
|
should not match the pattern specified as argument |
|
case insensitive match of the pattern specified as argument |
|
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 |
---|---|
|
argument should be present in the collection or string should be present in the text |
|
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 |
---|---|
|
property should not be set (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 |
---|---|
|
property should not have any members (in case of collection) or should not be set (if single value property) |
|
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 |
---|---|
|
returns the name of the current authenticated principal |
Date and time functions
Function | Description |
---|---|
|
returns current timestamp |
|
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 |
|
'name' |
|
(name, 'name') |
- - |
users(name, 'name') |
[arguments] - - |
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 ofEQValue
.
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
Some useful (utility) classes for managing attributes for different EntityModule objects:
-
EntityAttributes
contains a variation of common attribute constants and some static helper methods -
EntityAttributeRegistrars
contains helper registrar factory methods -
EntityViewFactoryAttributes
contains common attribute constants and helper methods, specific for entity views
EntityConfiguration attributes
The following table lists commonly present attributes on an EntityConfiguration
.
Key | Value |
---|---|
|
In case of an entity registered through a Spring Data repository. |
|
In case of an entity registered through a Spring Data repository. |
|
In case of an entity registered through a Spring Data repository. |
|
In case of an entity registered through a Spring Data repository that exposed |
|
Holds the |
|
Holds the |
|
Optionally holds the name of the |
|
Holds an |
|
When set on an |
|
When set on an |
|
When set on an |
EntityPropertyDescriptor attributes
The following table lists commonly present attributes on an EntityPropertyDescriptor
.
Key | Value |
---|---|
|
In case of a property of a |
|
Contains the default |
|
Optional: required to be a |
|
When present holds the Java beans property descriptor that was used to create the |
|
When set on an |
|
When set on an |
|
When set on an |
|
Only applicable if the property is of an enum type.
When set, the attribute holds the |
|
Can hold the configuration instance that should be used when generating a select control for this propery.
Unless a specific |
|
Should be a |
EntityViewFactory attributes
The following table lists commonly present attributes on an EntityViewFactory
.
Key | Value |
---|---|
|
The registry the view belongs to, either the |
|
If present, holds the |
|
In case of an entity registered through a Spring Data repository that exposed |
|
Name of the view under which it is registered in the |
|
Optionally contains a |
|
Optionally contains a |
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. Used by default for form groups and fieldsets in a writable form configuration. If not empty, description text will be added before the control in a form group, and above the content of a fieldset. Example: UserModule.entities.user.properties.username[description] |
EntityPrefix.properties.propertyName[help] |
Help text for a property. Used by default for form groups and fieldsets in a writable form configuration. If not empty, help text will be added after the control in a form group, and below the content of a fieldset. Example: UserModule.entities.user.properties.username[help] |
EntityPrefix.properties.propertyName[tooltip] |
Tooltip text for a property. Used by default for form groups and fieldsets in a writable form configuration. If not empty, the tooltip icon will be added in the label of a form group, and in the legend of a fieldset. Example: UserModule.entities.user.properties.username[tooltip] |
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.adminMenu.views[viewName] |
Name of the tab for the view with that name (if there is a menu item for that view). |
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 |
EntityPrefix.properties.propertyName.value[false] |
Label that should be used instead of |
EntityPrefix.properties.propertyName.value[notSet] |
Label that should be used for the |
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:
-
(If association view) ModuleName.entities.sourceEntityName.associations[associationName]
-
ModuleName.entities.entityName
-
EntityModule.entities.entityName
-
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":
-
MyModule.entities.user.views[listView].properties.name
-
MyModule.entities.user.properties.name
-
MyModule.entities.views[listView].properties.name
-
MyModule.entities.properties.name
-
EntityModule.entities.views[listView].properties.name
-
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)
Customizing message code prefixes
The default message code prefix is MODULE_NAME.entities
.
It’s possible to configure the entity message codes that should be used for a specific module through configuration properties:
entityModule:
message-codes:
MyModule: prefix to use
You can specify multiple prefixes if you want, just realize this will have a big impact on the number of message codes tried.
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 |
---|---|
Verifies an the entity configuration or association being requested is not hidden, and a configured |
|
Uses the default repository or query fetcher to fetch all items for the current entity view context. |
|
Registers the default |
|
Fetches items using a configured |
|
Registers a custom |
|
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. |
|
Renders global feedback on a |
|
Adds a default form at the top of a list view. Optionally add a create button. |
|
Generates the page structure for an entity list view.
Add a page title and publishes the |
|
Configures custom prefixes that should be used for message code resolving. |
|
Creates a |
|
Renders a list of properties: allows the properties to be configured as well as the |
|
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. |
|
Generates the page structure for a single entity.
Adds a page title, builds the entity specific menu (renders it as tabs) and publishes the |
|
Generates a sortable table for a list of entities. Allows several configuration options like properties to render, sorting options etc. |
|
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)
);
}
);
});
}
3. Creating an extension form
Creating an additional form view for an entity is a very common action.
Often you want to manage data that does not actually represent entity type properties.
You can attach these to an existing (or separate) form by registering an extension on the EntityViewCommand
.
Using extensions has the advantage that data binding and default (annotation based) validation will happen automatically.
If you add an extension to the default entity form (having the SaveEntityViewProcessor ) and the extension has errors upon validation, that will count as global validation and will block saving the entity.
This makes for a very easy way to extend a default properties form with custom behaviour.
|
In this how-to we’ll show you how to create a custom view with a form backed by a custom model object registered as an extension. The form will show up as a separate tab on the entities where it’s registered.
Create the extension class
The extension class is the model for our form and will be validated when the form is submitted.
@Data
static class MyExtension
{
@NotBlank
private String url;
@Min(1980)
@Max(2000)
private int creationYear;
}
Create an EntityViewProcessor for the extension
Render the extension on the form and manage the form data. Annotation based validation will be done on form POST.
/**
* Custom EntityViewProcessor for MyExtension form.
* Does not specify an extensionName() but uses the default one (class name),
* as all processing will be done inside this implementation and there will
* never be multiple instances of this processor for a single view.
*
* Builds a simple form manually.
* Validation is actually done transparantly using annotation validation.
* Form elements will show errors because the control names match the extension properties.
*/
static class MyExtensionViewProcessor extends ExtensionViewProcessorAdapter<MyExtension>
{
@Override
protected MyExtension createExtension( EntityViewRequest entityViewRequest,
EntityViewCommand command,
WebDataBinder dataBinder ) {
return new MyExtension();
}
@Override
protected void doPost( MyExtension extension,
BindingResult bindingResult,
EntityView entityView,
EntityViewRequest entityViewRequest ) {
if ( !bindingResult.hasErrors() ) {
// Put a dummy feedback message
entityViewRequest.getPageContentStructure()
.addToFeedback(
BootstrapUiBuilders.alert()
.success()
.dismissible()
.text( "Updated url with: " + extension.getUrl() )
.build()
);
}
}
@Override
protected void render( MyExtension extension,
EntityViewRequest entityViewRequest,
EntityView entityView,
ContainerViewElementBuilderSupport<?, ?> containerBuilder,
ViewElementBuilderMap builderMap,
ViewElementBuilderContext builderContext ) {
builderMap.get( SingleEntityFormViewProcessor.LEFT_COLUMN, ColumnViewElementBuilder.class )
.add(
formGroup()
.label( "URL" )
.control( textbox()
.controlName( controlPrefix() + ".url" )
.text( extension.url ) )
)
.add(
formGroup()
.label( "Creation year" )
.control( textbox()
.controlName( controlPrefix() + ".creationYear" )
.text( "" + extension.creationYear ) )
);
}
@Override
protected void postRender( MyExtension extension,
EntityViewRequest entityViewRequest,
EntityView entityView,
ContainerViewElement container,
ViewElementBuilderContext builderContext ) {
EntityViewContext entityViewContext = entityViewRequest.getEntityViewContext();
// Manually change the cancel button url
container.find( "btn-cancel", ButtonViewElement.class )
.ifPresent( button -> button.setUrl(
entityViewContext.getLinkBuilder().update( entityViewContext.getEntity() )
) );
}
}
Register the view with our processor
The view itself can be registered under any name on the entity configuration. The view name will be used in the message code resolving.
When registering the view, some of the EntityViewCustomizers
are used to specify an admin menu item (tab) should be rendered for this view.
// Use a configuration template for a simple extension form
// Configure the view to create a menu item under the advanced options
entities.withType( ... )
.formView(
"extension",
EntityViewCustomizers.basicSettings()
.adminMenu( "/advanced-options/extension" )
.andThen( EntityViewCustomizers.formSettings().forExtension( true ) )
.andThen( builder -> builder.viewProcessor( new MyExtensionViewProcessor() ) )
);
Translate the menu item title
Set the right message code for the specific view menu item.
# Default value for every entity with that view
EntityModule.entities.adminMenu.views[extension]=My extension
# Specific title for the menu item on myEntity page
MyModule.entities.myEntity.adminMenu.views[extension]=Extra Fields