Skip to content

Latest commit

 

History

History
5447 lines (4376 loc) · 221 KB

index.adoc

File metadata and controls

5447 lines (4376 loc) · 221 KB

Dynamo

Table of Contents

image

Web Application Accelerator Framework

Developed, maintained and sponsered by

image

Main committer: Bas Rutten

Copyright © 2014 - 2024 Open Circle Solutions BV.

Nothing from this document may be copied and/or made public by use of print, photocopy, microfilm or by any other means, without prior permission of Open Circle Solutions BV.

Introduction

The Dynamo Web Application Accelerator Framework is a software development framework developed by Open Circle Solutions that aims to increase productivity by using design principles such as convention over configuration, model-driven development and DRY (don’t repeat yourself).

At the core of Dynamo is the concept of the Entity Model. The Entity Model describes the attributes and behaviour of an entity (or domain object) in your application. This Entity Model can then be used as the basis for creating forms, tables, search screens etc.

The Entity Model of an entity is automatically constructed based on the properties of the attributes of the entity (using sensible defaults as described by the convention over configuration principle) and can further be modified by using annotations and message bundle entries. The main goal is to reduce the amount of (boilerplate) code required to perform common actions like creating search screens and edit forms.

Complementing the Entity Model is a set of user interface components (widgets) that can be used to quickly construct screens for common use cases, and several base classes for the Data Access and Service layers.

The Dynamo framework is built around a number of proven and highly productive set of technologies:

  • JPA2 for ORM

  • QueryDSL for type-safe query generation

  • Spring Core & Spring Boot as the application framework

  • Angular & PrimeNG as the user interface framework

  • Apache Camel for integration

The Entity Model

Basics

To create the entity model, you need access to an EntityModelFactory. The EntityModelFactory is a Spring singleton and can be acquired by injection (@Inject or @Autowired). You can also acquire a reference to the EntityModelFactory by calling the getEntityModelFactory method on the *ServiceLocator*.

You can then acquire the EntityModel for a certain entity by calling the getModel(Class<?> clazz) method. This will retrieve the entity model for the specified class, lazily constructed when needed. Note that the entity model is effectively immutable and application-scoped (or more precisely, it has the Spring Singleton scope, i.e. there is one instance per Spring application context).

This also means that the same entity model is in principle used by all screens within an application. Since this would be too restrictive in practice, it is possible to construct separate instances for separate screens or use cases, by calling the getModel(String reference, Class<?> clazz) method. This will construct the Entity Model based on the provided class (the second parameter), but it will allow you to override certain attributes using message bundle entries (more on this later). The reference string is the unique identifier that you assign to the model (if you just call the getModel variant with a single parameter, then the simple name of the class is used as the reference) and which is then used as part of the message bundle entry.

The classes for which you create an Entity Model must inherit from the com.ocs.dynamo.domain.AbstractEntity class. See chapter 6 for details.

Entity level settings

The Entity Model supports several attributes that define how the entity itself is represented. These include:

  • displayName: the name of the entity (e.g. “Car”)

  • displayNamePlural: the name of the entity, in plural form (e.g. “Cars”)

  • description: textual description of the entity

  • displayProperty: the name of the property to use when displaying the entity inside e.g. a combo box. This property is also used as the title of the entity that is placed above an edit form.

  • sortOrder: how the entities are sorted by default when displayed in a grid or list. The sortOrder consists of a comma separated list of attribute names and sort directions, e.g. “name asc, age desc”. The direction is optional and if it is not supplied, “asc” will be used by default. This should be familiar to anybody who has worked with SQL.

  • filterProperty: this is a new property that comes into play when you define an entity field (like a combo box) that used a paginated data model. When this is the case, the values will not all be loaded into memory at once, but instead retrieved on-demand. In this case, you can use the filterProperty setting to define the property to filter on while searching the database. If this property is not explicitly set, the application will revert to the displayProperty instead but it can be necessary to override this e.g. if the displayProperty is a calculated property.

Attributes

Every Entity Model consists of a number of Attribute Models. By default, an Attribute Model is created for every valid property of the entity. E.g. if you have an entity Person with properties “name” and “age”, then the attribute model for the Person entity will contain two attribute models, one for “name” and one for “age”.

The following rules apply when constructing the attribute models:

  • An attribute model will be created for every public, non-static no-parameter getter-method that follows the JavaBean naming convention (e.g., “getAge()”; for Boolean or boolean properties, the getter may also start with “is”, e.g. “isValid”).

  • You can use Lombok to generate getters and setters for you.

  • The entity class does not necessarily have to contain an actual field corresponding to the property. This allows you to create attribute models for read-only or composite properties (e.g. a “getNameAndAge()” method which concatenates the name and age as a String). Note that such an attribute will have to be defined as read-only.

  • Certain attributes are ignored. Currently this includes only “version” (used for JPA optimistic locking) and “class” (every object has a “getClass()” method).

  • Attributes can be simple (String, Integer, Long, enumerated types etc.) or complex (a reference to another entity, a collection of primitive values, or a collection of other entities). The Entity Model generation is nested, which means that if a property of an entity is again an entity, then an entity model for the nested property will also be generated. This entity model is separate from the non-nested entity model that would be constructed directly for the entity.

    • Nearly all settings for nested models are treated the same as the setting on the top level, but there is one exception: the searchable setting on attributes of nested entities is ignored – this is because when you are creating a search screen for an entity, you normally want to search on the attributes of that entity, not on the nested attributes.

  • Getters that are annotated with @*AssertTrue* or @*AssertFalse* are skipped (these are methods that are used for Bean Validations, not properties for the meta model).

An attribute model has a name attribute that is equal to the name of the property. This name can be used to retrieve the attribute model from the entity model:

AttributeModel getAttributeModel(String attributeName);

For a nested attribute model, the name of the model consists of the concatenation of the names of the non-nested models separated by periods. E.g. if you have a Person entity that has an attribute address of type Address, then the “houseNumber” attribute model of the address has the path “address.houseNumber”.

Note: this should all make sense as it corresponds to the paths that are used in e.g. JPQL queries and by the Vaadin data binding framework.

Defaults and attribute overrides

The Entity Model generation is based on sensible defaults and metadata. E.g. the value of the type setting of an Attribute Model is directly taken from the Java type of the property, and certain other aspects e.g. whether the attribute is visible in a grid or can be used in a search form are derived from this type (e.g. by default a complex attribute will not be visible in a grid).

In addition to this, the Entity Model generation process will take certain JSR-303 annotations (e.g. @NotNull, @Size) into account. A detailed explanation for each setting will be given below.

If the default values are not sufficient, you can override them by using annotations:

  • On the entity level, you can use the @Model annotation.

  • On the attribute level, you can use the @Attribute annotation.

The @Model annotation can be used like this:

@Model(displayProperty = "description")
public class Meeting extends AbstractEntity<Integer> {

The @Attribute annotation can be placed either directly on the property, or on its getter method. Annotations placed on the getter method override those placed on the property, to easily allow you to override default behaviour in subclasses. Within a single entity class, you can use both access types interchangeably.

Message bundle overrides

The annotation override mechanism is quite powerful, but it has some drawbacks. E.g. it hard-codes certain String values (display name, description) into your application and it does not directly allow for internationalization. It also only allows you to override the behaviour of the “default” Entity Model that is based directly on the class, and not the behaviour of any derived Entity Models.

If you need to override the behaviour of a derived Entity Model, you can use the message bundle mechanism to achieve this. Message bundle overrides must be placed in the entitymodel.properties file (create a locale-specific version of this file if you need to; the normal Java message bundle mechanic is supported).

Message bundle entries in general have the following structure:

[Reference].[Attribute Model Name].[Attribute]=[Value]

Where:

  • [Reference] is the reference to the attribute model. This is the simple class name of the entity for a standard entity model, and the user-provided reference for a non-standard model.

  • [Attribute Model Name] is the (possibly nested) name of the attribute model. This is empty in case you are directly overriding a setting of the Entity.

  • [Attribute] is the setting that you want to modify. For a full list, see the EntityModel class which contains constants that denote the possible values (or refer to the sections below).

  • [Value] is the desired value of the setting.

The [Attribute Model Name] part must be omitted when you want to directly set an attribute of the Entity Model itself.

Some examples:

Organization.displayName=Criminal Organization

Sets the display name of the Organization entity to “Criminal Organization”.

Person2.displayName=Gang Member

Sets the display name for Person in the “Person2” entity model to “Gang Member”

Person.name.visible=true

Sets the visibility of the “name” attribute model to true

Person.address.street.readOnly=true

Sets the “read only” setting of the address.street attribute model (a nested attribute model) to false.

Please observe the following:

  • For Boolean values, use the (lower case) values “true” and “false”.

  • For numeric values, simply use the String representation of the numeric value. Use the period “.” as the decimal separator.

  • For enumeration values, use the upper-case String representation of the enumeration value.

  • For dates and times, use the formats that are specified by the system properties:

    • “ocs.default.date.format” (dd-MM-yyyy) for dates

    • “ocs.default.time.format” (HH:mm:ss) for times

    • “ocs.default.datetime.format” (dd-MM-yyyy HH:mm:ss)

    • “ocs.default.datetime.zone.format” (dd-MM-yyyy HH:mm:ssZ)

  • For the visibleInForm and visibleInGrid settings, both the enumeration values (SHOW/HIDE) and the boolean values true and false are supported.

Attribute Model settings

In this section, we explain all the supported settings of the attribute model.

AllowedExtensions

In message bundle: allowedExtensions = [String]

This setting can be used to specify the extensions of the files that are accepted by the file upload component that is generated for a LOB property. By default, its value is empty, which means there are no restrictions on the file type.

The value can be set to a comma-separated list of supported extensions, e.g. bmp,jpg,png. Note that you must not include the “.” Characters.

On the @Attribute annotation, you can use an array of String values instead of a comma-separated String. Extensions are not case-sensitive and you must not include the “.” character.

AttributeType

The attributeType setting is a classification of the type of the property. It is determined automatically during the Entity Model generation process and can have the following values:

  • BASIC: represents a simple property like a String, a number, a date etc.

  • DETAIL: a property that appears as a @OneToMany or @ManyToMany relation in the entity class, e.g. the orderLines attribute inside an Order entity will be considered a DETAIL attribute.

  • MASTER: a property that appears as a @OneToOne or @ManyToOne in the entity class.

  • LOB: a property that is annotated with @Lob and represents a large binary object (like a file or an image)

  • EMBEDDED: used during the Entity Model construction process to handle embedded properties (using the @Embedded annotation). This will be covered in the Advanced section.

  • ELEMENT_COLLECTION: a property that is annotated with the @ElementCollection annotation, i.e. a collection of simple values like integers and Strings (note: the JPA spec does allow more complex element collections, but these are not currently supported by Dynamo).

The attribute type in combination with the Java type determines how a certain attribute will be displayed on-screen in an edit form:

  • For a BASIC property, a simple user interface component will be displayed, based on the type of the property:

    • For String fields and numeric fields, a text field will be rendered. For a String property, you can use the textFieldMode setting to render a text area or a password field instead. For an Integer field, you can use the numberFieldMode setting to render an “integer field” instead.

    • For a Boolean, a check box will be rendered

    • For a LocalTime attribute, a time picker will be rendered.

    • For a LocalDate attribute, a date picker will be rendered.

    • For a LocalDateTime attribute, a date/time picker will be rendered.

    • For a ZonedDatetime attribute, a date/time picker (with a time zone component) will be rendered.

    • For an enumeration, a combo box will be created. You can use the message bundle mechanism to specify translations for the enumeration values (more on this below)

  • For a LOB property, a file upload field will be created.

  • For a MASTER property, by default a combo box that contains all the possible values (as retrieved from the repository) will be created. You can replace this by a lookup field or ListSelect by changing the value of the selectMode setting.

  • For a DETAIL property, by default a token field will be displayed. This can be modified using the selectMode setting and can e.g. by changed to a lookup field.

  • For an ELEMENT_COLLECTION property, the application renders a simple grid component that can be used to manage the collection of simple values.

Inside a search form the rendering is a slightly different:

  • For a BASIC property:

    • For a String property, a text field is created. This text field can be used to perform a search. You can use additional properties to toggle the case sensitivity and whether to allow prefix or substring matches. The textAreaMode setting is ignored in search forms.

    • For a numeric or a date/time property, two search fields are generated. These allow the user to perform an interval search (return all values that are higher than the value in the first field and lower than the value in the second field). This interval search is inclusive (i.e. the boundaries match). If you do not want this behaviour, you can change the value of the searchForExactValue setting to *true*; if you do this then only a single search field will appear.

      • For a property of type LocalDateTime or ZonedDateTime you can set the searchForDate setting to true. If you do this then only a single search field will be created. In this field you can select the date to search on.

    • For a Boolean property, a combo box is displayed. This combo box contains three options: true, false, and “no value”.

    • For an enumeration, a combo box containing all values of the enumeration is displayed.

  • For a MASTER property, by default a combo box containing all possible values of the master entity is displayed. You can use the searchSelectMode and/or selectMode settings to replace this by a lookup field or a listbox.

  • For a DETAIL property, by default a token field is created. You can use the searchSelectMode and/or selectMode settings to replace this by a lookup field or a combo box.

  • LOB properties cannot be used in search forms.

The attributeType setting also determines whether the property will be visible by default:

  • In a results grid, by default only BASIC attributes will be visible. Use the visibleInGrid attribute to show a complex attribute inside a grid.

    • For a MASTER property, the value of its displayProperty property will be used.

    • For a DETAIL property (remember, this represents a collection!), the values of the displayProperty properties of all individual entities in the collection will be displayed, separated by commas.

  • a MASTER or DETAIL attribute will by default not be displayed inside an edit form. You can change this by setting the visibleInForm setting of the attribute model to true. Note that in some cases you may also have to implement some custom logic for dealing with these properties.

When displaying an enumeration value inside a combo box, the values that are displayed inside the combo box are taken from the message bundle:

[Simple Class Name].[Enumeration Value]=[desired value]

E.g.:

Reputation.REALLY_NOT_FEARSOME=Really not [.underline]#fearsome#
Reputation.MILDLY_FEARSOME=Mildly [.underline]#fearsome#
Reputation.FEARSOME=[.underline]#Fearsome#
Reputation.EXTREMELY_FEARSOME=Extremely [.underline]#fearsome#

AutofillInstructions

In message bundle: autofillInstructions = [String]

The autofillInstructions setting can be used to define the attribute-specific instructions for automatically filling a form based on an AI service (Large Language Model). This is covered in more detail in a later section.

Cascade

In message bundle:

cascade.[index] = [path to attribute]
cascadeFilterPath.[index] = [path to attribute]
cascadeMode.[index] = BOTH | EDIT | SEARCH

The cascade setting can be used to define “cascading search” for selection components. Cascading search means that when you select a value in a certain selection component, the available values in another component change based on this choice. E.g. suppose that you are editing or searching for an Organization and you have selection fields for a country and for a list of members of the organization– choosing a country from the list will limit the values in member list to the people that originate from that country.

To set up cascading, you can define one or more @Cascade annotations as part of the @Attribute annotation. Each @Cascade annotation takes three parameters:

  • cascadeTo - this is the path to the attribute for which the selection must change in response to a change of the annotated attribute. In our example, the attribute to change is “members” (see below).

  • filterPath – this is the path that determines which filter to apply to the selection component that is on the receiving end of the cascade action. In our example, we want to filter a list of Person so that only persons from a certain country are returned – this country is stored in the countryOfOrigin property of the person so this is our filter path.

  • mode - this specifies whether cascading should be enabled in search forms, in edit forms, or in both cases. The default is BOTH.

@Attribute(visibleInGrid = VisibilityType.SHOW, searchable = SearchMode.ALWAYS, visibleInForm = VisibilityType.SHOW, cascade =
    @Cascade(cascadeTo = "members", filterPath = "countryOfOrigin", mode = CascadeMode.EDIT))
private Country countryOfOrigin;

@Attribute(searchable = true, visibleInForm = VisibilityType.SHOW)
private Set<Person> members = new HashSet<>();

Setting up cascading in a message bundle is a bit more involved. You can do so by defining two or three messages like this:

Organization.countryOfOrigin.cascade.1=members

Organization.countryOfOrigin.cascadeFilterPath.1=countryOfOrigin

Organization.countryOfOrigin.cascadeMode.1=EDIT

The cascade message defines the property to apply the cascading to – the cascadeFilterPath is the property path to filter on and the optional cascadeMode determines when to apply the cascading. Each message must end with a number that is used to group the messages together. The numbering starts at “1” and must use increments of 1, so if e.g. you want to define another cascade for the same attribute, that would look like this:

Organization.cascade.2=[some other property to cascade]

Organization.cascadeFilterPath.2=[some other path]

Organization.cascadeMode.2=EDIT

ClearButtonVisible

In message bundle: clearButtonVisible = true | false

This setting can be used to specify whether a “clear” button (by default a big X) must appear at the end of a component. This button can be used to easily clear the contents of the component. This is supported for most components, but not for e.g. check boxes and list boxes. Also, for the lookup field the clear button is always present and hence this setting will be ignored for that component.

CollectionTableFieldName

In message bundle: N/A

This setting is used to store the value of the name of the field inside the collection table that is used to store an attribute of type ELEMENT_COLLECTION. The value for this setting is normally determined by the “name” specified on the @Column annotation. E.g., in this case:

@ElementCollection
@CollectionTable(name = "person_lucky_numbers")
@Column(name = "lucky_number")
@Attribute(visibleInForm = VisibilityType.VISIBLE, minValue = 10, maxValue = 25)
private Set<Integer> luckyNumbers = new HashSet<>();

the value of the collectionTableFieldName setting is automatically set to “lucky_number”

This setting is used internally by the framework when for searching inside the collection table and cannot be modified directly.

CollectionTableName

In message bundle: N/A

This setting is used to store the value of the name of the collection table that is used to store an attribute of type ELEMENT*_*COLLECTION. The value for this setting is normally determined by name specified on the @CollectionTable annotation. E.g., in this case:

@ElementCollection
@CollectionTable(name = "person_lucky_numbers")
@Column(name = "lucky_number")
@Attribute(visibleInGrid = VisibilityType.SHOW, minValue = 10, maxValue = 25)
private Set<Integer> luckyNumbers = new HashSet<>();

the value of the collectionTableName setting is set to “person_lucky_numbers”.

This setting is used internally by the framework when for searching inside the collection table. You do not normally have to modify it.

Currency

In message bundle: currency = true | false

This setting can be used to specify that a numeric field (currently only supported for BigDecimal properties) contains a currency value. If this setting is changed to true for a property, then a currency symbol will be displayed in front of the value of the property. The currency symbol defaults to the Euro sign (€) but can be modified using the system property ocs.default.currency.symbol.

CurrencySymbol

In message bundle: currencySymbol = [string]

The currencySymbol setting can be used to set the currency symbol (e.g. “$”) to use when the currency setting is set to true.

The default value is the value of the system property ocs.default.currency.symbol.

Custom

It is also possible to specify custom attribute settings. This can be done by using the @CustomSetting annotation which can be applied as follows:

@Attribute(main = true, textFieldMode = AttributeTextFieldMode.TEXTAREA, custom = {
    @CustomSetting(name = "customString", value = "string"),
    @CustomSetting(name = "customInt", value = "4", type =  CustomType.INT),
    @CustomSetting(name = "customBool", value = "true", type =  CustomType.BOOLEAN) })
private String name;

In the above, we specify three custom settings named “customString”, “customInt” and “customBool”. Each setting consists of a name, a value and a type. The type can be omitted in which case it defaults to CustomType.STRING.

You can retrieve the value of a custom setting by calling the AttributeModel.getCustomSetting(String name) method.

You can also add custom settings via the message bundle. This looks as follows:

Organization.name.custom.1=customString
Organization.name.customValue.1=countryOfOrigin
Organization.name.customType.1=INT

For each custom setting, you specify three properties, “custom”, “customValue” and “customType”. Each property must contain a number, starting at 1, to indicate which properties belong together. Custom settings defined in the message bundle overwrite the settings defined using the @CustomSetting annotations.

DateType

In message bundle: dateType = TIMESTAMP | DATE | TIME

The dateType setting can be used to determine how an attribute of type LocalTime, LocalDate, LocalDateTime or ZonedDateTime will be managed:

The allowed values are:

  • TIMESTAMP: In this case the application renders a date picker that includes a time selection component.

  • DATE: in this case the application renders a date picker without a time selection component.

  • TIME: in this case a custom time selection component is rendered.

By default, the value of the dateType setting is derived from the Java type of the property. You do not normally have to manually override it.

Dynamo does not support the Legacy Java date types (java.util.Date and java.sql.Date).

DefaultValue

In message bundle: defaultValue = [String]

The defaultValue setting can be used to set the default value that appears inside the user interface component when creating a new entity.

You always specify this setting as a String; if the value must be converted to a decimal number, use the period (“.”) as the decimal separator. For enumeration values, use the upper-case String representation of the desired value.

For date attributes, use the String representations according to the system properties ocs.default.date.format (dd-MM-yyyy), ocs.default.time.format (HH:mm:ss), ocs.default.datetime.format (dd-MM-yyyy HH:mm:ss), ocs.default.datetime.zone.format (dd-MM-yyyy HH:mm:ssZ)

Description

In message bundle: description = [String]

The description setting determines the value of the tooltip that the user will see when hovering over the input field for the property.

If not explicitly set, it will default to the value of the displayName setting.

DisplayFormat

In message bundle: displayFormat = [String]

The displayFormat setting indicates how date/time values will be formatted. It is supported for attributes of a Java 8 date/time type (LocalDate, LocalTime etc).

The value of the displayFormat attribute must be a valid Java data/time formatting pattern, e.g. “dd-MM-yyyy”, but you can use different separators like “dd/MM-yyyy” or use formats like “yyyy-MM-dd”.

If you do not specify a displayFormat for an attribute, the framework will default to the value of the ocs.default.date.format, ocs.default.time.format, ocs.default.datetime.format, or ocs.default.datetime.zone.format system variables depending on the dateType of the attribute model.

Note: this currently only applies to how the date/time value will be displayed in read-only mode, and to setting default values. Inside a date picker or time picker, the formatting will depend on the user’s or application’s locale. This is due to a limitation in the underlying framework.

The description is localized – you can use message bundles to define custom display names depending on the locale of the user.

DisplayName

In message bundle: displayName=[String]

The displayName setting determines how the attribute will be named onscreen. By default, it is derived from the name setting, replacing CamelCase notation by spaces and then capitalizing individual words, e.g. “mininumAge” will be translated to “Minimum Age”. You can use the system property ocs.capitalize.words and set it to false so that only the first word will be capitalized.

The display name is localized – you can use message bundles to define custom display names depending on the locale of the user.

EditableType

In message bundle: editableType = READ_ONLY | CREATE_ONLY | EDITABLE

The editableType setting specifies when an attribute can be edited. The default value EDITABLE means that the attribute can always be edited. CREATE_ONLY means that the attribute can only be edited when creating a new entity. READ_ONLY means that the property is read-only and cannot be edited in a the interface.

Note: in previous versions of the framework, this setting was called “readOnly” and could only be used to toggle between editable and read-only state.

Email

In message bundle: N/A

The email setting can be used to specify that a field must contain a valid email address. It is automatically set to true if the property is annotated with the (custom) @Email annotation.

Embedded

In message bundle: embedded = true | false

The embedded setting can be used to specify that a certain (complex) attribute must be treated as an embedded attribute. An embedded attribute is treated as an integral part of the entity, and no separate entity model is created for an embedded attribute.

By default, any attribute annotated with the JPA @Embedded attribute will have its embedded setting set to true but you can override this using the @Attribute annotation or a message bundle.

// inside Gift.java
@Attribute(embedded = true)
@JoinColumn(name = "logo")
@OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL)
private GiftLogo logo = new GiftLogo();

// inside GiftLogo.java

@Lob
@Attribute(image = true, fileNameProperty = "logo.fileName")
private byte[] image;

@Attribute(readOnly = true, showInTable = VisibilityType.HIDE)
private String fileName;

In the example above, we see a Gift class with an embedded attribute “logo” of type GiftLogo. The GiftLogo class contains several fields like “image” and “filename”.

By setting embedded to true, we effectively make sure that there is no entity model for GiftLogo, and all the attributes from GiftLogo are made part of the entity model of the embedding class, Gift. When you want to access an attribute model for an attribute inside GiftLogo, you must use its full path, e.g. “logo.fileName” or “logo.image” rather than just “fileName’ or “image”.

FileNameProperty

In message bundle: fileNameProperty = [property name]

The fileNameProperty setting can be used to specify the name of the property that is used to store the name of an uploaded file after a file upload. This setting is intended to be used on attributes of type LOB:

@Lob
@Attribute(image = true, fileNameProperty = "logo.fileName")
private byte[] image;

// hide in grid to prevent fetch issues
@Attribute(readOnly = true, visibleInGrid = VisibilityType.HIDE)
private String fileName;

By default, if you define an attribute of type LOB, then the application will render a file upload component for editing this attribute. The byte content of the uploaded file will be stored in the property itself, but the file name of the file that was uploaded will not be persisted.

If you want to store the file name as well, simply create another property (of type String) and then set the fileNameProperty of the @Attribute annotation that is placed on the property that holds the byte content to point to this property. The framework will then store the name of the uploaded file in this property as part of the file upload process.

Note that the actual “fileName” property is annotated as “readOnly” since it is automatically set by the framework and does not need to be modified by the user.

GridSelectMode

In message bundle: gridSelectMode = COMBO | LOOKUP | LIST | TOKEN

The gridSelectMode setting is used to specify how a component for an attribute of type MASTER or DETAIL will be rendered inside a grid (EditableGridLayout).

By default, the value of the gridSelectMode setting is equal to the value of the selectMode but you can change it explicitly if you want a different component to be rendered inside a grid.

It is typically not advisable to use a component that takes up more than one row inside an editable grid, so it is probably best to stick to COMBO or LOOKUP.

GroupTogetherWith

In message bundle: groupTogetherWith = [comma separated list of attribute names]

The groupTogetherWith setting can be used to specify that the input components for several attributes must be placed together on a single row in an edit form. This is a good way of saving screen space. The value of this setting consists of a list of attribute names. The input components for these attributes will be placed behind the original attribute, in the order in which they are defined.

Here you see an example of how the “region” attribute

@Attribute(visibleInGrid = VisibilityType.SHOW visibleInGrid = VisibilityType.SHOW, searchable = true, groupTogetherWith = {"region"})
private Country country;

And this is the input form that will be generated:

image

You can still use all available settings to modify the behaviour of the components for the “extra” attributes that are placed behind the first attribute. The framework makes sure that the extra attributes do not show up more than once.

Note that for this to work properly, the attribute that the groupTogetherWith setting refers to must occur in the attribute order after the attribute that does the referring (in term of the example above, region must come after country). If this rule is not observed, then a warning message will be logged during the entity model creation process and the behaviour is undefined.

You can modify how the grouped together fields scale when the screen is made larger or smaller by using the setGroupTogetherMode and setGroupTogetherWidth methods found on many layouts. By default, the values of these properties are determined by the system properties ocs.default.group.together.mode and ocs.default.group.toghether.width . The mode supports two values, pixel and percentage. Pixel means that each field will be the value of groupTogetherWidth pixels wide. Percentage means that every field will take up an equal percentage of the available space.

IgnoreInSearchFilter

In message bundle: ignoreInSearchFilter = true | false

This setting can be used for rare occasions in which you want to use an attribute inside a search form (e.g. for setting up cascading) but you want to ignore the selected value when actually performing a search.

Image

In message bundle: image = true | false

This setting can be used on a LOB property to specify whether it represents an image. By default, this setting has the value false*. If set to *true, the application will try to render a preview image of the value (byte contents) of the property.

Main

In message bundle: main = true | false

The Boolean setting main can be used to specify that a certain property is the main property of an entity. The main property is the property that will be used as the default field to search on using a quick search field inside a SplitLayout.

By default, the first encountered property of type String will be marked as the main attribute but you can use this setting to override the default.

In the past, the main attribute was also used for creating the title that appears above a search form. However, currently the displayProperty from the @Model annotation is used for this.

MaxLength

In message bundle: maxLength = [integer value]

The maxLength setting can be used to specify the maximum allowed length of the individual values inside a collection of Strings for a property of type ELEMENT_COLLECTION.

Note: for String attributes, you can just add the @Size(max=xxx) annotation from the Java validation framework. This will add the proper validation to the form (and does not depend on the entity model)

MaxLengthInGrid

In message bundle: maxLengthInGrid = [integer value]

The maxLengthInGrid setting can be used to set the maximum length of the value of a String property when it is displayed inside a grid – if the value of the property is longer than this, the value will be truncated after the first maxLengthInGrid characters. This can help save space in grids.

MaxValue

In message bundle: maxValue = [integer value]

The maxValue setting can be used to specify the maximum allowed value of the individual numeric values that make up the value of a property of type ELEMENT_COLLECTION.

This is an example of the use of maxValue on a collection of Integers:

@ElementCollection
@CollectionTable(name = "person_lucky_numbers")
@Column(name = "lucky_number")
@Attribute(visibleInGrid = VisibilityType.SHOW, minValue = 10, maxValue = 25)
private Set<Integer> luckyNumbers = new HashSet<>();

MemberType

In message bundle: N/A

The memberType setting can be used to explicitly set the member type (i.e. the type of an individual entity) of an attribute type DETAIL. Normally, the member type can be derived from the source code automatically, but there are certain cases in which this is not possible, e.g. when working with a property that does not directly map to a member field, but rather returns a collection that is calculated on the fly. In this case, you can use the memberType to set the exact type of the members of the collection.

This setting is only supported as an annotation override.

MinLength

In message bundle: minLength = [integer value]

The minLength setting can be used to specify the minimum allowed length of individual String values inside a property of type ELEMENT_COLLECTION.

Note: for String attributes, you can just add the @Size(min=xxx) annotation from the Java validation framework. This will add the proper validation to the form (and does not depend on the entity model)

MinValue

In message bundle: minValue = [integer value]

The minValue setting can be used to specify the minimum allowed value of the individual numeric values that make up the value of a property of type ELEMENT_COLLECTION.

An example of the use of minValue on a collection of Integers:

@ElementCollection
@CollectionTable(name = "person_lucky_numbers")
@Column(name = "lucky_number")
@Attribute(visibleInGrid = VisibilityType.SHOW, minValue = 10, maxValue = 25)
private Set<Integer> luckyNumbers = new HashSet<>();

This specifies that the individual values inside the collection must all be greater than or equal to 10.

MultiSelectMode

In message bundle: multiSelectMode = CHECKBOX | ROWSELECT

The multiSelectMode can be used to specify the way in which multiple items can be selected inside a search dialog (used when the select mode = LOOKUP). When this setting is set to CHECKBOX, each row is prefixed with a check box. When it is set to ROWSELECT then the user can select multiple rows by Ctrl- and Shift-clicking on the rows.

The default value of this setting depends on the value of the system property ocs.use.grid.selection.checkboxes (if set to true, then the default will be CHECKBOX).

MultipleSearch

In message bundle: multipleSearch = true | false

The multipleSearch setting can be used to allow searching on multiple values at once for attributes of type MASTER. By default, you would only be allowed to search on a single value at a time for such attributes, but if you set this setting to true you will be allowed to select multiple values (and the application will return all entities that match at least one of the selected values). This will also change the component that is rendered by default from a combo box to a token field.

You can use the searchSelectMode to further modify the type of the search component that is rendered (you can also use a lookup field).

Default searching for many-to-one property –> combo box:

@Attribute(visibleInGrid = VisibilityType.SHOW, searchable = true, visibleInGrid = VisibilityType.SHOW)
private Country countryOfOrigin;

image

Multiple search enabled –> lookup field:

@Attribute(visibleInGrid = VisibilityType.SHOW, searchable = true, multipleSearch = true, visibleInGrid = VisibilityType.SHOW)
private Country countryOfOrigin;

Multiple search on MASTER attributes is currently only supported when using LOOKUP select mode (TOKEN is not supported).

image

You can also use the multipleSearch setting for searching on distinct String values – As you have seen before, by default the framework will generate a text field. However, if you set multipleSearch to true and searchSelectMode to TOKEN then the application will render a token field that allows you to enter multiple values to search on. The token field will contain a list of all distinct values of the property from which the user can choose.

@Attribute(searchable = true, main = true, maxLengthInGrid = 10, multipleSearch = true, searchSelectMode = AttributeSelectMode.TOKEN)
private String name;

image

Navigable

In message bundle: navigable = true | false

The navigable setting can be used to specify that a hyperlink for in-application navigation must be rendered for a certain property. This works both in a grid and in a detail form. This is only supported for properties of type MASTER (i.e. many-to-one relations). When a property is declared to be navigable, a button that looks like a hyperlink will be rendered to represent it (in a grid or in a form in edit mode) and clicking this button will kick of the navigateToEntityScreen method (see below).

In order to use this form of navigation, you need to do three things:

  • Set the navigable setting to true.

  • In the UIHelper class, which can be injected in your own classes, call the addEntityNavigationMapping method. This method takes as its parameters the class for which you want to defined a navigation, and a Java 8 consumer that defines the action to be take. Typically, what you will want to do in this method is assign the selected object (the argument of the consumer) to a field on your UI, and then navigate to the appropriate view.

  • In the view to which you navigate, you need to retrieve the object you just stored from the UIHelper, and do something with it. Typically, this will involve calling the edit method on a composite component, which will cause the component to display an edit form containing the selected entity.

NumberFieldMode

In message bundle: numberFieldMode = TEXTFIELD | NUMBERFIELD

The number field mode can be used to set the field mode to use for a numeric property (currently only supported for fields of type “int” or “Integer”). When set to “TEXTFIELD” (the default), a text field will be rendered to edit the attribute. This text field includes validation, but it is still possible to input non-numeric characters. When set to “NUMBERFIELD” a special numeric input component (IntegerField) will be used instead. This component only accepts numeric input and also comes with a set of plus and minus buttons.

image

The default value of this setting can be modified by changing the system variable ocs.default.number.field.mode.

NumberFieldStep

In message bundle: numberFieldStep = <integer>

The number field mode can be used to set the step size to be used for a number field (see the previous section). The default value is 1, but you can set this to any positive integer.

PagingMode

In message bundle: pagingMode = PAGED | NON_PAGED

The pagingType setting is used to specify how the items within an entity component (combo box, token field, or list select) will be retrieved. When set to PAGED the items will be retrieved in an on-demand matter (one page at a time). When set to NON_PAGED all the items will be retrieved at once. If there are many items in the collection, this might cause memory issues.

With regard to filtering the items within the component, when you use the NON_PAGED setting, the application will simply search inside the descriptions of the items (which are simply the values of the displayProperties of the items). When using the PAGED setting, the value of the filterProperty setting of the entity model will be used instead.

If you don’t specify a value for the filterProperty, the value of the displayProperty will be used instead. If this property does not exist in the database, you will get an error at run-time when the users tries to filter the results.

The default value of this setting can be set by using the system property ocs.default.paging.mode.

Percentage

In message bundle: percentage = true | false

The percentage setting is used to indicate whether a numeric value represents a percentage. By default, this attribute has the value false*. If set to *true, then the value of the property will be displayed with a “%” sign following it, both in read-only and edit mode.

The percentage sign is purely cosmetic; the actual value of the property is not converted or changed in any way.

Precision

In message bundle: precision = [Numeric value]

The precision setting determines the number of digits will be shown behind the decimal separator when displaying non-integer numbers. By default, it is set to 2 but you can change this by changing the value of the system property ocs.default.decimal.precision.

Prompt

In message bundle: prompt=[String]

The prompt setting determines the value of the prompt that shows up inside the editable field for the property.

If not set, it defaults to the value of the displayName setting.

Note: in the Vaadin source code, this property is now called placeholder. It is unfortunately not supported on all components.

QuickAddAllowed

In message bundle: quickAddAllowed = true | false

The quickAddAllowed setting can be used to allow the creation of entities directly from inside a form, for a UI component that is used to manage a MASTER or DETAIL relation (e.g. a list select, combo box, token select, or lookup field). Normally, in such a case a combo box, list select or similar component will be rendered (depending on the value of the selectMode setting)

If you set the quickAddAllowed setting to true, an Add button will be rendered next to the edit component for the property. When pressed, this button will bring up a dialog that will allow the user to create a new entity.

When the user presses the OK button in this dialog, the framework will create a new entity based on the contets of the dialog. This comes with an automatic check for duplicate values, provided you have configured this on the underlying service.

As an example, consider the following:

@NotNull
@JoinColumn(name = "country_of_origin")
@ManyToOne(fetch = FetchType.LAZY)
@Attribute(visibleInGrid = VisibilityType.SHOW, quickAddAllowed = true, selectMode = AttributeSelectMode.LOOKUP)
private Country countryOfOrigin;

Here, we define a “countryOfOrigin” property that is of type “Country”.. We set the quickAddAllowed to “true”. Once the user now starts the application, they will see an “Add” button behind the field that can be used to create a new country. Once pressed, the button will bring up the following dialog:

image

The user can now enter the properties of the country in the popup – once the user presses the “OK” button the application will store the new Country, add it to the options that are present in the selection component, and select it.

The application will carry out an automatic check for duplicates when the user tries to save the entity (based on the findIdenticalEntity functionality), and will then look for an error message stored under the “<short name of entity>.not.unique” key in order to display an error message. E.g. in the example above, you should add a “Country.not.unique” message to the message bundle.

ReplacementSearchPath

In message bundle: replacementSearchPath = [desired string value]

The replacementSearchPath setting can be used to modify the search path that is used when translating search filters into a query – it can happen that you are using a derived property in your search screen (e.g. to allow searching on only a subset of values) and when you take no further action this will produce an error when carrying out the query since the property is not known in JPA. In cases like this, you can use the replacementSearchPath setting to specify the alternate (real) path to use during the search.

A common use case for this occurs when you are using the functionality from the dynamo-functional-domain module, and you have multiple many-to-many relations from an entity to various Domain subclasses. In this case, you can model a single many-to-many relation between the entity and the domain table as follows:

@ManyToMany
@JoinTable(name = "product_domain", joinColumns = { @JoinColumn(name = "programme") }, inverseJoinColumns = { @JoinColumn(name = "domain") })
private Set<Domain> domains = new HashSet<>();

You can then retrieve and set the values for a certain domain subclass as follows:

@Attribute(memberType = Channel.class,replacementSearchPath = "domains")
public Set<Channel> getChannels() {
    return DomainUtil.filterDomains(Channel.class, domains);
}

public void setChannels(Set<Channel> channels) {
    DomainUtil.updateDomains(Channel.class, domains, channels);
}

In the above, we explicitly set the memberType so that the framework knows the type of the elements of the collection, and set the replacementSearchPath to “domains” so that when the framework generates a query, it will use the domains relations.

Note the getter and setter methods for the “channels” attribute don’t directly manipulate the domains property but rather use methods from the DomainUtil class that make sure the values are retrieved and updated correctly.

ReplacementSortPath

In message bundle: replacementSortPath = [desired string value]

You can use this setting to override the path to sort on when the user clicks on a column header in a search results grid. By default, the application will then sort on the exact path to the property, but if the replacementSortPath is set, that value will be used instead.

RequiredForSearching

In message bundle: requiredForSearching = true | false

The requiredForSearching setting determines if a property is required before a search can be carried out inside a SearchLayout. If you create a search form that contains properties that have requiredForSearching set tot true, you will not be able to carry out a search (i.e. the Search button will be disabled) until you provide a search value for these properties.

The default value of this setting is false.

When you do want to perform a check before carrying out a search but the fields that are required are dynamic (i.e. they depend on search values), you can instead use the callback method validateBeforeSearch offered by the SearchLayout components. This is described in more detail later.

Searchable

In message bundle: searchable = NONE | ALWAYS | ADVANCED

searchable = NONE | ALWAYS | ADVANCED

The searchable setting determines whether a property will show up in a search form on a search screen. By default, it is set to NONE which means it will not show up in a search form. Setting this property to ALWAYS means it will always show up in a search form. Setting it to ADVANCED means it will only show up in search forms for which “enabledAdvancedSearchMode” has been set to true (via the FormOptions) and only when the user activates the “advanced mode” in the screen.

SearchCaseSensitive

In message bundle: searchCaseSensitive = true | false

The searchCaseSensitive setting determines whether search operations on the property are case sensitive. The default is given by the system property ocs.default.search.case.sensitive which defaults to “false”. This setting is only used for attributes of type String and ignored in all other cases.

On the attribute, you can use the values BooleanType.TRUE and BooleanType.FALSE.

SearchDateOnly

In message bundle: searchDateOnly = true | false

The searchDateOnly setting determines whether search operations on an attribute that represents a date/time (either LocalDateTime or ZonedDateTime) are carried out using only date selection fields rather than time selection fields.

By default, when searching on an a date/time attribute, the application will render two timestamp search fields that allow you to specify a search interval. When you change this setting to true then instead the application will render to date selection fields. Searching using these date selection fields will return any time stamps that fall within the specified date interval (inclusive). E.g. if you enter the search values 2020-04-04 to 2020-04-06 you will return any records for which the time stamp value matches the interval from 2020-04-04 00:00:00 up to 2020-04-06 23:59:599999999

SearchForExactValue

In message bundle: searchForExactValue = true | false

This setting determines whether to search for an exact value rather than a range, when searching for numeric or date values. By default, for such a field two search fields will be rendered: one for the lower bound of the range to search for, and one for the upper bound of the range to search for.

By default, this setting has the value false. If set to true, then instead of the two search fields, a single field will be rendered that allows the user to search for an exact value.

SearchPrefixOnly

In message bundle: searchPrefixOnly = true | false

The searchPrefixOnly setting determines whether search operations on the property check only for a prefix match. If this is set to true, then searching for e.g. “a” will only match “almond” (“a” appears at start) but not “walnut” (“a” appears in the middle). If set to false, then “a” will match both “almond” and “walnut”.

By default, this setting has the value false. This setting is only used for attributes of type String and ignored in all other cases.

SearchSelectMode

In message bundle: searchSelectMode = COMBO | LOOKUP | LIST | TOKEN

The searchSelectMode setting is used to specify how the component for searching an attribute of attribute type MASTER or DETAIL will be rendered (inside a search form). It can also in some instances be used for searching on a BASIC attribute of type String.

By default, the value of the searchSelectMode setting is equal to the value of the selectMode but you can change it explicitly if you want a different component to be rendered inside a search form.

The following restrictions apply:

  • For a property of type MASTER you can use the values COMBO, LOOKUP or LIST.

  • For a property of type DETAIL you can use the values LOOKUP and TOKEN

  • For an attribute of type String, you can use the value TOKEN, in combination with setting the multipleSearch to true. This will produce a search field that allows you to select multiple values from among a list of all the distinct values of the attribute.

ShowPassword

In message bundle: showPassword = true | false

The showPassword setting can be used to specify whether a button to toggle between showing and hiding a password inside a password field will be shown. When this setting has the value false (the default), then the contents of the password field will always be shown obscured. When this setting has the value true, a button to toggle between an unobscured and obscured view is displayed at the end of the input component.

image

Sortable

In message bundle: sortable = true | false

The sortable setting can be used to specify whether a grid can be sorted on the attribute. By default, it is set to true for all attributes.

TextAreaHeight

In message bundle: textAreaHeight = [string value]

The textAreaHeight setting can be used to specify the height of a text area. It is only used when textFieldMode is set to TEXTAREA and accepts all ways to specify height in CSS/HTML (e.g. “px”, “em” etc). The default value is the value of the system property ocs.default.text.area.height

TextFieldMode

In message bundle: textFieldMode = TEXTAREA | TEXTFIELD | PASSWORD

The textFieldMode setting can be used to specify whether to render either a text field, a text area or a password field for editing an attribute of type String. The default is TEXTFIELD*. When value *TEXTAREA will be ignored inside a search form an inside an editable grid. The value PASSWORD will be ignored inside a search form.

ThousandsGroupingMode

In message bundle: thousandsGroupingMode = NEVER | ALWAYS | VIEW | EDIT

The thousandsGroupingMode setting can be used to indicate when a thousand-grouping separator must be used when formatting a floating point number:

  • NEVER indicates that the thousands grouping separator must never be used for this attribute.

  • ALWAYS indicates that the separator must be used both in view and edit mode.

  • VIEW indicates that the separator must only be used when the attribute value is displayed in read-only mode.

  • EDIT indicates that the separator must only be used when the attribute value is displayed in edit mode.

TrimSpaces

In message bundle: trimSpaces = true | false

This indicates whether extraneous space characters will be trimmed from the start and end of the input inside text areas and text fields. This defaults to false but can be modified by changing the value of the ocs.trim.spaces system property.

On the @Attribute annotation, you can use the “trimSpaces” setting which supports the values INHERIT, TRIM and NO_TRIM. When INHERIT is used, it will just use the value of the system property. With TRIM and NO_TRIM you can either enable or disable the trimming for this specific attribute.

TrueRepresentation and FalseRepresentation

In message bundle: trueRepresentation = [desired value] falseRepresentation = [desired value]

The trueRepresentation and falseRepresentation settings can be used to modify how a boolean value is displayed in read-only mode. By default, such a value will simply by displayed as “true” or “false”, but this can be overruled by setting respectively the trueRepresentation and falseRepresentation values.

This setting does nothing in edit mode, since in that case a checkbox will be rendered.

This e.g. allows you to include a search field for “region” in a search form, that when used will filter the available values in the “country” search field – once a search query is executed, the value of the (transient) region field will be ignored.

Type

The type setting represents the Java type of the property. It cannot (for obvious reasons) be modified using the annotation or the message bundle.

URL

In message bundle: url = true | false

The url setting can be used to specify that a certain String property must be rendered as a clickable URL.

The default value is false*. If set to *true, then a validator will be added to the field (when in edit mode) that checks if the entered value is a valid URL (must start with http or https). Also, in read-only mode the framework will render a clickable URL containing the value of the attribute – when clicked it will open the provided URL in a separate browser window.

VisibleInForm

In message bundle: visibleInForm = true | false | SHOW | HIDE

The visibleInForm setting determines whether a property will be displayed inside an edit form. It is not to be confused with the visibleInGrid attribute that governs whether a property shows up in a grid.

By default, all simple properties will have visibleInForm set to true, except for the “id” property which is reserved for a technical primary key and will by default be hidden from the user. In contrast, all complex properties will have visibleInForm set to false by default.

Instead of true you can also use the value SHOW and instead of false you can also sue the value HIDE.

VisibleInGrid

In message bundle: visibleInGrid = true | false | SHOW | HIDE

The visibleInGrid setting determines whether a property will be displayed in a search results grid.

By default, all BASIC properties will have visibleInGrid set to true, except for the “id” property which is used for a technical primary key and will by default be hidden from the user.

For all other properties, you must manually set the attribute to true (or SHOW).

Week

In message bundle: week = true | false

The week setting can be used to specify that a date field must be treated as a week code (e.g. “2016-34”). The default for this setting is false. If set to true, then instead of a date picker, a text field will be rendered for editing the property, and this text field will only accept input in the correct format (yyyy-ww). Also, in view mode the date will be displayed as the week code.

Note that under the covers, the value will still be treated and stored as a java.time.LocalDate.

Attribute ordering and grouping

Attribute ordering

In message bundle: attributeOrder=[comma separated list of attribute names]

By default, the properties of an entity will be displayed in the order in which they appear in the Java class file. This can be overruled by using an @AttributeOrder annotation or setting the attributeOrder via the message bundle.

The @AttributeOrder annotation takes a single parameter, named attributeNames which contains an array of field names – the order in which the attributes appear in the array is the order in which they will appear in the application.

@AttributeOrder(attributeNames = { "name", "headQuarters", "address", "countryOfOrigin", "reputation" })
public class Organization extends AbstractEntity<Integer> {

You can achieve the same effect by including a message like Organization.attributeOrder=name,headquarters,address,countryOfOrigin,reputation in the message bundle (use commas to separate the values). The message in the bundle will overwrite the ordering set by @AttributeOrder. If your entity has a large number of attributes this might get a bit unwieldy though.

The ordering does not have to be contain all properties; if you leave out any attributes, then those will be placed (in the normal order) after any attributes that are explicitly mentioned in the annotation or the message bundle.

Grid and search form attribute ordering

Also by default, the attribute order in a search form and in results grid is the same as the default attribute order (see the previous paragraph). You can override this by using the @GridAttributeOrder and @SearchAttributeOrder annotations.

@GridAttributeOrder(attributeNames = { "name", "headQuarters", "address", "countryOfOrigin", "reputation" })
@SearchAttributeOrder(attributeNames = { "name", "headQuarters", "address", "countryOfOrigin", "reputation" })
public class Organization extends AbstractEntity<Integer> {

These annotations do the following:

  • GridAttributeOrder sets the order of the attributes in the search results grid for the SearchLayout and the SplitLayout.

  • SearchAttributeOrder set the order of the attributes in the search form for the SimpleSearchLayout and in popup search screens.

These additional attribute orders completely overwrite the default attribute order, so you will have to redefine all attributes in the order you want to see them. Any attributes that are not explicitly mentioned are included at the end in alphabetical order.

You can also overwrite using message bundles:

Organization.searchAttributeOrder=name,headquarters,address,countryOfOrigin,reputation
Organization.gridAttributeOrder=name,headquarters,address,countryOfOrigin,reputation

Attribute grouping

In addition to ordering the attributes, they can also be grouped together. To do this, you can include an @AttributeGroups annotation on your class definition, which can in turn include any number of @AttributeGroup annotations.

Each @AttributeGroup annotation contains the name of the group and an array that contains the names of the properties that must be included in the group. As an example, consider:

@AttributeGroup(messageKey = "Organization.first", attributeNames = {"name", "address", "headQuarters", "countryOfOrigin" }),
@AttributeGroup(messageKey = "Organization.second", attributeNames = {"reputation" })
@AttributeOrder(attributeNames = { "name", "headQuarters", "address", "countryOfOrigin", "reputation" })
public class Organization extends AbstractEntity<Integer> {

The above defines two attribute groups identified by the message keys “Organization.first” and “Organization.second”. The display names of the groups can be defined in the message bundle:

Organization.first=First
Organization.second=Second

When you want to achieve the same using a message bundle, you can do this in the following way:

Organization.attributeGroup.1.messageKey=Organization.first
Organization.attributeGroup.1.attributeNames=name,address,headquarters,countryOfOrigin
Organization.attributeGroup.2.displayName=Organization.second
Organization.attributeGroup.2.attributeNames=reputation

I.e. you include two messages for every attribute group: one containing the message bundle key and one containing the attribute names as a list of comma-separated attribute names. The messages are numbered starting at “1”.

The attribute grouping is only used to determine which properties to group together, not to determine the order in which the attributes appear within this group. This order is still determined by the @AttributeOrder annotation as described in section 4.1.

When you want to refer to a certain attribute group in your code, you should do so by using the (unique) message key of that group.

Advanced entity model topics

Nested entity models

The Dynamo framework supports dealing with nested entities. When Dynamo generates an entity model for an entity, it automatically creates nested entity models for all complex properties it encounters. This is currently supported up to three levels deep. The models are constructed lazily when needed.

Note that the entity model that is created for a nested entity is a separate model from the top-level model for the entity. So, the direct model for the “Address” entity is a different model than the nested model for Person.address.

Some settings behave differently for nested entity models. E.g. for any properties of nested entities, the searchable and visibleInGrid settings will be set to false by default.

You can override settings on nested attribute models in the same way as you can override attributes of non-nested entities, i.e. by including a message in the message bundle that contains the full path to the property (e.g. Movie.director.name.displayName=Director Name). The @Attribute annotation only works on the non-nested entity model.

Element collections

The Entity Model framework also supports dealing with “element collection” properties, i.e. properties that are collections of simple types (currently String, Integer, Long and BigDecimal are supported) and that are annotated with the @ElementCollection annotation.

For these properties, the application will automatically generate a simple grid component that allows you to add items to, remove items from, and modify items in the collection. You can use the minLength and maxLength settings to modify the minimum allowed length and maximum allowed length of the individual items (in case of a collection of Strings), or use the minValue and maxValue settings to define a minimum or maximum value for a collection of numeric values.

This is needed because the regular Java Validation @Size annotation operates on the size of the collection, not the length of its individual members.

The grid component will look like this:

image

Data access, Service layers and general concepts

Data access layer and entities

Dynamo has certain requirements regarding the Data Access layer and Entity classes that are used in applications developed with the framework.

All Entity classes (classes that map to a table in the database) must inherit from the AbstractEntity class. This means that they inherit a version field (used for optimistic locking) and an id field that denotes the technical primary key. The type of this id field is configurable via the type parameter of the AbstractEntity class.

An example Entity class looks like this:

@Entity
@Model(displayProperty = "name")
@Table(name = "organization")
public class Organization extends AbstractEntity<Integer> {

For every Entity class, you must (normally) create a Data Access Object (DAO) interface and the accompanying implementation. The DAO must inherit from the BaseDao interface:

public interface OrganizationDao extends BaseDao<Integer, Organization> {
}

And the implementation must inherit from BaseDaoImpl:

@Repository("organizationDao")
public class OrganizationDaoImpl
    extends BaseDaoImpl<Integer,Organization>
    implements OrganizationDao {

    private QOrganization qOrganization = QOrganization.organization;

    @Override
    public Class<Organization> getEntityClass() {
        return Organization.class;
    }

    @Override
    protected EntityPathBase<Organization> getDslRoot() {
        return qOrganization;
    }
}

The minimal implementation contains just two methods: getEntityClass() which returns the type of the entity that is managed by the DAO, and getDslRoot() which returns the QueryDSL root.

QueryDSL is a framework that is used by the Dynamo Framework to create type-safe queries. Basically, what QueryDSL does is create a QueryDSL class for every entity class in your application. When developing in Eclipse, the IDE will automatically generate the appropriate classes. You can also run a command line Maven build to generate them.

Finally, note that the DAO implementation class is annotated with @Repository, which will register it as a Spring bean (it also has additional functionality in Spring Data, but Dynamo does not currently use the Spring Data library).

Service

In addition to developing a DAO for your entity, you must also create a service class. This service class in its basic form will serve as a delegate to the DAO, but it is also the place where you can place business logic.

The declaration of a service interface is very easy; the service must extend BaseService.

public interface OrganizationService
    extends BaseService<Integer, Organization> {
}

The implementation is equally simple:

@Service("organizationService")
public class OrganizationServiceImpl
    extends BaseServiceImpl<Integer, Organization>
    implements OrganizationService {

    @Inject private OrganizationDao dao;

    @Override
    protected BaseDao<Integer, Organization> getDao() {
        return dao;
    }
}

In its most basic form, you can defined a service by extending the BaseServiceImpl class and inject the appropriate DAO. This DAO must also be returned by the getDao method. Note that the service must be annotated with @Service, registering it as a Spring service.

By default, the methods of the service that manipulate data (basically, save and delete) are already annotated with the @Transactional annotation (from the Spring framework). If you add any methods yourself that also need an active transaction, you either have to mark these methods (in the service implementation class) as transactional. Alternatively, you can place the @Transactional annotation on the service implementation subclass in order to make all methods in that service transactional.

Commonly used service methods

The BaseService (and BaseDao) class offer several commonly used methods that should take care of the most basic data retrieval and storage needs:

  • long count() → return the number of entities in the database

  • long count(Filter filter, boolean distinct) → returns the number of entities that match the provided filter.

  • T createNewEntity() → creates a new entity

  • void delete(List<T> list) → Deletes a list of entities

  • void delete(T entity) → Deletes a single entity

  • List<T> fetch(Filter filter, FetchJoinInformation… joins) → fetches entities based on a filter (without a sort order)

  • List<T> fetch(Filter filter, SortOrders sortOrders, FetchJoinInformation… joins) → fetches entities based on a filter (with a sort order)

  • List<T> fetch(Filter filter, int pageNumber, int pageSize, FetchJoinInformation… joins) → fetches a page of data (without a sort order)

  • List<T> fetch(Filter filter, int pageNumber, int pageSize, SortOrders sortOrders, FetchJoinInformation… joins) → fetches a page of data (with a specified sort order)

  • T fetchById(ID id, FetchJoinInformation… joins) → fetches an entity and its relations based on a primary key

  • List<T> fetchByIds(List<ID> ids, FetchjoinInformation… joins) → Fetches a page of entities based on the IDs of the entities (without a sort order)

  • List<T> fetchByIds(List<ID> ids, SortOrders sortOrders, FetchjoinInformation… joins) → Fetches a page of entities based on the IDs of the entities (with a sort order)

  • T fetchByUniqueProperty(String property, Object value, boolean caseSensitive, FetchJoinInformation… joins) → Fetches an entity based on a unique property value.

  • List<T> find(Filter filter) → Finds a list of entities based on the provided filter

  • List<T> find(Filter filter, SortOrder… orders) → Retrieves a page data of data based on the provided filter.

  • List<T> findAll(Order… orders) → Retrieves all entities of a certain type. Use with caution

  • T findById(ID id) → Find an entity based on its primary key

  • T findByUniquePropertyId(String property, Object value, boolean caseSensitive) → Retrieves an entity based on a unique property value.

  • <S> List<S> findDistinct(Filter filter, String distinctField, Class<S> elementType, SortOrder… orders) → Searches for all distinct values that occur in a specific field

  • <S> List<S> findDistinctInCollectionTable(String tableName, String distinctField, Class<S> elementType) → Searches for all distinct values inside a collection table. Used by the framework when creating a search field for searching inside a collection table.

  • List<ID> findIds(Filter filter, SortOrder… orders) → Returns a list of IDs that match the provided filter, sorted according to the provided sort orders.

  • Class<?> getEntityClass() → Returns the class of the entity that is managed by this service.

  • T save(T entity) → Saves an entity

  • List<T> save(List<T> entities) → Saves a list of entities.

Fetching and paging

The Dynamo framework is built around the concept of fetching data (using fetch join queries) whenever possible. The philosophy behind this is that it is usually much faster to fetch all required data using a single query, rather than performing numerous smaller queries to achieve the same result.

For this reason, we recommend to keep the use of eager fetching to an minimum and use lazy fetching combined with fetch joins whenever possible.

The framework supports several methods that make it possible to fetch data based on a primary key or collection of keys, and also allow you to specify with relations to fetch as part of the query.

Note e.g. the following method defined in BaseService:

public T fetchById(ID id, FetchJoinInformation…​ joins);

As you can see, this method accepts a vararg parameter that specifies which relations to fetch. If left empty, the application will use the default setup, which you can specify by using the @FetchJoins annotation on an entity class.

@ToString
@FetchJoins(joins = {@FetchJoin(attribute = "countryOfOrigin"),

@FetchJoin(attribute = "mainActivity")},
detailJoins = {@FetchJoin(attribute = "countryOfOrigin"),
@FetchJoin(attribute = "neighbourhoods")})
public class Organization extends AbstractEntity<Integer> {

This means that whenever you perform a fetch query (for multiple entities) using a standard service method, and you do not explicitly specify which relations to fetch, all relations specified by the “joins” property will be returned.

When performing a query to fetch just a single entity, the detailJoins will be used instead.

The consequences of this is that the joins setting should normally contain the relations that you want to display in a results table, whereas the detailJoins should contain the relations that you want to display inside an edit form.

When declaring an @FetchJoin, you can specify the type of join. The default is LEFT JOIN which means that the entity will be returned even if the relation to fetch is empty. You can change this to INNER. This will often improve performance but only used this if it relation you are fetching is mandatory and thus always present.

Take great care not to include any substantially large relations, since this can lead to poor performance.

When you create a component that contains a tabular display of data, you can specify the way in which the data will be retrieved. There are two options here:

  • ID_BASED – As described above. The application will execute a query that will retrieve the primary keys of the entities to be displayed, followed by a query that fetches a number of these entities (and their relations) based on these primary keys and information about which relations to fetch.

  • PAGING – The application will first execute a query to determine the amount of entities, and will then use a paging query (using firstResults and maxResults) to retrieve a subset of the desired entities). This approach supports the fetching of associated relations, but take care that you must only fetch many-to-one or one-to-one relations in this fashion. This is because if you fetch one-to-many or many-to-many relations, the result set will contain multiple rows per entity, which clashes with the firstResults and maxResults settings and will cause the underlying ORM provider to retrieve the entire data set first and do the filtering in memory. This is often very inefficient.

In both cases, the grid is filled lazily – only a small subset of the available data will be retrieved. The best approach depends on the situation – if you have a large data set and no relations to fetch then paging is preferred. If you have a lot of relations to fetch (or if you must fetch any one-to-many or many-to-many relations), use the ID-based approach.

Validation

The validation facilities in the Dynamo Framework are based on the JSR 303 (Bean Validation) standard: to express validation rules, simply use the standard annotations (@NotNull, @Size, @Min etc.) on the properties of your entity.

You can also use @AssertTrue and @AssertFalse to express more complex (inter-field) validation rules, or write your own validations by implementing the ConstraintValidator interface. To use @AssertTrue or @AssertFalse, create a method on the entity class that returns a Boolean, then annotate that method with either of these annotations – during the validation process these methods will be executed and if the return value does not match the value expected by the annotation then a validation error will be reported.

Custom validation messages can be included in the ValidationMessages.properties message bundle.

When you save an entity (by calling the service method save), it is automatically validated against these validation rules, and an OCSValidationException will be thrown if any validations fail.

Also, note that when the application creates a default edit form, the appropriate validators are automatically assigned to each field based on the JSR 303 validation rules. So, if you enter a value of “1000” in a field that has “999” as the maximum value, a validation error message is automatically displayed.

If you need to perform any custom validations for a certain entity class, you can do so by overriding the validate method in the Service implementation class for that entity.

Checking for identical entities

There is one additional feature with regard to validation that we must mention here. In case you have an entity that contains a logical primary key (either a single field or a combination of fields) the framework provides an easy way to check for possible duplicates. To do so, you only have to override the findIdenticalEntity method from the BaseServiceImpl in your service implementation class.

This method takes an entity as its only parameter; inside the method body, you can perform any query to check if there already is an entity that has the same values for the unique field or combination of fields. If the method returns a non-null value, then the framework will throw an OCSValidationException as part of the validation process.

Consider the following example that checks if there already is an organization with the same name as the organization you are trying to save (which is passed as a parameter to the method):

@Override
protected Organization findIdenticalEntity(Organization entity) {
    return dao.fetchByUniqueProperty("name", entity.getName(), false);
}

Note that you do not have to check if the entity being returned is equal to the entity being validated, the framework will take care of this for you.

Filtering

Both the user interface and the service layer make use of a filtering mechanism to limit the result sets returned by certain queries (e.g. the count and find methods of the BaseService).

This mechanism is based on the predicate functionality provided by Vaadin: for every Vaadin data provider, you can add one or more filters (instances of com.vaadin.server.SerializablePredicate and its subclasses) which can be used to restrict the data set that is contained in it.

For convenience, the Dynamo framework includes a wide range of filters that extend from the base Vaadin class. These include:

  • PropertyPredicate → The base class for predicates that filter on a single property value

    • ComparePredicate → The base class for predicates that compare a single property value.

      • EqualsPredicate → for an exact comparison.

      • LessThanPredicate → for checking whether a property value is below a certain value.

      • LessOrEqualPredicate → for checking whether a property value is less than or equal to a certain value.

      • GreaterOrEqualPredicate → for checking whether a property value is greater than or equal to a certain value.

      • GreaterThanPredicate → for checking whether a property value is greater than a certain value.

      • IsNullPredicate → for checking whether a property value is null.

    • LikePredicate → for checking whether a String value matches a certain pattern.

    • SimpleStringPredicate → for checking whether a String value matches a certain pattern (more advanced than the LikePredicate).

    • InPredicate → for checking whether the property value occurs within a collection of values.

    • ContainsPredicate → for checking whether the property value contains a certain value.

    • ModuloPredicate → for checking whether the property value is equal to some value modulo a provided number

    • BetweenPredicate → for checking whether the property value is between an upper and lower bound (inclusive).

  • NotPredicate → The logical negation of another predicate.

  • OrPredicate → The logical disjunction of two or more predicates.

  • AndPredicate → The logical conjunction of two or more predicates.

Most user interface components in Dynamo that can be used to display a collection of data have the option to specify the Vaadin filters that are used to filter the data that is retrieved by the component.

In addition, some components offer the developer the option to specify a fieldFilters map. This map contains key/value pairs that map an attribute name (from the Entity Model framework) to a Vaadin filter. This mechanism can be used to restrict the values that appear in e.g. a combo box or a lookup field inside an edit form or search form.

Default Services and DAOs

It can happen that you have a very simple entity for which you will only need the default methods provided by BaseService*. If this is the case, then you do not have to go through the trouble of creating a DAO and Service class. Instead, you can configure a *DefaultServiceImpl and/or DefaultDaoImpl in a configuration class. This looks as follows:

@Bean
public BaseDao<Integer, Region> regionDao() {
    return new DefaultDaoImpl<Integer, Region>(QRegion.region, Region.class);
}

@Bean
public BaseService<Integer, Region> regionService(BaseDao<Integer,
Region> regionDao) {
    return new DefaultServiceImpl<Integer, Region>(regionDao, "code");
}

As you can see, you can configure a bean that is an instance of DefaultServiceImpl and supply the necessary arguments to the constructor. This includes:

  • An instance of *DefaultDaoImpl*. This in turn has two (or three) constructor arguments, namely:

    • The QueryDSL base class (the QEntity class)

    • The entity class.

    • Optionally, the names of the properties to fetch when performing a fetch query (these will always be fetched using a left join).

  • Optionally, the name of the properties for which the values must be unique. You can use a comma-separated list to specify multiple properties, e.g. “code,name” means that both the “code” and “name” properties must be unique.

  • Optionally, a boolean parameter that indicates whether the search for the unique value is case sensitive (defaults to false).

After you have configured a service like this, you can inject it into your code as follows. Note that an @Qualifier annotation that matches the name of the bean is required:

@Autowired
@Qualifier("countryService")
private BaseService<Integer, Country> countryService;

Project Setup

Project structure

By default, projects created using the Dynamo Framework consist of a root project (with a root pom) that contains three sub-projects: domain*, *core and *ui*:

  • The domain subproject contains the domain classes.

  • The core subproject contains the service and business logic classes.

  • The ui project contains the user interface.

Each subproject follows the default structure of a Maven project and thus has four source folders:

The following directory structure shows how the projects are organized:

  • domain

    • src/main/java

    • src/main/resources

      • META-INF

        • entitymodel.properties the message bundle used to customize the entity model generation process.

      • application.properties contains the application properties (Spring Boot file).

      • ui.messages.properties the message bundle used for internationalization.

      • ValidationMessages.properties the message bundle used for configuring Bean Validation error messages

    • src/test/java

    • src/test/resources

  • core

    • src/main/java

    • src/main/resources

    • src/test/java

    • src/test/resources

  • ui

    • src/main/java

    • src/main/resources

    • src/main/webapp → you can place any image files in the frontend/images directory.

    • src/test/java

    • src/test/resources

    • frontend/styles → contains custom CSS files

Message bundles

A Dynamo application uses a number of message bundles (see the tree in the previous section for information on where these bundles are located). These message bundles are made available to the Spring Framework and you can retrieve a message from them using the MessageService which is a Spring-managed singleton bean that you can inject into your services. Note that many standard components already have a reference to this *MessageService*.

The message bundles used in the application all serve different purposes:

  • ui.messages.properties is the message bundle that must be used for all text messages that actually appear on your screens. E.g. if you want a button to show the text “Click me” then you could include a message like mybutton.caption=Click me in the message bundle and then use the messageService.getMessage(“mybutton.caption”) method in order to retrieve the message. Note that you can use the standard features of the Java message bundle mechanism to provide internationalization, e.g. you can create a message bundle “messages_fr.properties” and fill that with the French translations of your messages. This bundle will then be picked up if the locale of the application is set to French.

  • ValidationMessages.properties is the message bundle used configuring Bean Validation error messages. You can refer to messages from this bundle in the following way. Suppose that in your bundle you have the following message:

product.aliases.different=Duplicate product aliases found. Please make
sure they are all different

Then in the code you can refer to this message by placing the message name within curly brackets:

@AssertTrue(message = "{product.aliases.different}")
*public* *boolean* isAliasesDifferent() {
  • entitymodel.properties is the message bundle that is used to override the default behaviour of the Entity Model factory. See section 2.5 for instructions on how to use this override mechanism.

  • menu.properties is the message bundle that is used to configure the structure of your menu. It contains both the textual descriptions of menu items, and the structure of the menu. More on this can be found in chapter 8.

The MessageService provides a number of methods for retrieving messages. Some of these are used internally by the framework and should not normally be used directly. The following methods are intended for developers:

  • getMessage(String key, Locale locale) retrieves a message based on its key, using the specified locale. If no message is found, then a warning message will be returned.

  • getMessage(String key, Locale locale, Object… args) retrieves a message based on its key, using the specified locale, and using the specified parameters. If the message contains placeholders ({0}, {1}, {2} etc.) these will be replaced by the provided parameters.

If a message with a certain key cannot be found, then a default warning message will be returned. If you do not want this behaviour, you can use the getMessageNoDefault version of the method instead. This version returns null when a message cannot be found.

Spring Boot configuration

A Dynamo application uses Spring boot as its application framework. This means that there must be a main class that is annotated with @SpringBootApplication.

@SpringBootApplication
@Import(ApplicationConfig.class)
public class GtsApplication {

    public static void main(String[] args) {
        SpringApplication.run(GtsApplication.class, args);
    }

}

As you can see, we use the @Import annotation to import the main configuration file. Strictly speaking this is optional (you can add the configuration directly in the main class) but using this setup allows you to use a base class from Dynamo that will take care of instantiating the basic Dynamo components.

@ComponentScan(basePackages = { "com.ocs.gts", "com.ocs.dynamo" })
@EntityScan(basePackages = { "com.ocs.gts.domain", "com.ocs.dynamo.functional.domain" })
public class ApplicationConfig extends ApplicationConfigurationSupport {

    @Override
    protected String[] getBaseNames() {
        return new String[] { "classpath:/META-INF/entitymodel", "classpath:/menu", "classpath:/ui.messages", "classpath:/ocscommon",
                "classpath:/ValidationMessages"};
    }

    @Bean
    @UIScope
    public UIHelper uiHelper() {
        return new UIHelper();
    }


    @Bean
    public BaseDao<Integer, Region> regionDao() {
        return new DefaultDaoImpl<>(QRegion.region, Region.class);
    }
}
  • Use @ComponentScan to specify which packages to scan for classes to be instantiated by Spring.

  • Use @EntityScan to specify which packages to scan for entity classes. Normally this will be your own package (in this example “com.ocs.gts.domain”). If you want to use the dynamo-functional-domain module you must also add “com.ocs.dynamo.functional.domain”.

  • Extends the ApplicationConfigurationSupport class to automatically instantiate the base components needed by Dynamo.

  • Implement the “getBaseNames” method to specify the all resources bundles that are used by the application.

  • Below this you can add your own bean definitions. If you use the dynamo-functional-domain module then this will likely including some BaseService and BaseDao declarations (see paragraph 6.7).

  • You must also include a bean of class UIHelper with scope @UIScope.

As a Dynamo application is a Spring Boot application, you can add or modify any properties by changing the application.properties file which should be located in the src/main/resources directory of the UI subproject. The properties specific to Dynamo will be covered in the section on system properties.

Dynamo does not provide any functionality with regard to login or security. You can use Spring Security to set up your desired security configuration, which is normally done by overwriting the WebSecurityConfigurerAdapter from Spring.

This is an example for Basic authentication with one in-memory user.

@EnableWebSecurity
@Configuration
public class GtsSecurityAdapter extends WebSecurityConfigurerAdapter {

    @Autowired
    private MyBasicAuthenticationEntryPoint entryPoint;

    @Autowired
    public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
        auth.inMemoryAuthentication().withUser("Dynamo").password("{noop}Dynamo").authorities("user");
    }

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        // disable CSRF (already provided by Vaadin)
        http.csrf().disable().authorizeRequests().anyRequest().authenticated().and().httpBasic().authenticationEntryPoint(entryPoint);
    }

    @Bean
    GrantedAuthorityDefaults grantedAuthorityDefaults() {
        return new GrantedAuthorityDefaults("");
    }

}

Most of this set-up is not specific to Dynamo, but a couple of things are worth mentioning:

  • We add a bean of type “GrantedAuthorityDefaults”, instantiated with an empty String. This removes the “ROLE_” prefix that Spring Security normally adds to role names.

  • We disable CSRF protection by calling http.csrf().disable(). This is not needed since Vaadin already provided this.

System properties

Dynamo several ways of ways of dealing with (system) properties.

The easiest way of declaring a property is by including it in the application.properties file which is located in the src/main/resources directory of the ui subproject. This is of course the standard file used by Spring boot, and as such you can add both your own system properties to it, as well as using it to modify any Spring Boot settings. You can use the default mechanisms offered by Spring Boot (e.g. external configuration file, explicitly set system parameters) to override the values in application.properties.

The Dynamo Framework relies on several pre-configured system properties:

Property Name Default Value Explanation

ocs.allow.list.export

false

Whether to allow the export of grid contents to Excel (or CSV) by right-clicking in the grid

ocs.capitalize.words

True

Indicates whether to capitalize all words that appear in captions. If set to “false” then only the first character will be capitalized.

ocs.default.clear.button.visible

False

Whether a “clear” button (displayed as a big gray X) will be rendered at the end of the input component (not all components support this).

ocs.default.currency.symbol

The default currency symbol to be displayed in currency fields

ocs.default.date.format

dd-MM-yyyy

The pattern to use when formatting date values

ocs.default.datetime.format

dd-MM-yyyy HH:mm:ss

The pattern to use when formatting timestamp values

ocs.default.datetime.zone.format

dd-MM-yyyy HH:mm:ssZ

The pattern to use when formatting timestamp values that include a time zone

ocs.default.date.locale

uk

The locale used for localization of date and time fields.

ocs.default.locale

uk

The default locale. This is used for number formatting and localization of messages and labels

ocs.default.decimal.precision

2

The default number of decimals to use when formatting/entering a decimal value

ocs.default.details.grid.sortable

False

Indicates whether the column in a DetailsEditGrid are sortable by default

ocs.default.edit.form.columns.thresholds

0px

Sets the thresholds for when to use multiple columns of input fields in an edit form.

If set to the default value of “0px” then edit forms will always just contain a single column.

If e.g. you set it to “0px,800px”, then a second column will appear on screens wider than 800 pixels.

ocs.default.edit.grid.height

200px

The default height of a grid that is included in an edit form (i.e. a DetailsEditGrid or ElementCollectionGrid)

ocs.default.false.representation

false

The default textual representation of a boolean “false” value

ocs.default.grid.height

400px

The default height of the grid that is used within composite layout components

ocs.default.group.together.mode

Pixel

The default “group together” mode to use. This specifies how to scale fields when they are grouped together. Supported values are pixel and percentage.

ocs.default.group.together.width

200

The default width of a field when it is grouped together width one or more other fields, in pixels

ocs.default.list.select.height

100px

Default height of list select component

ocs.default.lookupfield.max.items

3

The maximum number of selected items to display in the label of a lookup field

ocs.default.max.edit.form.width

100%

The default width of the edit form

ocs.default.max.search.form.width

100%

The default width of the search form (for SearchLayouts)

ocs.default.message.display.time

2000

The default time (in milliseconds) that an error or information message is displayed

ocs.default.number.field.mode

TEXTFIELD

The default field type to use for integer properties

Allowed values: TEXTFIELD, NUMBERFIELD

ocs.default.paging.mode

NON_PAGED

The default paging mode to use inside components that display a list of elements (combo box, token field select)

Allowed values: NON_PAGED, PAGED

ocs.default.search.case.sensitive

false

Whether to use case sensitive mode when searching on string fields

ocs.default.search.dialog.grid.height

300px

The default height of a results grid inside a search dialog grid

ocs.default.search.form.columns.thresholds

0px,650px,1300px

Sets the thresholds for when to use multiple columns of input fields in a search

If set to the default value of “0px” then edit forms will always just contain a single column.

If e.g. you set it to “0px,800px”, then a second column will appear on screens wider than 800 pixels.

ocs.default.search.prefix.only

False

Whether to only look for matches at the start of a string (rather than anywhere) when searching on sting fields.

ocs.default.text.area.height

200px

The default height of a text area component

ocs.default.search.case.sensitive

false

Whether searching on String values is case sensitive by default.

ocs.default.search.dialog.grid.height

300px

The default height of the results grid in a search dialog

ocs.default.search.prefix.only

false

Whether searching on String values only considers matches at the start (rather than anywhere in the String)

ocs.default.time.format

HH:mm:ss

The pattern to use when formatting time values

ocs.default.true.representation

True

The default textual representation of a boolean “true” value

ocs.default.thousands.grouping

NEVER

When to display a thousands grouping separator when editing or displaying numerical values

(allowed values are NEVER, ALWAYS, EDIT, VIEW)

ocs.enable.view.authorization

false

Whether to enable role-based authorization for UI views

ocs.export.csv.escape

“”

The escape character to use when exporting to CSV

ocs.export.csv.separator

;

The separator character to use when exporting data to CSV

ocs.export.csv.quote

The quote character to use when exporting data to CSV

ocs.indent.grids

True

Determines whether to indent any DetailsEditGrids and DetailsEditLayouts when the are used inside edit forms

ocs.trim.spaces

false

Whether to remove extraneous spaces at the start or end of the input in text fields and text areas

ocs.use.default.prompt.value

false

Whether to display a prompt/hint value inside input components in edit forms

ocs.use.browser.time.zone

false

Whether to use the time zone as retrieved from the user’s browser when formatting timestamps that contain a time zone. If this is set to false (default) the system time zone will be used instead.

ocs.use.grid.selection.checkboxes

true

Whether to use selection check boxes for multiple select functionality inside grids. If set to false, you can select multiple rows by using Ctrl- and Shift clicking.

You can use the static methods from the SystemPropertyUtils class to access these system properties from inside your code.

DatePicker Customization

It is possible to modify the way that the Date Picker component used for modifying values of type LocalDate, LocalDateTime and ZonedDateTime is localized. You can do this by including some messages in any of the message bundles used by your application.

The message keys are as follows:

Key Usage

ocs.calendar.days

Comma-separated list of the names of the days of the week, starting with Sunday

ocs.calendar.days.short

Comma-separated list of the short names of the days of the week, starting with Sunday (Sun)

ocs.calendar.months

Comma-separated list of the names of the months, starting with January

ocs.calendar.first

Index of the first day of the week. 0 = Sunday, 1 = Monday, etc.

Since this uses the standard Java message bundle functionality, you can have different settings for different languages/locales simply by using different message bundles, e.g. “messages_nl.properties” for Dutch, “messages_es.properties” for Spanish etc. You can set which locale to use for a certain user by calling the VaadinUtils.storeDateLocale method.

UIs, Views, Menus, and authorization

Basic UI setup

A Dynamo application is expected to have a single entry point. This is basically a class that implements the Vaadin RouterLayout interface and defines the basic page structure. A simplified version may look as follows:

UIScope
@Theme(Lumo.class)
@CssImport("./styles/shared-styles.css")
@Viewport("width=device-width, minimum-scale=1.0, initial-scale=1.0, user-scalable=yes")
@CssImport(value = "./styles/vaadin-custom-field.css", themeFor = "vaadin-custom-field")
@CssImport(value = "./styles/vaadin-dialog-overlay.css", themeFor = "vaadin-dialog-overlay")
@CssImport(value = "./styles/vaadin-combo-box.css", themeFor = "vaadin-combo-box")
@CssImport(value = "./styles/vaadin-button.css", themeFor = "vaadin-button")
@CssImport(value = "./styles/vaadin-text-field.css", themeFor = "vaadin-text-field")
public class GtsUI extends VerticalLayout implements RouterLayout {

    private static final long serialVersionUID = -4652393330832382449L;

    private MenuBar menu;

    @Autowired
    private MenuService menuService;

    @Autowired
    private MessageService messageService;

    @Autowired
    private UIHelper uiHelper;

    @PostConstruct
    protected void init() {

        VaadinUtils.storeLocale(new Locale("nl"));
        VaadinUtils.storeDateLocale(new Locale("nl"));

        // construct your title bar

        // construct the menu
        menu = menuService.constructMenu("gts.menu");
        add(menu);

        menuService.setLastVisited(menu, Views.ORGANIZATION_VIEW);

        uiHelper.addEntityNavigationMapping(Organization.class, item -> {
            uiHelper.setSelectedEntity(item);
            uiHelper.navigate(Views.ORGANIZATION_VIEW);
        });

    }
}
  • The class must be annotated with @UIScope to specify that it is scoped to a single browser tab.

  • The class must include the Vaadin Lumo theme.

  • The @CssImport annotation is used to import several HTML and CSS files that are used to customize the default styling.

  • The method must implement the RouterLayout interface from Vaadin.

  • The actual screen is constructed in a method annotated with @PostConstruct. We start by storing the desired locale and date locale in the session (more on this below) and then construct the application menu. Note that you can add your own code for constructing e.g. a title banner before the menu is added.

  • Inside the method annotated by @PostConstruct you should construct the part of the page that is shared between all pages in your application. When creating specific pages, you will refer to this base class to ensure that each specific page includes the generic part.

Note that you must not subclass the UI class from Vaadin – this seems like a logical thing to do but does not work properly.

Views

A screen in Vaadin was traditionally represented by a View. In the new version of Vaadin this class no longer exists and any user interface component can serve as the basis for a view. Dynamo provides the BaseView class which offers some common functionality, and you are encouraged to subclass BaseView to create your own views.

A view is configured as follows:

  • The @UIScope annotation is used to indicate that the view is scoped to a single browser tab.

  • @Route is used to indicate how the user can navigate to the page. It is recommended to store the available view names in a constants file. The “value” attribute refers to the relative path at which the view will be available. The “layout” attribute should point to your class that contains the generic screen layout as described in section 8.1.

  • @RouteAlias can be used to mark a view as the default view. This is the view that will be opened when the user does not enter a relative path in the browser address bar. At most one view in your entire application may be annotated with @RouteAlias.

  • @PageTitle can be used to set the page title (that will be displayed in the user’s browser’s title bar)

  • The doInit method can be used to add your own code. Inside this method, you must construct your screen. You can add your own components to the VerticalLayout that is passed as the constructor argument (don’t forget this).

@UIScope
@Route(value = Views.ORGANIZATION_VIEW, layout = GtsUI.class)
@RouteAlias(value = "", layout = GtsUI.class)
@PageTitle("Organizations")
public class OrganizationView extends BaseView {

    @Autowired
    private OrganizationService organizationService;

    private static final long serialVersionUID = 3310122000037867336L;

    @Override
    protected void doInit(VerticalLayout main) {

        .. your code goes here
    }

}

Navigation

Navigating between the various views in your application is easy: you can use UI.getCurrent().navigate(viewName) to navigate to the desired view. Several components also provide a convenience method named navigate which directly navigates to this method.

The Navigator class which existed in previous versions of the Vaadin framework no longer exists.

View authorization

By default, all views inside a Dynamo application are accessible to all users. In order to protect one or more views, you must first ensure that two application properties are properly set:

ocs.enable.view.authorization=true
ocs.view.package=com.ocs.gts

ocs.enable.view.authorization makes sure the view based authorization is enabled. ocs.view.package must be set to the base package that contains your view classes. Any properly annotated classes in the specified package (or its sub-packages) will be protected by Dynamo’s authorization mechanism.

Under the covers, using these application properties makes sure that a number of Spring beans are autoconfigured. This includes:

  • The PermissionChecker that will scan the provided package for view classes that must be protected.

  • An AuthorizationServiceInitListener that will intercept any requests and redirect them to an error page when the user is not properly authorized.

You can then annotate a view as follows:

@UIScope
@Route(value = Views.PERSON_VIEW, layout = GtsUI.class)
@Authorized(roles = "admin")
public class PersonView extends BaseView {

	@Autowired
	private PersonService personService;

The above shows an example of a view definition. The view is annotated with @Route as we’ve seen before. It is also annotated with @Authorized. @Authorized can be configured with any number of roles and only a user that is in at least one of the provided roles is allowed to access the view.

If a user is not allowed to access a certain view, that view is excluded from the menu (see below) and if the user manually tries to navigate to that view then they will be taken to the error view instead.

Note that for this to work properly, you must create an appropriate error view in your application. This can looks as follows:

package com.ocs.gts.ui;

import com.ocs.dynamo.constants.DynamoConstants;
import com.ocs.dynamo.ui.component.DefaultVerticalLayout;
import com.ocs.dynamo.ui.view.BaseView;
import com.vaadin.flow.component.html.Label;
import com.vaadin.flow.component.orderedlayout.VerticalLayout;
import com.vaadin.flow.router.Route;
import com.vaadin.flow.spring.annotation.UIScope;

@UIScope
@Route(value = DynamoConstants.ERROR_VIEW, layout = GtsUI.class)
public class GtsErrorView extends BaseView {

    private static final long serialVersionUID = 3955677765990706688L;

    @Override
    protected void doInit(VerticalLayout main) {
        VerticalLayout main = new DefaultVerticalLayout(true, true);

        VerticalLayout inside = new DefaultVerticalLayout(true, true);
        main.add(inside);

        Label errorLabel = new Label(message("ocs.view.unknown"));
        inside.add(errorLabel);
        add(main);
    }

}

The error view should container an @Route annotation with a value of “errorView”. Other than that, it is up to you to determine what the view looks like.

Menu Construction

Based on the above, Dynamo also supports the declarative construction of a menu bar. For this, the MenuService is used.

The MenuService is a Spring-managed bean which contains a constructMenu method that you can use to construct the menu bar. The menuService is configured using the menu.properties message bundle:

gts.menu.1.displayName=Top Level Menu
gts.menu.1.1.displayName=Organizations
gts.menu.1.1.destination=organizationView
gts.menu.1.2.displayName=Members
gts.menu.1.2.destination=personView
gts.menu.1.3.displayName=Gifts
gts.menu.1.3.destination=giftView
gts.menu.2.displayName=Deliveries
gts.menu.2.destination=deliveryView

When calling the constructMenu service, you must pass along a prefix that will be used to retrieve the messages from the bundle. In the above case, the prefix is gts.menu which means that the menu will be constructed based on the messages starting with this prefix. You can include multiple menu structures in the same “menu.properties” file and use this prefix to determine which menu to construct and to switch menus on the fly.

The following code suffices to create a menu and add it to the UI (main refers to the main layout of the UI):

menu = menuService.constructMenu("gts.menu");
main.add(menu);

You can switch the menu bar at any time by using Vaadin’s support for replacing components (“menu” refers to a field on the UI that holds the reference to the current menu).

MenuBar other = menuService.constructMenu("gts.other", getNavigator());
main.replace(menu, other);
menu = other;

The MenuService will iterate over the messages, starting at “gts.menu.1”, followed by “gts.menu.2”, “gts.menu.3” etc., and try to recursively build a menu for each message. Child items can be specified by including additional numbers, e.g. “gts.menu.1.1” is the first child item of the first top-level menu item. The numbering must always start at 1 and must be continuous (you are not allowed to skip any numbers). Menu items can be nested arbitrarily deep, e.g. “gts.menu.1.4.3.2” would be valid provide that you do not skip any of the intermediate levels).

For every menu item, you can specify the following:

  • displayName: this is the name of the menu item (the text that will appear in the menu). It is required

  • destination: this is the name of the view to which the application will navigate after the user selects the corresponding menu item. It is an optional value. If you do not specify a destination, then the menu item will not be clickable, and only serve as a container for child menu items.

  • description: This is the value that will be used as the tool tip of the menu item. It is optional. When omitted, no tool tip will be shown.

  • mode: This is an additional parameter that can be supplied to specify a certain mode for a view. When this value is specified, it will be placed in the screenMode field on the application’s UIHelper (more on this below). You can retrieve this value inside your View and use it to construct your screen in the correct mode.

The menu service will use the above information to build a menu as follows:

  • It will find the message gts.menu.1.displayName and use it to create a top-level menu item named “Top Level Menu”.

  • It will see that this menu item has children (gts.menu.1.1, gts.menu.1.2, and gts.menu.1.3) and add three menu items named “Organizations”, “Members” and “Gifts” to the top-level menu item. All three items are clickable.

  • It will then return to the top level, find the gts.menu.2.displayName message and use this to construct a top-level menu item named “Deliveries”, which is clickable and lead to the view named “deliveryView”.

Any menu items will automatically be hidden or shown based on the @Authorized annotations placed on the views they correspond to. So, if the framework detects that a menu item leading to the “personView” must be included, but the user does not have the “admin” role needed to access that view, then the menu item will be hidden automatically. Any parent items that do not have any visible children will also be hidden.

If you need to programmatically modify the visibility of a menu item, you can call the setVisible method on MenuService.

The framework will also by default highlight the top-level menu item that leads to the last visited view – If you need to change this programmatically you can use the setLastVisited method of the MenuService to highlight a different top level menu item.

Vertical menu

New in Dynamo v2.10 is the option to add a vertical menu bar. This works in nearly exactly the same way as the regular horizontal menu bar, except that you need to insert a reference to the VerticalMenuService in your code. You can then construct the menu in much the same way:

Accordion verticalMenu = verticalMenuService.constructMenu("blip.menu");

The component returned by this method (the Accordion) can be added anywhere in your layout to create a menu that displays the various options below each other. Note that since this menu takes up a lot of vertical space, it’s not generally feasible to add it to the general UI class (the class that is implementing the RouterLayout) but you can add it to the individual layouts pretty easily.

View scoping

A Spring Vaadin View can have several scopes that govern how long the view sticks around – it can be newly created every time it is accessed (@ViewScope), persist while the UI persists (@UIScope) or persist as long as a Vaadin session exists.

In general, to get the most out of the Dynamo Framework we suggest creating views that have UI scope – this prevents the application from having to recreate the view on every request and allows you to e.g. save state (like which search filters were entered) in between subsequent visits to the same page.

Using @UIScope also ensures that the application will work across multiple browser tabs: each browser tab that the user opens within the same session gets its own Vaadin UI instance and can be used independently. Note that for this to work, you must not yourself store any tab-specific data in the Vaadin Session – the Vaadin session is shared between tab sheets so this would cause unwanted interference. For this reason, the Dynamo Framework also stores any state that needs to be preserved between views on the BaseUI.

Note that application state that can be shared across multiple browser tabs (e.g. a locale setting or a user name) can still be stored this on the Vaadin session.

Preserving search filters during navigation

By default, the Dynamo framework preserve search filters between page navigations. By this we mean that if you enter some search terms in a search form, execute a search, and then navigate to a different page and then back again to the original page, any search terms that you entered before will still be present. If you want to disable this behaviour for a certain layout, you can use the FormOptions.setPreserveSearchTerms to false for that screen.

Sharing state between views

Dynamo comes with some basic functionality for staring state between views. This can e.g. be used when selecting an entity in one screen and then navigating to a different screen that must display the data about that entity.

This state is stored in the UIHelper class which can be injected into your views or accessed by calling the getUIHelper method inside your components.

The UIHelper offers methods for storing the following:

  • A selectedEntity – used for storing an entity

  • A selectedTab – used for storing the index of a tab. This can be used to select a specific tab when opening a screen that contains multiple tabs

  • A screenMode – used for storing a screen mode. This can be used to open a view in a certain state or mode.

These methods on the UIHelper are used internally by the framework but you can use them programmatically as well. We already saw an application where the screenMode can be set when the user clicks on a certain menu item. Another example occurs when you mark a property of an entity as navigable. When you do this, you must also provide an EntityNavigationMapping as follows:

uiHelper.addEntityNavigationMapping(Organization.class, item -> {
  uiHelper.setSelectedEntity(item);
  uiHelper.navigate(Views.ORGANIZATION_VIEW);
});

This ensures that the application knows which view to navigate to (and potentially which other actions to take) when the user clicks on a link for that entity in e.g. a grid.

The code above only ensures that the state is shared and that navigation occurs, but it will not make sure the selected entity is displayed correctly. This is because Dynamo does not make any assumptions about the structure of your views, so you must write the code for this yourself. Fortunately, normally this is not very hard.

E.g. you can place the following code at the bottom of your view to check if an entity has been set on the UIHelper, and then navigate to the detail screen if needed.

if (getUiHelper().getSelectedEntity() instanceof Organization) {
  Organization org = (Organization) getUiHelper().getSelectedEntity();
  org = organizationService.fetchById(org.getId());
  layout.edit(org);
  getUIHeler().clearState();
}

The clearState method removes any state stored in the UIHelper. It is recommended to use this to clear up after yourself.

Asking for confirmation before navigation

It is possible to configure the framework to ask for confirmation before navigating away from a page. To do so, you must construct the view and pass “true” to the constructor parameter confirmBeforeLeave.

You can then overwrite the isEditing method to indicate when the screen is in edit mode. In most cases you can simply delegate to the isEditing method of the underlying composite component.

Note: the check for whether something is currently being edited is a bit simple, it merely checks if the component is currently displaying an edit form, if the edit form opens in read only ode, and if this edit form is currently in edit mode. If all conditions are true then the isEditing returns true.

Localization

By default, the Dynamo framework uses the value of the system property ocs.default.locale as the locale for localization. If you want to modify the locale on a user-by-user basis, you can use the VaadinUtils.storeLocale method to store a different locale in the user’s session:

E.g. the following will make sure that the locale from the user’s browser is used:

VaadinUtils.storeLocale(VaadinRequest.getCurrent().getLocale());

Note that for date field (i.e. for displaying month names), Dynamo uses a separate locale setting. By default, the value of the system property ocs.default.date.locale is used for localization in date components. You can use the VaadinUtils.storeDateLocale method to set a different locale.

In order to localize your application (e.g. to modify the contents of fixed text labels) you can use the standard Java message bundle mechanism. E.g. if you put your messages in the ui.messages.properties file then you can include French translations in ui.messages_fr.properties and Spanish texts in ui.messages_es.properties

It is also possible to localize the values used in the entity and attribute model, e.g. the displayName, displayNamePlural, description, etc.

In order to do so:

  • Create a file named entitymodel_<loc>.properties where <loc> is the name of the locale e.g. “fr” or “es” in the same directory that already contains the entitymodel.properties file. You can also use constructs like “entitymodel_nl_NL.properties” for the variant of Dutch spoken in the Netherlands, or “entitymodel_en_US.properties”.

  • Inside this file, place the desired translations in the same way as you would include regular message bundle overrides. For example, these are some lines that could be included in entitymodel_es.properties

Organization.displayName=Organización
Organization.displayNamePlural=Organizaciones
Organization.description=Una empresa criminal
Person.displayName=Persona
Person.displayNamePlural=Personas
Organization.name.displayName=Nombre

Dynamo uses the following algorithm to determine the appropriate value: * First, look if the appropriate message can be found in entitymodel_<locale>.properties * If this is not the case, revert to the generic entitymodel.properties * If no message can be found in this file, revert to the value declared using the @Attribute annotation. * Finally, use the calculated default value.

Localization is supported for the following settings:

Entitymodel:

  • displayName

  • displayNamePlural

  • description

AttributeModel

  • displayName

  • description

  • prompt

  • trueRepresentation

  • falseRepresentation

For the description and prompt setting on the AttributeModel, the algorithm is slightly different:

  • First, look if the appropriate message can be found in any of the message bundles.

  • If no message is found, look for the message for the displayName instead (since the description and prompt default to the displayName if not explicitly defined)

  • If no message can be found in this file, revert to the value declared using the @Attribute annotation.

  • If all else fails, use the calculated default value.

Composite UI components

Everything you have read until now is an introduction that is needed to understand how Dynamo can be used to create complex screens and user interface components. The next sections will cover some of the most common use cases.

SimpleSearchLayout

The Dynamo framework offers the SimpleSearchLayout component for creating search screens. A basic application of this SimpleSearchLayout looks as follows:

EntityModel<Organization> em = getModelFactory().getModel(Organization.class);
FormOptions fo = new FormOptions().setAttributeGroupMode(AttributeGroupMode.TABSHEET).setShowIterationButtons(true);
SimpleSearchLayout<Integer, Organization> layout = new      SimpleSearchLayout<>(organizationService, em, QueryType.ID_BASED, fo,
            new SortOrder<String>("name", SortDirection.ASCENDING));

As you can see, you create a new instance of the SimpleSearchLayout and declare both the class of the primary key of the entity and the class of the entity itself as type parameters. You then provide the following:

  • The Service that is used to retrieve data from the database.

  • The Entity Model that is used to construct the layout.

  • The type of the query: this can either be ID_BASED or PAGING as described in section 6. Please remember the restrictions that apply when using PAGING as the query type (i.e. you shouldn’t fetch any one-to-many or many-to-many relations).

  • A FormOptions parameter object. This will be explained below.

  • The default sort order (optional).

  • The relations to fetch – this is a vararg parameter and thus optional. It can be used to specify which relations to fetch when executing the search query. If left empty, then the default joins defined in the data access object associated with the entity will be used.

And that is basically all the code you need to create a working search screen.

Note that the Entity Model that you have defined before will be used as the template for creating the screen. I.e. all properties that are marked as searchable will be present in the search form, all properties that have visibleInGrid set to true will show up in the results grid, etc.

By default, the SimpleSearchLayout comes with an Add button that will bring up a form that allows you to define a new entity, and a View/Edit button that allows you to inspect the details of an entity after you have selected a row in the grid.

You can use the FormOptions object to specify the behaviour of the screen. The FormOptions class is a parameter object that is used to prevent excessively long lists of parameters. It uses the builder style and has several methods that can be used to toggle the availability of certain options (covered below).

E.g., by default any detail screen will open in edit mode, but if you don’t want this behaviour you can set the openInViewMode parameter to true. If this parameter is set, the detail screen will be read-only after being opened.

Some commonly used settings of the FormOptions class includes:

  • attributeGroupMode – this is used to specify how attribute groups will be displayed: the value PANEL means that a separate panel will be created for every attribute group, with the panels being placed below each other, and TABSHEET means that a tab sheet component will be rendered that contains a separate tab for each attribute group.

  • complexDetailsMode – When this is set to true, instead of displaying a “simple” detail screen that allows you to only edit a single entity, a lazy tab layout with multiple tabs will be rendered. This is explained in more detail below.

  • confirmClear – If this is set to true the user will be asked for confirmation if the user presses the Clear button to clear all search filters in a search form.

  • confirmSave – If this is set to true the user will be asked for confirmation before changes are saved in response to clicking on the Save button in an edit form.

  • detailsEditLayoutButtonsOnSameRow – this is used to specify whether the buttons in a DetailsEditLayout that is nested in an edit form will appear on the same row as the edit components. If set to false the buttons will be displayed on a separate row below the edit components.

  • detailsGridSearchMode – Used on a DetailsEditGrid to specify that it must be used in search mode. In this mode, you can bring up a modal search dialog for adding entities to the grid.

  • detailsGridSelectionMode – Used to set to grid selection mode (SINGLE or MULTIPLE) of a details grid. Normally, you will want to keep this at the default value SINGLE, but setting this to multiple will allow you to select multiple rows at a time.

  • detailsModeEnabled – Indicates whether it is possible to navigate from the search mode of a SearchLayout to the details view (the part that shows the details of a single selected entity). Note that this is different from setting showEditButton to false – setting editAllowed to false ensures there is still a detail view but it cannot be used in edit mode. Setting detailsModeEnabled to false completely turns off the detail view.

  • doubleclickSelectAllowed – Specifies whether the user can navigate to a detail screen in a SearchLayout by double-clicking on a row in the results grid. This is enabled by default.

  • editAllowed – specifies whether the Edit button will show up above/below and edit form when the screen is in view mode. Set openInViewMode to true and this setting to false if you want to disable editing for a component.

  • enableAdvancedSearchMode – by setting this to true, you enable the “advanced search mode” which means that you can toggle between the simple and advanced search mode. Attributes with search mode set to ADVANCED will only show up when the screen is in advanced mode.

  • exportAllowed – This specifies whether it is allowed to export the data in a grid to Excel or CSV by right-clicking on the grid.

  • exportMode – This specifies the desired export mode. The possible values are FULL (exports all visible attributes) or ONLY_VISIBLE_IN_GRID (exports only attributes that are visible in the grid). The default value is ONLY_VISIBLE_IN_GRID.

  • gridEditMode – This specifies the desired edit mode inside an EditableGridLayout (see below). The possible values are SINGLE_ROW (edit one row at a time using an in-line editor) and SIMULTANEOUS (edit multiple rows at once).

  • openInViewMode – By default, any forms used for editing entities will be shown in edit mode. If you set openInViewMode to true, then the edit form will open in view mode and you need to click the Edit button to put the form in edit mode.

  • placeButtonBarAtTop – If set to true, the application will display the button bar (containing Save, Cancel, Edit buttons etc.) above of the title label (rather than behind it) inside the edit form. This is true by default.

  • preserveAdvancedMode – this setting is used to specify whether the “advanced search mode” should be preserved when navigating away from a search screen and then back. It defaults to true.

  • preserveSearchTerms – this setting ensures that any search terms are preserved when navigating away from the screen and then back. By default it is set to true.

  • preserveSelectedTab – This specifies whether the selected tab will be preserved when switching between entities (by using the Previous and Next buttons) in the detail view of a SearchLayout.

  • preserveSortOrder – specifies whether the sort order will be preserved and restored when the navigating away from a screen and then returning.

  • screenMode – Switches between displaying components next to each other or below each other (for the various SplitLayout components).

  • searchImmediately - The option to search immediately when opening a search screen (as opposed to waiting until the user presses the search button for the first time). This is true by default. If disabled, the user will see a label indicating that he/she must perform a search rather than a search results grid.

  • showAddButton – Use this to show or hide the Add button that can be used to create new entities.

  • showCancelButton - The option to hide or show the Cancel button when in edit mode (shown by default after switching to edit mode, after the screen has been opened in view mode).

  • showClearButton – Used to show or hide the Clear button that shows up below the search fields in a search form.

  • showEditFormCaption – Whether to display a caption (that lists the name/description of the thing you are editing) above an edit form. This defaults to true.

  • showDetailsGridDetailsPanel – Whether to display a panel containing the details of the selected row when a row is selected in a DetailsEditGrid.

  • showDetailsGridPopup – Whether to display a button that opens a pop-up to view the details of the row that is selected in a DetailsEditGrid.

  • showFormFillButton – This causes a button to show up that can be used to automatically fill an input form based on a Large Language Model (LLM). This is covered in more detail in section 11.2.

  • showNextButton – This will cause a Next button to be displayed in the button bar of the detail screen of a SearchLayout. This button can be used to navigate to the next entity in the search results.

  • showIterationsButtons – This is shorthand for showNextButton and showPrevButton and will toggle the appearance of the Next and Previous buttons.

  • showPrevButton – This will cause a Previous button to be displayed in the detail screen of a SearchLayout. This button allows the user to navigate to the previous entity in the search results.

  • showRefreshButton – Specifies whether to display a “refresh” button in the button bar of the details mode for a search layout. This button can be used to refresh the currently displayed entity without having to navigate away.

  • showRemoveButton - The option to hide or show the Remove button below the search results grid (hidden by default).

  • showToggleButton - The option to show a “show/hide” button inside a search form in SearchLayout that can be used to show/hide the search form (hidden by default).

  • splitLayoutSearchButton – Specify whether to include a search button above a split layout. This search button will bring up a search dialog that allows you to filter the results that show up in the grid. In addition to the search button, Dynamo will also render a clear button that can be used to revert to the default filtering.

  • startInAdvancedMode – Specifies whether the search form will be displayed in Advanced Mode (if available) when first opening a screen.

Finally, you can use the setReadOnly method as a shorthand for making sure that a screen always opens in read-only mode (under the covers this hides the appropriate buttons and sets openInViewMode to true).

The SimpleSearchLayout also supports disabling the Search button until all required search fields have been filled (the required search fields are those marked as requiredForSearching in the entity model) – if your search form includes any mandatory field to search on, the Search button will simply be disabled until values for all these mandatory fields have been filled in by the user.

SimpleSearchLayout offers some additional functionality:

  • The defaultFilters property can be used to set a list of filters that will be applied to every search query (even when the user leaves all search fields blank). Use this if you don’t want to search on all entities in the system, but only on a certain subset. These filters are always preserved, even after pressing the Clear button.

  • The addFieldFilter method can be used to apply additional filters to search fields that allow the user to pick from a set (notably, for attributes of type MASTER or DETAIL). E.g. suppose that you have a search field for attribute “country” that displays a list of countries. Say you only want to display countries whose name contains the string “ab” then you can add the following field filter:

layout.addFieldFilter("countryOfOrigin", new
LikePredicate<String>("name", "%au%", false));
  • detailJoins is used to pass along the relations that must be fetched when retrieving a single entity when displaying the details screen. If you specify a non-empty list of relations here it will be used when retrieving a single entity (e.g. just before that object is displayed in a details screen). If left empty, this property will fall back to the joins that are defined in the DAO implementation. Any joins that are passed to the constructor will be ignored when retrieving a single entity.

  • The addSortOrder method can be used to add additional sort orders (the first sort order can be passed to the constructor).

  • addFieldEntityModel can be used to set a specific entity model to be used for rendering a particular input component. This method takes two arguments: the name of the attribute and the reference used to identity the entity model. This method can e.g. be used if you want to modify which fields show up inside a pop-up dialog in a lookup component (if you don’t specify anything, the default entity model for the entity to search for will be used, which will probably result in many more search fields than will fit inside the pop-up).

  • setMaxResults can be used to specify the maximum number of allowed search results. If the number of search results exceeds this value, then the result set will be truncated after the specified amount and the user will be shown a warning message.

You can also use the setSearchValue methods to explicitly set a predefined search value for a certain search field.

By default, the component starts in search mode, meaning that an empty search form and a grid of search results are rendered. You can use the following methods to modify the mode:

  • reload will revert the component to search mode, and clear the search form

  • refresh will also revert the component to search mode, leave the search form intact, but refresh the search results. It will also refresh the contents of any lookup components in the search form.

  • search will carry out a search (using the currently entered search terms)

  • searchMode will revert the component to the search mode.

  • reloadDetails will refresh the detail screen (only useful when you are already in details mode).

In addition to this, the behaviour of the component can be modified by using callback methods. This is covered in a later section.

Complex Details Mode

Normally, when you select an entity in the results grid of a search layout, you will be taken to an edit screen that allows you to edit the selected entity. This generally should be sufficient, but sometimes it can also be useful to be able to edit or view additional entities that are connected to the entity being edited. E.g. when you select an organization you also want to have a tab that allows you to view and edit that organization’s members. This is where “complex details mode” comes into play. To enable it, set the “complexDetailsMode” property on the FormOptions to true.

Next, you must call the setDetailModeTabCaptions to set the caption of the individual tabs. Implicitly this will also specify the number of tabs.

layout.setDetailsModeTabCaptions(*new* String[] \{ "Organizations",
"Members" });

You can then use the addDetailTabCreator method to specify what the individual tabs must look like:

layout.addDetailTabCreator(0, (formOptions, isNew) -> {
    EntityModel<Organization> em2 = getModelFactory().getModel(Organization.class);
    return new SimpleEditLayout<Integer, Organization>(layout.getSelectedItem(), organizationService, em2,
            formOptions, new FetchJoinInformation("members"), new FetchJoinInformation("countryOfOrigin"));
});

This method takes two parameters: the zero-based index of the tab and a bifunction that takes the form options and a boolean that indicates whether a new entity is being created, and must return the component that is to be constructed (in the example above this is a SimpleEditLayout). You must call this method once for every tab you want to add.

Now, when you select a row in the search results grid, you will be taken to an edit screen that contains multiple tabs. Clicking on a tab will bring up the appropriate composite layout component.

If you want a title to be displayed above the tabs, you can use the setDetailsModeTabTitle to set it.

When you try to create a new entity using a search layout while in complex details mode, you will not immediately get to see all the tab sheets after clicking on the Add button. Instead, you will just see an edit form for creating the entity. This edit form is constructed by calling the TabCreator with index 0 so it’s identical to the first tab.

To display an icon in front of the title of tab, use the setIconCreator method.

FlexibleSearchLayout

As an alternative to using the SimpleSearchLayout you can use the FlexibleSearchLayout. This component is instantiated in almost exactly the same way as the SimpleSearchLayout but behaves differently: once you open the search screen you will see an empty search form and a button that allows you to add extra filters.

When pressed, this button will add a new filter row to the search form – this filter row will contain a combo box that allows the user to select the attribute they want to filter on. Selecting an attribute will then cause another combo box to appear, which can be used to select the type of filter (e.g. equals, greater than, etc.). When the user selects a value from this second combo box, an additional input component (or in some cases, two components) will show up that allows the user to enter the exact value(s) to filter on – the component that will be rendered here is the same as the component that would show up in the SimpleSearchLayout and can be changed by using the searchSelectMode and selectMode settings.

The only significant way in which the FlexibleSearchLayout behaves differently from the SimpleSearchLayout is with regard to mandatory search properties: when you create a FlexibleSearchLayout, the framework will automatically add a filter row to the search form for every property that is marked as requiredForSearching (and these filters cannot be removed).

SplitLayout

A SplitLayout is a layout that contains both a results grid and an edit form, displayed either next to each other or below each other (with the results grid on top). You can use the screenMode parameter on the FormOptions object that you pass to the layout’s constructor to set the desired orientation.

An example of a split layout in horizontal mode can be seen here:

image

Note that there are several variants of the SplitLayout:

  • ServiceBasedSplitLayout – retrieves the data to display by calling methods on the BaseService.

    • ServiceBasedDetailLayout – a service based split layout that keeps a reference to a parent entity that is typically used for filtering.

  • FixedSplitLayout – a split layout which is instantiated with a (fixed) collection of data to display (it doesn’t use the service to retrieve the data)

    • FixedDetailLayout – a fixed split layout that keeps track of a parent entity

The Detail variants that keep a reference to a parent entity are useful when you are displaying a collection of that data that depends on a parent object that has been selected before – imagine for example that you have an application that first lets you pick an organisation and then allows you to browse a list of members of that organisation – in this case you can use a ServiceBasedDetailLayout that uses the organisation as the parent object.

A simple example of a ServiceBasedSplitLayout looks as follows:

EntityModel<Person> em = getModelFactory().getModel(Person.*class*);

FormOptions fo = *new* FormOptions();

ServiceBasedSplitLayout<Integer, Person> layout = *new*
ServiceBasedSplitLayout<Integer, Person>(personService, em,
QueryType.ID_BASED, fo, *null);*

main.addComponent(layout);

You can specify the following parameters when creating the layout:

  • The service that is used to retrieve data from the database.

  • The Entity Model that is used to construct the layout.

  • The query type (see section 6).

  • A FormOptions parameter object.

  • A sort order (optional).

  • A list of relations to fetch when executing the search query (vararg).

By default, the data displayed by the component will not be filtered. If you want to apply a filter to the data, you can do so by using the setFilterCreator method.

Like the SimpleSearchLayout, the SplitLayout supports the use of field filters

The SplitLayout components support the use of the FormOptions parameter object. Of special interest is the screenMode setting, which allows you to specify whether to display the form in vertical mode (grid above the edit form) or horizontal mode (grid to the left of the edit form). The default value is *HORIZONTAL*.

For the ServiceBasedSplitLayout you can also set the showQuickSearchField property to true or false to specify whether a quick search field must appear above the results grid. The default value is false. If you set it to true, then the screen will contain a quick search field for searching on the main attribute of the entity. If this is not sufficient, you can use the setQuickSearchSupplyFilter method to define you own custom search filter. This method takes a function that translates the search term entered by the user to the desired search predicate.

As with the SearchLayout components, you can use the reload, refresh and reloadDetails methods to refresh/reload the component.

SimpleEditLayout

The SimpleEditLayout component can be used to display or edit a single entity. It is useful if you already have selected an entity that you want to display or edit, without having to first select it using a search screen or results grid.

Creating a SimpleEditLayout is similar to creating the other composite components and looks as follows:

EntityModel<Organization> em =
getModelFactory().getModel(Organization.class)

FormOptions fo = *new*
FormOptions().setOpenInViewMode(*true*).setEditAllowed(*true*);

SimpleEditLayout<Integer, Organization> editLayout = *new*
SimpleEditLayout<Integer, Organization>(getEntity(),
organizationService, em, fo);

You can pass the following parameters to the constructor:

  • The entity of which you want to display the details (can be null; in this case a new entity will be created automatically).

  • The Service that is used to retrieve data from the database

  • The Entity Model that is used to construct the layout.

  • A FormOptions object that specifies how the layout behaves.

  • A list of relations to fetch when executing the search query (vararg – defaults to settings on the DAO if omitted).

As there is no results grid in this layout, it is not possible to define a sort order or a query type for this component.

EditableGridLayout

The EditableGridLayout component can be used to display and edit a collection of entities inside a grid. The constructor for the EditableGridLayout constructor looks as follows:

*public* EditableGridLayout(BaseService<ID, T> service, EntityModel<T>
entityModel,

FormOptions formOptions, SortOrder sortOrder, FetchJoinInformation...
joins) {

*super*(service, entityModel, formOptions, sortOrder, joins);

}

This results in a grid in which items can directly be edited, added, and deleted.

You can use the GridEditMode setting on the FormOptions to switch between the two available edit modes:

  • SINGLE_ROW mode allows you to edit one row at a time. In this edit mode, the data in the grid is shown in read-only mode. You can click on a button in each row in the grid to open an in-line editor row that allows you to edit the selected row. The in-line editor contains a Save button that can be used to save just the changes to this row.

  • SIMULTANEOUS mode allows you to edit multiple rows at once. In this mode, all the rows in the grid are shown in edit mode, and a Save button will show up below the grid. When clicked, this button will save all the pending changes for all modified rows at once.

Please note that in SIMULTANEOUS mode a lot of input components might be have to be rendered at once. This can have consequences for the performance of the application. We advise using the SINGLE_ROW mode by default and only using the SIMULTANEOUS mode when you know that the amount of data that will be shown in the grid is limited.

image

It may be tricky to display components that take up more than a single line (e.g. TokenFieldSelect). We advise sticking to components that only take up a single line.

The gridSelectMode from the attribute model will be used to determine which component to render inside an editable grid.

Apart from these two grid modes, the EditableGrid also supports switching between view mode and edit mode. This is independent of the grid mode: if you set openInViewMode to true, the grid will (obviously) open in view mode first, with an edit button below it. If your press this button and the grid mode is SINGLE_ROW then the grid will still be in read-only mode afterwards, but you can use the buttons showing up at the end of the rows to put a single row in edit mode.

It can be a bit awkward to use multi-line components like token fields or list selects inside a grid. You can use the gridSelectMode setting on the @Attribute annotation to specify which type of component to use inside an editable grid. This overrides the value of the selectMode setting.

For an attribute of type String, Dynamo will always render a text field (rather than a text area) inside an editable grid. The value of the textFieldMode will be ignored in this case.

When you want to add a new entity to using the EditableGridLayout, you can do this (as usual) by clicking the Add button. This will bring up a dialog that can be used to create the new entity:

image

HorizontalDisplayLayout

The HorizontalDisplayLayout is a very simple component that simply displays the attributes of an entity using a horizontal layout. Only properties of type BASIC are displayed inside this component.

TabLayout

This is a component that is meant as an alternative to the standard Vaadin Tabs component, which immediately loads all tab sheets when it is first created. The TabLayout component improves upon this by only loading a tab when it is opened. Until then, it simply constructs an empty dummy tab. The TabLayout will also make sure that any tab is properly refreshed before being opened to ensure that the most up-to-date data is shown.

To use this component, you need to create an instance of it and then set a number of methods that define the captions, icons, and actual tab contents:

tabLayout = new TabLayout<Integer, ProgrammeSelection>(selection);

	 tabLayout.setDescriptionsCreator(index -> {
			switch (index) {
			case INDEX_PROGRAM_LIST:
				return message("blip.programme.list.description");
			case INDEX_PROGRAM_COUNT:
				return message("blip.programme.count.layout.description");
			default:
				return null;
			}
		});
		tabLayout.setIconCreator(index -> {
			switch (index) {
			case INDEX_PROGRAM_LIST:
				return VaadinIcon.LIST.create();
			case INDEX_PROGRAM_COUNT:
				return VaadinIcon.ABACUS.create();
			default:
				return null;
			}
		});
		tabLayout.setCaptions(new String[] { message("blip.programme.list"), message("blip.programme.count.layout") });
		tabLayout.setTabCreator(index -> {
			SerializablePredicate<Programme> filter = fsf.extractFilter() == null ? statusFilter
					: new AndPredicate<>(fsf.extractFilter(), statusFilter);

			switch (index) {
			case INDEX_PROGRAM_LIST:
				return constructProgrammeListLayout(() -> fsf.extractFilter() == null ? statusFilter
						: new AndPredicate<>(fsf.extractFilter(), statusFilter), tabLayout.getEntity());
			case INDEX_PROGRAM_COUNT:
				return constructProgrammeCountLayout(filter);
			default:
				return null;
			}
		});

These methods mostly follow a similar structure: they take a lambda function as an argument – the lambda in turn takes the input of the tab as the argument and return a component, icon or String. The exception is the “setCaptions” method which is just used to define the captions of the individual tabs (and implicitly, the total number of tabs).

Note that whenever a tab is opened after the first time, the framework automatically reloads the contents of that tab – if the tab contains any of the standard Dynamo components, this will mean that they will be reloaded automatically.

If you want to create a tab that contains custom content, you can have your custom component implement the Reloadable interface and put any code that refreshes the component inside the reload method.

ProgressForm and UploadForm

ProgressForm is a component that can be used for executing (synchronous) batch processes. It comes with a built-in progress bar that can be used to give the user feedback on the status of the batch process.

Note that you can set the mode of the form: it can either be SIMPLE (no progress bar is used) or PROGESSBAR (a progress bar is used to keep track of the progression)

It its most basic form, the ProgressForm is simply a class that offers some basic method to construct a form:

  • The buildMain method can be used to build the form. It can contain UI components that can be used to modify the parameters of the batch process, or one or more buttons to start the process. Note that by default the ProgressForm does not contain a button to start the job.

  • The estimateSize method can be used estimate the size of the data that is being processed. You can use any means necessary in order to arrive at an estimate. This method does not do anything in case the mode of the progress form is set to SIMPLE.

  • The startWork method must be called to start the batch process. Typically, this is the method that you will call from e.g. a Button’s ClickListener.

  • The setProcess method is the method that contains the actual logic that must be called to carry out the batch process. Typically, you will want to delegate to a service method here.

  • If you need to carry out any validations before starting the process, call the setIsFormValid method an add any required checks. Return false from this method if you want to abort the processing.

Under the covers, the progress form will use the prepare method to spawn two threads: one thread in which the actual processing is carried out and one thread that will periodically poll for the progress of the job and update the progress bar. Once the process starts, the component will automatically switch to the progress bar view, and once it is done it will switch back to the form view. You can use the afterWorkComplete method to carry out an action after the job completes.

By default, the ProgressForm uses a ProgessCounter to keep track of the progress – if you want to use this mechanic, you can pass it along to the code that performs your batch processing and increment it whenever a step of your processing is done. The polling thread will use this to determine the completion rate of the job.

UploadForm is a subclass of ProgressForm that can be used to easily create a file upload form – it already contains a file upload component that will kick off the batch process once the file has been uploaded.

DetailsEditGrid

The Dynamo framework contains built-in support for managing many-to-many relations (to already existing entities) in edit forms (a token field component will be rendered by default) but this does not cover the situation in which you have an object that contains a one-to-many relation in which the detail elements are an integral part of the entity being edited. A classic example of this would be the creation of a new order and the order lines that make up the order: the order lines do not exist on their own but are an integral part of the order.

To be able to create both an entity and its related sub-entities, you can use a *DetailsEditGrid*. This is a component that you must initialize explicitly.

You can do so by calling the addCustomField method on the component to which you want to add the DetailsEditGrid.

This looks as follows:

addCustomField("translations", context -> {
    FormOptions fo = new FormOptions().setShowRemoveButton(true).setDetailsEditLayoutButtonsOnSameRow(false)
            .setDetailsGridSearchMode(false);

    EntityModel<GiftTranslation> model = getEntityModelFactory().getModel(GiftTranslation.class);
    DetailsEditGrid<Integer, GiftTranslation> dt = new DetailsEditGrid<>(model, context.getAttributeModel(),
            context.isViewMode(), fo);
    dt.setService(giftTranslationService);

dt.setCreateEntity(() -> {
        GiftTranslation translation = new GiftTranslation();
        translation.setDescription("desc");
        getSelectedItem().addTranslation(translation);

        return translation;
    });
    return dt;
});
  1. The DetailsEditGrid, like many composite components, takes two type parameters: the type of the primary key of the entity and the type of the entity itself.

  2. The constructor takes a FormOptions parameter object which you can set to e.g. specify if a Remove button must be shown. By default, an Add button is shown but the Remove button is hidden.

  3. Finally, the constructor takes a Boolean viewMode parameter. You can simply pass along the parameter from the context or set your own value.

  4. The setCreateEntity method can be used to initialize a new entity. It is called after the user clicks the Add button below the grid.

  5. Likewise, the setRemoveEntity method can be used to remove the detail object from the parent entity in response to a click on the Remove button.

Note that it is also possible to use the DetailsEditGrid as a lookup component for a many-to-many relation. You can do so by setting the detailsGridSearchMode on the FormOptions parameter object to true:

FormOptions fo = new FormOptions().setDetailsGridSearchMode(true);

This will display the DetailsEditGrid in read-only mode and will also add a Search button below the grid that will bring up a dialog that allows you to select multiple (already existing) entities to add to the grid/couple to the entity being edited. Note that in this mode it is not possible to manually add new entities – you are looking up and coupling existing entities rather than creating new ones. As such, the setCreateEntity method is pointless in this mode. However, in this scenario you must use the setLinkEntity method which must contain the code that is required to link an item that was selected in the search dialog to the entity that is being edited in the edit form.

If the collection that you are using to store the entities that are displayed in the grid does not allow duplicates (e.g. it is a Set), you have to provide appropriate implementations of hashCode and equals in your entity class that ensure that objects are never considered equal if they have an ID that is null – without this measure, you can’t (properly) add multiple entities at the same time without saving the changes in between.

ServiceBasedDetailsEditGrid

In the case that you want to edit an 1-to-N relationship or N-to-N relationship inside an edit form but the number of coupled entities is potentially very large (e.g. the relation from a country to its inhabitants), the approach described above using the DetailsEditGrid is no longer sufficient. Instead, you can use the ServiceBasedDetailsEditGrid which will lazily load the coupled entities when needed.

Note that in this case, there must not be a JPA relation modelled in your Java code, so we need to employ some tricks in order to get an edit component to show up.

You can start by adding a property like this to your (parent) entity:

@Attribute(visibleInGrid = VisibilityType.HIDE)
public Integer getOrganizationId() {
	return id;
}

This is simply a getter that has the same value as the entity’s primary key. It is simply used as a hook for starting the custom field construction.

Next, you can use the following code to instantiate the custom component inside the addCustomField method:

ServiceBasedDetailsEditGrid<Integer, Person, Integer, Organization> grid = new ServiceBasedDetailsEditGrid<>(
							personService, getModelFactory().getModel(Person.class), attributeModel, viewMode,
							new FormOptions().setDetailsGridSearchMode(false).setShowRemoveButton(true));
grid.setFilterCreator(org -> new EqualsPredicate<>("organization.id", org.getId()));
	grid.setCreateEntity(() -> {
		Person person = new Person();
		person.setOrganization(getSelectedItem());
		return person;
	});
	grid.setRemoveEntity(person -> {
		person.setOrganization(null);
		personService.save(person);
	});
	return grid;

As you can see, the ServiceBasedDetailsEditGrid takes four type parameters; the first two are identical to the ones used by the basic DetailsEditGrid. The third and fourth parameters respectively represent the type of the primary key of the parent entity and the type of the parent entity (the parent entity is the entity that you are editing/displaying in the edit form that this component is part of).

The methods for creating and removing entities (setCreateEntity and setRemoveEntity) function in roughly the same way as for the DetailsEditGrid.

Of special interest is the setFilterCreator method, which is used to specify the filter that is used to limit the entities that show up. In this case, we apply a filter that makes sure that only the members for which their organization matches the organization that is currently being edited in the edit form show up.

If you create a ServiceBasedDetailsEditGrid in the way described above, the application will render a grid in read-only mode, with each row in the grid containing an edit button. Clicking on this button will bring up a dialog that allows you to edit the entity displayed in that row.

There will also be an Add button below the grid – this can be used to bring up a dialog that can be used to add a new entity and couple it to the parent entity being edited in the form.

Note that if you have a pre-existing collection of entities that you want to couple to the entity being edited (a many-to-many relation) you can use the grid in couple mode as well. As with the basic DetailsEditGrid you can specify this via the form options.

FormOptions fo = new FormOptions().setDetailsGridSearchMode(true);

In this mode, there will not be any Add or Edit buttons, but instead you will see a Search button below the grid. Clicking this button will bring up a search dialog that allows you to select the entities to link. You can use the setSearchDialogFilters method to specify the (default) filters that must be applied when within the search dialog.

In this mode, you will need to use the setLinkEntity method to specify the code that is to be carried out to link the selected item(s) from the dialog screen to the entity that is being edited in edit form. The method is called once for each entity that was selected in the dialog screen.

DetailsEditLayout

A slightly more complex use case occurs in the situation in which you want to manage a one-to-many relation inside an edit form, while using a nested form per dependent entity rather than a grid for all dependent entities together. This can be achieved by using a DetailsEditLayout component which can be constructed in a way similar to the creation of a DetailsEditGrid:

The DetailsEditLayout constructor takes the following arguments:

  • The service used communicating with the database.

  • The collection of entities to display within the component – for each entity, a separate nested edit form will be rendered.

  • The entity model used to render the edit forms.

  • The attribute model that must be used to create the nested forms (this is only used to set the caption that appears in the main form)

  • The view mode parameter that determines whether the forms are in view mode or edit mode.

  • The familiar FormOptions object.

  • A comparator that can be used to order the entities before the nested forms are rendered (optional).

As is the case with the DetailsEditGrid, you can use the setCreateEntity and setRemoveEntity methods to specify what to do in order to add a new entity or remove a coupled entity.

DetailsEditLayout<Integer, GiftTranslation> dt = new DetailsEditLayout<Integer, GiftTranslation>(giftTranslationService, getEntityModelFactory().getModel(GiftTranslation.class), attributeModel,
					viewMode, fo, new AttributeComparator<>("description"));
dt.setCreateEntity(() -> {
	GiftTranslation gt = new GiftTranslation();
	GiftLayout.this.getSelectedItem().addTranslation(gt);
	return gt;
});
dt.setRemoveEntity(gt -> {
	Gift gift = GiftLayout.this.getSelectedItem();
	gift.removeTranslation(gt);
s});

As described above, the framework will render a sub-form that is nested within the main edit form for every entity. For each sub-form a Remove button will be included, and below the last sub-form an Add button will be placed that can be used to create a new sub-form.

It is not possible to individually save the sub-forms – any changes will be saved to the database only when the Save button for the main edit form is clicked.

EntityPopupDialog

The EntityPopupDialog is a simple modal dialog that can be used to add, modify, or view a single entity. It can be used as follows:

 Button popupButton = new Button("Popup");
   popupButton.addClickListener(event -> {
   EntityPopupDialog<Integer, Organization> popupDialog = new         EntityPopupDialog<Integer, Organization>(organizationService,
                            getSelectedItem(), getEntityModelFactory().getModel(Organization.class), new HashMap<>(), new    FormOptions());
    popupDialog.build();
    popupDialog.open();
 });
 buttonBar.add(popupButton);
 registerComponent(popupButton);

As you can see, the EntityPopupDialog takes many of the same parameters as the other composite components:

  • The service used to query the database.

  • The entity to display (the “getSelectedItem” in the example above)

  • The entity model used to construct the dialog.

  • The FormOptions used to modify how the form is constructed.

The code above will open a modal dialog that allows you to edit the selected entity:

image

By default, the popup will open in edit mode. You can modify this by using the FormOptions and setting openInViewMode to true.

CompositionLayout

A CompositionLayout is a layout that allows you to group multiple composite components together. The most common use case for this is inside a tab page of a lazy tab layout: once the tab is selected, the framework will reload the contents of the CompositionLayout, which will in turn delegate the reload request to all the components nested within.

When you want to use a CompositionLayout, call its setBuildMainLayout method and create any components that you want to nest inside this method. For each component, register it with the CompositionLayout using the addNestedComponent method.

Callbacks and method overrides

The previous sections should have given a good impression of the capabilities of the Dynamo Framework and the declarative mechanisms that are available to modify its functionality without writing custom code. However, there are some situations in which these mechanisms are not enough. To handle this, most components in the Dynamo framework offer callback methods that can be used to interfere in the component generation process.

These methods typically come in two variations:

  • Methods that are called just once during the component creation process

  • Methods that are called in response to a user action, e.g. when the user presses a button or selects a row in the grid.

To gain a better understanding of this, note that nearly all components in the Dynamo framework are lazily constructed and that the individual moving parts of a composite component (e.g. the search grid, the edit form) are lazily constructed as well.

This means that at the time you declare an instance of a composite component, its layout is not constructed yet. This allows to make changes e.g. to modify the page length or to defined field filters or default filters – at this point you are only calling setter methods.

Once the component is added to its parent component, Vaadin will call the onAttach method, and this method has been overridden to call the build method which is implemented by all composite components and it used to lazily construct the starting state of the component. E.g. for a search layout, the framework will initialize the search form and the results grid. During this process, callback methods are carried out.

Other parts of the component are not immediately created, e.g. the edit form in only created the first time the user presses the Add or View/Edit button. This might also trigger some callback methods.

In case it is not possible to directly attach a component to its parent component, you can also programmatically call the build method to construct the component.

A consequence of this construction method is that there are many methods that cannot be called before the component has been attached (or built) – e.g. trying to retrieve the search result grid or search form will simply result in a nullpointer. Therefore, you should always observe the following order: instantiate – call setters – build/attach.

Finally, note that even though components are constructed lazily and just once as long as the user stays on the same view, they are still rebuilt whenever the user navigates to a new view.

The next sections will go over some of the available callback methods. Not all callback methods are supported by (or make sense) for all composite components. For each method, we specify for which composite component it is available. We distinguish:

  • SearchLayouts (SimpleSearchLayout and FlexibleSearchLayout)

  • SplitLayouts (ServiceBasedSplitLayout, FixedSplitLayout)

  • EditableGridLayout

  • SimpleEditLayout

Initialization methods

These are methods that are typically called to initialize the component.

Method Available on Functionality

addCustomConverter

All composite components

Registers a custom converter for a certain attribute model. When constructing the component for that attribute model, this custom converter will be used instead of the default converter

addCustomField

All composite components

This method can be used to modify the component creation process. To use it, call it using the name of the attribute for which you want to create the custom component, and a lambda function that accepts the component creation context and must return your custom component.

The context can be used to retrieve the attribute model for the attribute, and can be used to check whether the component is being constructed for the search form or for a form that is in view mode. (See below for more explanation)

addCustomRequiredValidator

All composite components

Registers a custom validator that will only check whether a value has been entered for a certain attribute model.

addCustomValidator

All composite components

Registers a custom validator for a certain attribute model. When constructing the component for that attribute model, this custom converter will be used instead of the default converter

addFieldEntityModel

All composite components

Used for registering an entity model that is to be used for constructing a specific field. This can be used to modify the entity model that is to be used inside an EntityLookupField

addFieldFilter

All composite components

Adds a field filter that is to be used when constructing a specific field

addSortOrder

All composite components except SimpleEditLayout

Adds a sort order. This sort order is applied in addition to the sort order that is provided in the constructor

registerComponent

All composite components

Registers a component (usually a button) that is added to the button bar, so that it can be enabled/disabled when an entity is selected. By default, a registered component will be enabled when a row is selected in the results grid and disabled when this is not the case.

See below for more explanation.

setAfterEditFormBuilt

SearchLayouts, SplitLayouts

Executed after the edit form (inside a SearchLayout or SplitLayout) has been created for the first time.

setAfterLayoutBuilt

All composite components

This method is built at the end of the component construction process. Use this method to make any changes to the constructed component, e.g. changing button captions, hiding grid columns etc.

setDefaultFilters

SearchLayouts

Sets the default filters that will always be applied to any search request, regardless of what is entered in the search form.

setEditAllowed

All composite components

Use this method to specify when the user is allowed to perform edit actions within the component. This comes in addition to the settings from the form options, i.e. editing is only possible if this method return true AND the component is not in read-only mode.

setEditColumnThresholds

All composite components

Sets the threshold values at which new columns will be rendered inside an edit form

setFindParentGroup

All composite components

This method provides a mapping from an attribute group key (as defined on the entity) to the parent group specified using the setParentGroupHeaders method

setGridHeight

All except for SimpleEditLayout

This method is used to set the height of the search results grid

setLoadItemCreator

FixedSplitLayout

This code that is carried out to retrieve the fixed collection of items to display in a FixedSplitLayout

setMustEnableComponent

All composite components

This method is called after a user select a row in a grid, to see which button bar components must be enabled

setOnAdd

SearchLayouts, SplitLayouts

The code that is carried out after the Add button is clicked. By default this will open the details mode for creating a new entity.

setOnEdit

SearchLayouts, SplitLayouts

The code that is carried out after the Edit button is clicked. By default this will open the selected entity in the details mode

setOnRemove

SearchLayouts, SplitLayouts

The code that is carried out after the Remove button is clicked. By default this will delete the selected entity

setPostProcessButtonBar

SimpleEditLayout

This method is called after the button bar of a component has been constructed. It can be used to add additional buttons or other components.

This method is used to modify the button bar that appears above and below an edit form

setPostProcessDetailButtonBar

All except for SimpleEditLayout

This method is called after the detail button bar of a component has been constructed. It can be used to add additional buttons or other components.

This method is used to modify the button bar that appears above and below an edit form

setPostProcessEditFields

All composite components

This method is called after all fields inside an edit form have been constructed. It can be used to e.g. add dependencies between the fields

setPostProcessMainButtonBar

All except for SimpleEditLayout

This method is called after the main button bar of a component has been constructed. It can be used to add additional buttons or other components.

The “main” button bar is the button bar that appears below a results grid

setPostProcessSearchButtonBar

SearchLayouts

This method is called after the search button bar of a SearchLayout has been constructed. It can be used to add additional buttons or other components.

The search button bar is the button bar that appears below a results grid

setFilterCreator

ServiceBasedSplitLayout, EditableGridLayout

Use this method to construct the filter that is applied to limit the results that show up in the results grid

setMaxResults

SearchLayouts, SplitLayouts

Sets the maximum allowed number of search results

setParentGroupHeaders

All composite components

This method can be used to specify an additional layer of grouping that can be added inside an edit form. This method takes an array of Strings that will be used as the headers for the extra tabs or panels.

setQuickSearchFilterCreator

ServiceBasedSplitLayout

Use this method to construct the filter that will be applied to the search results grid when the user enters a search term in the quick search field. Use “showQuickSearchField” on the FormOptions to make this field show up.

setSearchColumnThresholds

SimpleSearchLayout

Sets the threshold values at which extra columns will appear inside a search form

Listeners

These methods are typically called after an event occurs (e.g. a user clicks a button or selects a row in a grid).s

Method Available on Functionality

setAfterAdvancedModeToggled

SearchLayouts

Executed after the search form switches between normal and advanced mode

setAfterClear

SearchLayouts,

Executed after the user clicks the “Clear” button in a search form

setAfterEntitySelected

All composite components except SimpleEditLayout

Executed after an entity is selected in a results grid and displayed in an edit form

setAfterEntitySet

All composite components except SimpleEditLayout

Executed after an entity has been selected to be displayed in an edit form but before the form has actually been constructed. Typically used to load additional data

setAfterModeChanged

All composite components

Executed after the mode of a component changes from view mode to edit mode or vice versa

setAfterSearchFormToggled

SearchLayouts

Executed after the search form inside a search layout is shown or hidden

setAfterSearchPerformed

SearchLayouts

Executed after a successful search is carried in a search layout.

setAfterTabSelected

All composite components

Executed after a tab is selected inside an edit form when attribute groups are used and the screen mode is set to TABSHEET.

setAfterUploadCompleted

All composite components

Code that is executed after file has been successfully uploaded using an edit form

setCreateEntity

All composite components

Executed after the user clicks the “Add” button to construct a new entity.

setCustomSaveAction

All composite components

Specifies the code to be executed when the save button is clicked. This overrides the default save behaviour so you will have to manually call the service to actually save any changes.

validateBeforeSearch

SearchLayouts

Use this method to perform extra validation before a search is carried out.

Operational methods

These are methods that are typically called directly from your code after the component has been created.

Method Available on Functionality

customDetailsView

SearchLayouts, SplitLayouts

Call this method if you want to display a custom component or set or components instead of the normal edit form. This method takes a single parameter of type Layout that represents the layout that must be displayed.

detailsMode

SearchLayouts, SplitLayouts

Opens the component in details mode with the provided entity selected

emptyDetailView

SplitLayouts

Clears the details view, replacing it by a label that says “Select an item to view or edit its details”

isRegisteredComponent

All composite components

Checks whether a component was registered under a certain identifier. Used for advanced scenarios in which to enable/disable certain buttons.

refresh

All composite components

Use the refresh method to refresh all selection components inside the search or edit forms inside a composite component.

reload

All composite components

Use the reload method to reload a composite search component. This will clear any search filters and perform a new search. It will also de-select any currently selected entity.

search

SearchLayouts

Carries out a search based on the currently selected search terms (and any default filters)

Further clarification

This section shows some further clarifications and examples of how to use some of the methods described in the sections above.

setParentGroupHeaders and setFindParentGroup

These methods can be used to add an additional layer of nesting to the grouping of your attributes inside an edit form. As you saw before, you can use the @AttributeOrder annotation to specify how the attributes of an entity are grouped together in an edit form. Sometimes this is not enough and in these cases, you can do the following:

  • The setParentGroupHeaders method can be used to return an array of Strings. These represent the captions of the extra groups you want to create.

  • The setFindParentGroup method can be used to link an existing attribute group (as defined on your entity) to the newly created top level group.

setParentGroupHeaders(
        new String[] { "blip.attributes.main", MSG_ATTRIBUTES_PROMOTED, "blip.attributes.other" });
setFindParentGroup(childGroup -> {
    switch (childGroup) {
    case Constants.AUDIT_DATA:
        return "blip.attributes.other";
    case Constants.PROMOTED_MAIN:
    case Constants.PROJECTED_FINAL:
    case Constants.OTHER_ATTRIBUTES_PROMOTED:
        // display on the promoted tab
        return MSG_ATTRIBUTES_PROMOTED;
    default:
        return "blip.attributes.main";
    }
});

The value of the attributeGroupMode from the FormOptions determines the top-level attribute grouping – so if this is e.g. set to TABSHEET then we get a tab component with two tabs, and on the lower level the attributes are grouped together using panels. If you set the attributeGroupMode to PANEL then you will get two panels on the top level, and a tab component within each panel.

Post-Processing button bars and registering component

The composite components offer a number of methods for modifying the various button bars that they contain.

The setPostProcessMainButtonBar method can be used to make modifications to the button bar that appears below the results grid in a SearchLayout, a SplitLayout or a EditableGridLayout.

The setPostProcessDetailButtonBar method can be used to make modifications to the button bar that appears above and below the edit form in a SearchLayout, a SplitLayout or an EditableGridLayout. For a SimpleEditLayout you can achieve the same by using the setPostProcessButtonBar method.

Finally, the setPostProcessSearchButton method can be used to make modifications to the button bar that appears below the search form in a SearchLayout (and that normally contains the Search and Clear buttons).

When a button or component must become enabled/disabled based on whether a row is selected in the grid, then you can call the registerComponent method (passing the component as the argument) inside one of the post process method. If you need to further modify the conditions under which the button is enabled, use the setMustEnableComponent method.

As an example, consider the following:

layout.setPostProcessMainButtonBar(buttonBar -> {
    Button notificationButton = new Button("Show name");
    notificationButton.addClickListener(event -> {
        showErrorNotification(layout.getSelectedItem().getNickName());
    });
    buttonBar.add(notificationButton);
    layout.registerComponent("notificationButton", notificationButton);

});
layout.setMustEnableComponent((com, ent) -> {
    if (layout.isRegisteredComponent("notificationButton", com)) {
        return ent.getFirstName().startsWith("M");
    }
    return true;
});

First, we create a button (“notificationButton”) that simply shows a notification message when clicked. We then register this button under the reference “notificationButton”.

Now, every time a row is selected or deselected in the results grid of the component, all registered buttons are evaluated. If no row is selected, the button is disabled. If a row is selected, the “mustEnableComponent” method is evaluated for every registered component. In the example above, we check if that component is the notificationButton (by calling the isRegisteredComponent method).

setPostProcessEditFields

If you want to add custom behaviour to the edit form, you can do so in the setPostProcessEditFields method. This method can e.g. be used to add extra behaviour to input fields (i.e. respond to the change of field A by changing the value in field B, or by disabling field C). This method is called once per composite layout component, after the first time the edit form has been constructed (it is not called if the form is in view mode, since in that case it doesn’t contain any actual input components).

The method invocation takes a lamdba has a single parameter of type ModelBasedEditForm. This is a class from the Dynamo Framework that is responsible for creating and managing an edit form based on the Entity Model. If you want to retrieve the component used for editing a certain attribute, you can use the getField method for this.

The getField method will return null when there is no appropriate field (e.g. because the associated attribute is read-only!) so use this with care.

Note: you must not use this method to modify the characteristics of the fields that you can easily manipulate using the entity model. E.g. if you always want a field to be invisible or read-only, use the entity model for this. Use the setPostProcessEditFields method only for complex inter-field behaviour e.g. for having one field respond to a Callback methods called in response to user action.

Additional Functionality

This chapter covers some additional functionality offered by the composite components in the Dynamo framework

Automatic form filling

The Dynamo Framework contains functionality for automatically (partially) filling an input form using an AI service (Large Language Model). In order to use this functionality, you need to set up the appropriate LLM service, which can mostly be done using application properties.

Then, you can set the showFormFillButton setting on the FormOptions parameter object to true. This will cause an “Auto fill form” button to show up in the button bar of an edit form.

The following services are supported:

  • OpenAI (ChatGPT)

  • Amazon Bedrock

  • Ollama

  • Google VertexAI Gemini

OpenAI

Using the Spring AI integration for OpenAPI proved to be the easiest integration of all, due to previous experience with OpenAPI and the generally robust and easy-to-use API. The integration can be enabled simply by setting the ocs.openai.enabled application property to “true”. IN addition, values for the API key, the desired model, and the maximum number of tokens must be provided.

ocs.openai.enabled=true +
ocs.openai.api.key=[SECRET]

ocs.openai.model=gpt-4-turbo +
ocs.openai.maxtokens=4096

To use the service, a (paid) subscription with OpenAI is required; given such a subscription, the user can generate an API key (token) that can be used to call the API.

Ollama

Ollama is an application that can be run locally (or in a private cloud) that makes it possible to service AI models in a fairly straightforward manner. It is possible to use Ollama both via the command line and via a REST interface.

Locally installing Ollama is relatively easy, although the directory in which Ollama is installed (at least on Windows) is slightly unusual, i.e. <user dir>\AppData\Local\Programs\Ollama . You might need to add this location to your Path system variable.

Afterwards, you can start Ollama by typing ollama run [model name], e.g.. Ollama run llama3. We have tried the models llama3 and mistral which both seem to work fairly well.

Configuring ollama is relatively easy since it is running locally and does not require an external account. The configuration needs a URL (the default is localhost:11434) and a model name (llama3 or mistral).

ocs.ollama.enabled=true +
ocs.ollama.url=http://localhost:11434 +
ocs.ollama.model=llama3

Google VertexAI Gemini (and PALM)

Google VertexAI offers two LLMs that are interesting for us: PALM and Gemini. We briefly tested both types of models and they seems to be fairly fast an reliable (and on par with OpenAI). Note that the Spring AI documentation mentions that PALM is only available in the USA but this does not appear to be the case anymore.

Using VertexAI in your application is relatively straightforward, but it does require that you set up a Google Gloud account (https://console.cloud.google.com/) and link this account to a credit card. You can then create a project and enable the Vertex API for this project.

ocs.vertexai.enabled=true +
ocs.vertexai.project.id=[SECRET] +
ocs.vertexai.project.region=europe-west1 +
ocs.vertexai.model=gemini-1.5-flash-preview-0514

With this in place, the configuration is pretty straightforward: you need to provide the application with your project ID, Google Cloud region, and desired VertexAI model.

It also seems perfectly possible to use the Spring AI gemini library in combination with PALM models (although the Gemini models seem to perform better)

Amazon Bedrock

Amazon Bedrock is a service (offered by AWS) that provides a large number of AI models. After a bit of experimentation, we have concluded that the Antropic models (“Claude”) seem to suit our purposes best.

Using the Amazon Bedrock services requires an Amazon account which is connected to a credit card.

Once this is in place, you can use the AWS IAM (Identity and Access Management) module to create a user that must be given “Bedrock Full” access rights, and you can then create a key/secret pair for accessing the service from your application.

Configuration can then be done as follows:

ocs.bedrock.enabled=true

ocs.bedrock.access.key=AKIA5FTY7NWAPH2I7CGV +
ocs.bedrock.access.secret=[SECRET]

ocs.bedrock.model.id=arn:aws:bedrock:eu-central-1::foundation-model/anthropic.claude-v2 +
ocs.bedrock.region=eu-central-1

The configuration for Bedrock can unfortunately be a bit messy, since the available models are region-dependent and also must be explicitly enabled in the AWS console. Furthermore, the exact ids of the models can be hard to determine. You can use the aws console, with the command aws bedrock list-foundation-models and then use the ids of the returned models.

When you have configured one or more AI models correctly, and set the formFillEnabled property to true, an “Auto Fill” button will show up in the button bar of he input form. Clicking this button will bring up a dialog that allows the user to:

  • Select one of the configured AI services.

  • Enter the text that is to serve as the input for the LLM prompt.

  • Provide any additional instructions that are to be applied to the entire request

  • Pressing the “OK”-button will send a request to the LLM service, which will try to translate the user input to valid property values, which will be pre-filled into the form.

image

Pressing the “OK”-button will send a request to the LLM service, which will try to translate the user input to valid property values, which will be pre-filled into the form. This makes the most sense when creating a new entity, but the functionality is also available when editing an existing entity.

The LLMs should generally do a reasonable job of interpreting the user input, especially for basic attributes like strings or numbers. For complex attributes (i.e. lookup lists) the framework will try to look up the appropriate entity based on its display property.

Excel and CSV Export

The Dynamo Framework offers some functionality for automatically exporting data that is displayed in results grid to Excel (.xlsx) or CSV format.

In order to use this functionality, you will first have to add the appropriate dependencies to your project:

<dependency>
    <groupId>org.dynamoframework</groupId>
    <artifactId>dynamo-frontend-export</artifactId>
    <version>${dynamo.version}</version>
</dependency>
<dependency>
    <groupId>com.github.pjfanning</groupId>
    <artifactId>excel-streaming-reader</artifactId>
    <version>4.3.1</version>
</dependency>

Next, to enable the export for all applications in the table, you will have to set the system property ocs.allow.list.export to true. This will enable the export functionality for all grids in your application. If you would rather enable the export on a screen-by-screen basis you can use the setExportAllowed setting on the FormOptions to true.

The result of this will be that when the user right-clicks on a search results grid in the application, the following dialog will appear:

image

Clicking on the buttons will respectively export the data in the grid to Excel or CSV. Note that the export that will be created contains all the data in the result set (i.e. all data that matches the search criteria), not just the rows that are currently displayed – Dynamo will iterate over the entire data set using pagination.

By default, only the columns that are visible in the grid will be exported. You can switch to a full export of all the (visible) attributes by using the FormOptions object:

FormOptions fo = *new* FormOptions().setExportMode(ExportMode.*_FULL_*);

Note that if you set the export mode to full, this might mean that the application will try to export related entities that have not been fetched, resulting in lazy loading exceptions.

To remedy this, you can do the following:

  • You can use the setExportJoins method to specify which relations to fetch when performing the export.

  • You can use the setExportEntityModel method to specify a custom entity model to use for the export. This overrides the default entity model that is used to construct the search results grid. You can e.g. use this to set the visibility of certain attributes to false so they are excluded from the export.

To further customize the export to Excel, it is possible to specify a custom style generator.

You can register such a custom style generator by using the static ExportDelegateHelper.addCustomStyleGenerator. This method adds a mapping between an entity model and a style generator.

The style generator is a fairly straightforward functional interface that defines a method with the following signature:

CellStyle getCustomCellStyle(Workbook workbook, T entity, Object value,
AttributeModel am, Object pivotColumnKey);

This method allows you to define custom style (that e.g. allows you to change the color, font, border etc.) for a cell based on the entity that is being displayed, the cell value, and the attribute model (the pivotColumnKey is only used in very specific circumstances and can usually be ignored).

Lookup tables and parameters

Dynamo contains an optional module for working with domains/lookup tables. These are simple database tables that typically contain only a couple of attributes and only a handful of records that are often more or less stable over time. Some examples include a list of countries, languages, order statuses etc.

If you want to use Dynamo’s functionality for easily managing lookup tables, you will have to include the following dependency in your pom.xml file:

<dependency>
    <groupId>org.dynamoframework</groupId>
    <artifactId>dynamo-functional-domain</artifactId>
    <version>$\{com.ocs.dynamo.version}</version>
</dependency>

This will give you access to a number of bases entities, DAO’s, and services for managing lookup tables/domains. The most important one is arguably the Domain entity which serves as the base for all your lookup tables.

@Inheritance
@DiscriminatorColumn(name = "type")
@Entity(name = "domain")
@Model(displayProperty = "name", sortOrder = "name asc")
public abstract class Domain extends AbstractEntity<Integer> {

The idea is that you can define a lookup table class by subclassing this class as follows:

@Entity
@DiscriminatorValue("COUNTRY")
@Model(displayNamePlural = "Countries", displayProperty = "name", sortOrder = "name asc")
public class Country extends DomainChild<Country, Region> {

As you can see, this example uses JPA inheritance to define a new entity that will be stored in the “domain” table using JPA’s “single table” inheritance model.

You can then define simple DAO and service objects for your domain class as follows (you would normally place these in a class annotated with @Configuration).

@Bean
public BaseDao<Integer, Country> countryDao() {
	return new DefaultDaoImpl<>(QCountry.country, Country.class, "parent");
}

@Bean
public BaseService<Integer, Country> countryService(BaseDao<Integer, Country> dao) {
	DefaultServiceImpl<Integer, Country> countryService = new DefaultServiceImpl<>(dao, "code");
	return countryService;
}

Note that the DefaultDaoImpl and DefaultServiceImpl classes are part of the core dynamo implementation so you can use them without adding the dynamo-functional-domain dependencies to your pom.xml.

In addition to the Domain class, the dynamo-functional-domain module contains several other classes like DomainParent and DomainChild that can be used to manage lookup tables with simple hierarchies.

Dynamo also comes with simply functionality for managing a list of parametes. To use this functionality, add the following dependency:

<dependency>
	<groupId>org.dynamoframework</groupId>
	<artifactId>dynamo-parameter</artifactId>
	<version>${com.ocs.dynamo.version}</version>
</dependency>

This makes the Parameter class as well as the associated ParameterDao and ParameterService available to your application.

The Parameter and Domain classes are database agnostic – this is great because you can use them with multiple databases, but it does mean that some additional configuration is required to make the module work with your database.

This configuration can be included in the orm.xml file that can be included in the src/main/resources/META-INF directory. The following shows the configuration for PostgreSQL:

<?xml version="1.0" encoding="UTF-8" ?>
<entity-mappings xmlns="http://java.sun.com/xml/ns/persistence/orm"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/persistence/orm http://java.sun.com/xml/ns/persistence/orm_1_0.xsd"
	version="1.0">
	<sequence-generator name="domain_id_seq"
		allocation-size="1" sequence-name="domain_id_seq" />
	<sequence-generator name="parameter_id_seq"
		allocation-size="1" sequence-name="parameter_id_seq" />
	<entity class="com.ocs.dynamo.functional.domain.Domain">
		<attributes>
			<id name="id">
				<generated-value strategy="SEQUENCE" generator="domain_id_seq" />
			</id>
		</attributes>
	</entity>
	<entity class="com.ocs.dynamo.functional.domain.Parameter">
		<attributes>
			<id name="id">
				<generated-value strategy="SEQUENCE" generator="parameter_id_seq" />
			</id>
		</attributes>
	</entity>
</entity-mappings>

Dynamo also contains some functionality for managing your lookup tables in the user interface. To use this functionality you must include the dynamo-functional-domain-frontend dependency:

<dependency>
	<groupId>org.dynamoframework</groupId>
	<artifactId>dynamo-functional-domain-frontend</artifactId>
	<version>${com.ocs.dynamo.version}</version>
</dependency>

This dependency includes a single component named MultiDomainEditLayout which offers a simple screen for managing your lookup tables.

List<Class<? extends Domain>> domains = new ArrayList<>();
domains.add(Country.class);
domains.add(Region.class);
MultiDomainEditLayout dom = new MultiDomainEditLayout(new FormOptions(), domains);

You can simply pass a list of the domain classes you want to manage to the MultiDomainEditLayout’s constructor. This will then result in a screen that looks as follows:

image

This component consists of a combo box that allows a user to select the lookup table they want to edit. Selecting a value in the combo box will cause a split layout for the selected lookup table to be rendered. The user can then use this split layout to add, modify or delete the values of the lookup table.

Field Factory

The component in Dynamo that is responsible for actually creating the input components that show up inside edit forms is called the FieldFactory. Normally, the field factory is used under internally by the framework and you will rarely have to use it directly, but it can be useful in situations in which you are creating custom screens that still use input components backed by an entity model – in this case you can use the field factory to create the components one-by-one.

In order to acquire a reference to a FieldFactory, you can use the static FieldFactory.getInstance() method.

You can then use the constructField(AttributeModel am) or constructField(FieldCreationContext ctx) methods to create the field.

The simplest version (with the attribute model) parameter simply creates a field that is based on the provided attribute model, using all default settings.

The version that takes a FieldCreationContext allows you to further customize the creation process. The FieldCreationContext allows you to specify:

  • The attribute model

  • The field entity model to use (if not specified, the default entity model for the field’s type will be used)

  • A map of field filters – if this map contains an entry that matches the path of the attribute model, the field filter will be applied when creating the field.

  • Whether the field should be constructed for search mode (this is false by default).

  • Whether the field will be placed inside an editable grid.

  • Whether the field should be constructed for view mode (this is false by default) – in most cases the field factory will not construct a field and return NULL when the view parameter is set to true.

  • The shared data providers – this is used internally by the framework in order to share the data providers that are used by similar components inside an editable grid.

In addition to the constructField-methods the Field Factory also offers the addConvertersAndValidators method. This method has the following signature:

addConvertersAndValidators method. This method has the following signature:
<U> void addConvertersAndValidators(BindingBuilder<U, ?> builder, AttributeModel am,
			Converter<String, ?> customConverter);

This method is used internally by the framework in order to add converters and validators to a field, but in case you are creating your own form it can be useful as well. The method takes the following parameters:

  • A BindingBuilder which is a class offered by the Vaadin framework that can be used to bind a field to the underlying data model.

  • The attribute model used for field construction.

  • A custom converter that will be used instead of the default converter used by the framework.

All Dynamo applications require a FieldFactory but since it is by autoconfigured by Spring Boot you do not manually have to create one.

It is also possible to modify the component creation process directly. To know how to do this, it is good to know that the FieldFactory delegates the actual creation of the components to a number of ComponentCreators. A ComponentCreator is an interface that has two subclasses: SimpleComponentCreator and EntityComponentCreator. Each of these has a number of subclasses that are each responsible for creating a certain type of input component, e.g. a TextField or an EntityComboBox (at this moment there are about twenty in total). Each ComponentCreator subclass implements a supports method that is used by the FieldFactory to check if it supports constructing a component for the current attribute model and creation context – the first component creator that is found is used by the FieldFactory to actually construct the component by calling the createComponent method. Later in the process, the FieldFactory also calls the addConverters and addValidators method to add converters and validators to the component (well, technically to the binding between the component and the backing bean, but you get the point).

When you fire up a Dynamo application, Spring Boot autoconfigures all of the relevant ComponentCreators and the FieldFactory, so there is nothing you have to do for this to work out of the box. However, note that you have two (obvious) options to interfere in the component creation process:

  • First of all, you can make a class that is a subclass of one of the ComponentCreators that come with Dynamo and annotate it as a Spring component (e.g. by using @Service or @Component). Spring Boot will then use your subclass instead of the default implementation. You can use this subclass to make modifications to the component creation process, e.g. by changing default settings, adding functionality etc. But if needed you can go much further and use your subclass to construct a completely different component. Note however that this will only work properly as long as your component can deal with the same data types as the original component – i.e. overwriting TextFieldCreator and having your subclass return a date picker is not going to work very well.

  • The ComponentCreators are also ordered using the Spring @Order annotation, so what you can also do is define your own class (that inherits from SimpleComponentCreator or EntityComponentCreator), then register this component as a Spring bean and annotate it with an @Order with a lower value than the default converters (the default converters start with an order value of 1000 so anything below that will work). The FieldFactory will start evaluating the ComponentCreators starting with the one with the lowest order, so your custom creator will be found before the default ComponentCreators. You can use this technique to create components for fairly specific scenarios, while still using the default components the rest of the time.

Styling

It is possible to update the styling of Dynamo applications. To do so, two separate approaches are possible (or necessary, depending on what exactly you are trying to do).

To make changes to the general styling of the application, create a file named “shared-styles.css” and place this in the “frontend/styles” subdirectory of your project root directory.

To make sure the application can actually find your style sheet you must pace an @CssLayout on the class in your application that imlements RouterLayout:

@CssLayout on the class in your application that imlements RouterLayout:
@CssImport("./styles/shared-styles.css")
public class GtsUI extends VerticalLayout implements RouterLayout {

Inside this css file you can place your custom styles or make changes to the default styles.

One thing to note is that you can refer to or modify the variables included with the Vaadin Lumo theme. A list of the available variables can be found at https://cdn.vaadin.com/vaadin-lumo-styles/1.5.0/demo/index.html.

In order to modify a variable, you can include an html element in your style sheet and then assign a new value as follows:

html {
	--lumo-primary-color: #f99d34;
}

You can then refer to the variable in the remainder of the style sheet as follows:

border: 1px solid var(--lumo-primary-color) !important;

As you can see, you use the name of the variable enclosed in a var() construct.

Unfortunately, this does not work for all styles in the application. That is because Vaadin now uses Web Components and the styling for these is often included in the Shadow DOM. The Shadow DOM is a technique that can be used to define styling in a way that is local to each component. This can be very useful but it does mean that modifying the styles from a global style sheet is tricky.

To modify styles that are in the shadow DOM, create a file named “<component-name>.css” in the frontend/styles directory, where “component-name” is the name of the component you want to modify, e.g. “vaadin-custom-field.css”.

Inside this file, you can place any valid CSS that you want.

Next, add another CssImport annotation to your RouterLayout class and make sure the value matches the name of the file you created.

@CssImport(value = "./styles/vaadin-custom-field.css", themeFor = "vaadin-custom-field")
public class GtsUI extends VerticalLayout implements RouterLayout {

Hibernate Envers Integration

Dynamo offers some functionality for visualising the history that can be generated using Hibernate Envers. For those unfamiliar with Hibernate Envers, it is a library that allows the developer to define a number of entities to be audited. Whenever any change is made to these audited entities, Hibernate Envers automatically stores a snapshot of the new state of the entity along with a time stamp.

In order to make this possible, Hibernate needs a number of additional database tables – one table to keep track of the time stamps and revision keys, and one table for every “regular” table you want to audit. By default, these shadow tables are named after the table they are shadowing, postfixed with “_aud”. E.g. the shadow table for “person” would be named “person_aud”. The table for keeping track of timestamps is normally named “revinfo”. However, Dynamo makes some small additions to the framework and stores the revision info in a table named “RevisionEntity” instead. The definition for this table (MySQL) is as follows:

CREATE TABLE `RevisionEntity` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `timestamp` bigint(20) NOT NULL,
  `username` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
);

And for Postgresql:

CREATE TABLE IF NOT EXISTS revisionentity(
id integer NOT NULL DEFAULT nextval('revision_entity_id_seq'::regclass),
 "timestamp" bigint NOT NULL,
username character varying(255) ,
  CONSTRAINT revisionentity_pkey PRIMARY KEY (id)
)

You must provide implementations for the shadow tables yourself. For more information on this, consult the Hibernate Envers documentation. However, note that compared to the original table:

  • There must be a REV column that contains the revision number

  • There must be a REVTYPE column that contains the type of the revision (insert, update, delete)

  • The primary key must consist of the primary key of the original table combined with the REV column.

  • Columns (other than REV, REVTYPE and primary key fields) cannot be required. This is because if the record in the shadowed table is deleted, a mostly empty record is stored in the shadow table.

Instead of creating these tables yourself, you can also set the spring.jpa.hibernate.ddl-auto property to “auto”. This will cause Hibernate to generate the missing shadow table the next time the application starts.

In order to use the Dynamo Envers functionality in your code, add the following to your POM file:

<dependency>
    <groupId>org.dynamoframework</groupId>
    <artifactId>dynamo-envers</artifactId>
    <version>${com.ocs.dynamo.version}</version>
</dependency>

Also, add the following to the UI project:

<dependency>
    <groupId>org.dynamoframework</groupId>
    <artifactId>dynamo-envers-frontend</artifactId>
    <version>${com.ocs.dynamo.version}</version>
</dependency>

You will probably also explicitly have to add the hibernate-envers dependency to the POM of your UI project.

Then, add the following to your application.properties:

spring.jpa.properties.org.hibernate.envers.revision_listener=com.ocs.dynamo.envers.listener.DynamoRevisionListener

This will make sure the framework uses the custom Dynamo revision listener rather than the default Hibernate listener.

Next, update the @EntityScan so that the “com.ocs.dynamo.envers.domain” package is included. This will make sure the “RevisionEntity” entity is recognized.

@ComponentScan(basePackages = { "com.ocs.gts", "com.ocs.dynamo", " com.ocs.dynamo.envers.domain" })
@EntityScan(basePackages = { "com.ocs.gts.domain", "com.ocs.dynamo.functional.domain", "com.ocs.dynamo.envers.domain" })
public class ApplicationConfig extends ApplicationConfigurationSupport {

After that, make sure to annotate your entity classes with @Audited and you should be good to go – whenever one of the audited entities changes, a snapshot of the new state is stored in the shadow tables.

Hibernate Envers supports two ways of querying the history: by time stamp and by entity. The first method allows you to look up the state at a certain moment in the past. The second allows you to retrieve the revision history of an entity.

Dynamo offers the VersionedEntityDao to expose some of this functionality. In order to use this class, take the following steps:

Create a VersionedXXX class, where XXX is the name of the original class:

@Model(displayName = "Organization revision", displayNamePlural = "Organization revisions", displayProperty = "entity.name")
public class VersionedOrganization extends VersionedEntity<Integer, Organization> {

	private static final long serialVersionUID = 4271855110308393329L;

	public VersionedOrganization(Organization entity, int revision) {
		super(entity, revision);
	}

	@Attribute(embedded = true)
	public Organization getEntity() {
		return super.getEntity();
	}

	@Attribute(visible = VisibilityType.HIDE)
	public String getDescription() {
		return RevisionType.DEL.equals(getRevisionType()) ? "Deleted" : getEntity().getName();
	}
}
  • Have the class extend VersionedEntity from the Dynamo framework.

  • Override the getEntity method an annotate it with @Attribute(embedded = true)

  • Overwrite the getDescription() method to have it return a custom value in case the revision type is “DEL” (deleted).

  • If you want to define attribute groups and/or an attribute order, note that you can refer to the properties from the original entity by prefixing them with “entity”. You can also refer to the additional attributes related to the history: revision (the unique revision number), revisionTimeStamp (the time stamp), revisionType (addition, deletion or modification) and user (name of the user that performed the notification).

Next, create a DAO like this:

public interface VersionedOrganizationDao extends VersionedEntityDao<Integer, Organization, VersionedOrganization> {
}

And the implementation associated with it:

@Repository
public class VersionedOrganizationDaoImpl extends VersionedEntityDaoImpl<Integer, Organization, VersionedOrganization>
		implements VersionedOrganizationDao {

	@Override
	public Class<VersionedOrganization> getEntityClass() {
		return VersionedOrganization.class;
	}

	@Override
	protected VersionedOrganization createVersionedEntity(Organization t, int revision) {
		return new VersionedOrganization(t, revision);
	}

	@Override
	public Class<Organization> getBaseEntityClass() {
		return Organization.class;
	}

	@Override
	protected void doMap(VersionedOrganization u) {
		super.doMap(u);
		if (u.getEntity().getCountryOfOrigin() != null) {
			u.getEntity().getCountryOfOrigin().getName();
		}
		if (u.getEntity().getMembers() != null) {
			u.getEntity().getMembers().forEach(c -> c.getFirstName());
		}
		if (u.getEntity().getMainActivity() != null) {
			u.getEntity().getMainActivity().getName();
		}
	}
}

Most of this implementation is very straightforward. Only the doMap method requires some additional clarification. In this method, you should retrieve all relations that must be loaded when loading a revision.

Finally, you must create a Service and ServiceImpl for you entity:

public interface VersionedOrganizationService extends BaseService<RevisionKey<Integer>, VersionedOrganization> {

}

@Service
public class VersionedOrganizationServiceImpl extends BaseServiceImpl<RevisionKey<Integer>, VersionedOrganization>
		implements VersionedOrganizationService {

	@Autowired
	private VersionedOrganizationDao dao;

	@Override
	protected BaseDao<RevisionKey<Integer>, VersionedOrganization> getDao() {
		return dao;
	}

}

After this, you can use the entity like you would use any other entity with regard to the creation of custom components. E.g. to create a simple search layout for browsing the revision table, do the following:

		SimpleSearchLayout<RevisionKey<Integer>, VersionedOrganization> versLayout = new SimpleSearchLayout<>(
				versionService, getModelFactory().getModel(VersionedOrganization.class), QueryType.ID_BASED,
				new FormOptions().setReadOnly(true), null);
		main.add(versLayout);

Dynamo offers one more convenient feature for showing the revision history of a single entity. This is the ViewRevisionDialog:

ViewRevisionDialog<Integer, Organization, VersionedOrganization> dialog = new ViewRevisionDialog(
  versionService, getModelFactory().getModel(VersionedOrganization.class),
	new FormOptions().setAttributeGroupMode(AttributeGroupMode.TABSHEET),
	layout.getSelectedItem().getId());
	dialog.buildAndOpen();

The code above will create and open a popup dialog that displays the revisions for just the selected entity. The FormOptions parameter can be used to specify the screen mode (horizontal/vertical) and group mode (tabsheet/panel).

There is one known limitation with regard to searching on the revision history – it is not possible to search for attributes of type DETAIL (i.e. one-to-many or many-to-many relations) or MASTER.

Composite components class diagram

image

The diagram above displays the most important classes that play a role in the generation of composite user interface components.

At the heart of the diagram is the BaseCustomComponent class which inherits from the Vaadin CustomComponent class and adds standard methods for e.g. accessing the message service. There are several subclasses of this BaseCustomComponent:

  • BaseGridWrapper which is the base class for the components that display a search results grid. The grid itself is an instance of ModelBasedGrid.

  • AbstractModelBasedForm which is the base class for components that display input components (can either be a search or an edit form). This class delegates to the FieldFactory for constructing the actual components.

  • SimpleEditLayout and BaseCollectionLayout are the base classes for components that respectively allow the user to manage a single entity or a collection of entities.

  • ProgressForm (not shown in diagram) is a form that allows the user to start a batch process (like a file upload) and contains a progress bar that can be used to keep track of this process.

  • TabLayout (not shown in diagram) is a tab sheet components that has the functionality for creating certain tabs on the fly.

As you can see, there are two subclasses of AbstractModelBasedForm: a ModelBasedEditForm and an AbstractModelBasedSearchForm: the former is the class that is used for rendering an edit form based on the entity model. It is one of the most complex classes in the framework and is used by any composite component that contains an edit form. The latter is the base class for all search forms within the framework. It has two subclasses: ModelBasedSearchForm and ModelBasedFlexibleSearchForm.

As you can see, each of the composite components (the ones whose name ends in “layout”) are comprised of several other components:

  • The SimpleEditLayout is straightforward and is mostly a wrapper built around the ModelBasedEditForm.

  • The BaseCollectionLayout is used to manipulate a collection of entities. It comes in three main varieties:

    • EditableGridLayout – this is the simplest variant, it contains only a table wrapper that displays a search results table.

    • Split layouts – consisting of a table with an edit form next to it or below it. There are several variants of Split Layout but each one contains both a table wrapper (which hold the search results table) and an edit form.

    • Search layouts - these are the most complex composite components. A search layout contains a search form, a search results grid, and an edit form. There are two variants: a SimpleSearchLayout that contains a “traditional” search form (ModelBasedSearchForm) and a FlexibleSearchLayout that contains a ModelBasedFlexibleSearchForm that allows the user to click together complex search filters.

Note that the SimpleSearchLayout is also re-used inside the ModelBasedSearchDialog that is used to perform searches for attributes that have their select mode set to LOOKUP.

Next, there is a BaseDetailsEditGrid that serves as the base class for grid components that are used inside edit forms (e.g. the ModelBasedEditForm). The BaseDetailsEditGrid has two subclasses:

  • DetailsEditGrid – for managing an in-memory collection of dependent entities.

  • ServiceBasedDetailsEditGrid – for managing a lazily loaded (and potentially very large) collection of dependent entities.

Finally, note that this diagram omits some of the less known components and some subclasses (e.g. the subclasses of BaseSplitLayout) in order to be able to focus on the main architecture.