Introduction

This tutorial shows how to use the Holon Platform Property model, the Datastore API and the Vaadin UI Module UI module to build a simple web application, leveraging on Spring Boot to auto-configure and run the application using an embedded servlet container.

The web application purpose is to manage a simple product entity, persisted in a RDBMS, allowing the user to list the available products, display product data, create a new product, update an existing product and delete a product.

What You’ll learn

  • How to configure and build a web UI using the Holon Platform Vaadin module

  • How to use the Holon Platform ViewNavigator to setup and manage the application views (or virtual pages)

What You’ll need

  • About 30 minutes

  • A favorite text editor or IDE

  • JDK 1.8 or later

Reference documentation and source code

The source code for this tutorial is available on GitHub in the Holon Platform examples repository:

Project setup

We’ll use Maven to create and setup the example project of this tutorial. So, create a standard Maven project and use the following decalrations in your pom.xml to obtain the required Holon Platform artifacts:

2. Use a holon.platform.version property to declare the Holon Platform version number in the properties section, for example:

<properties>
  <holon.platform.version>5.2.1</holon.platform.version>
</properties>
See Getting started to know about the last Holon Platform release version.

2. Import the Holon Platform BOM in the dependencyManagement section, using the last platform version:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.holon-platform</groupId>
      <artifactId>bom</artifactId>
      <version>${holon.platform.version}</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

Step #1: Datastore configuration

We’ll use the JDBC Datastore implementation to manage data persistence, auto-configured and enabled using the Holon Platform Spring Boot starter that includes the HikariCP DataSource.

So you want to add the following depencency to your project pom:

<dependencies>

  <dependency>
    <groupId>com.holon-platform.jdbc</groupId>
    <artifactId>holon-starter-jdbc-datastore-hikaricp</artifactId>
  </dependency>

</dependencies>

We’ll use H2 as in-memory database engine, so you want to add the following dependency:

<dependency>
  <groupId>com.h2database</groupId>
  <artifactId>h2</artifactId>
  <version>1.4.193</version>
</dependency>

Next you want to create a schema.sql script (in the default Maven src/main/resources resources folder) to create the products database table:

create table products (
  id bigint primary key auto_increment,
  sku varchar(100) not null,
  description varchar(500),
  category varchar(10),
  price double,
  withdrawn integer default 0
)

We also provide a data.sql script to store some example product data in the products table:

INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-1', 'Product 1', 'C1', 19.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-2', 'Product 2', 'C2', 29.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-3', 'Product 3', 'C1', 15.00, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-4', 'Product 4', 'C2', 75.50, 1);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-5', 'Product 5', 'C3', 19.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-6', 'Product 6', 'C1', 39.90, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-7', 'Product 7', 'C4', 44.20, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-8', 'Product 8', 'C1', 77.00, 0);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-9', 'Product 9', 'C3', 23.70, 1);
INSERT INTO products (sku,description,category,price,withdrawn) VALUES ('product-10', 'Product 10', 'C2', 92.20, 0);

Finally, we configure a DataSource using Spring Boot auto-configuration through the following configuration properties in the application.yml file:

spring:
  datasource:
    url: "jdbc:h2:mem:test"
    username: "sa"

Step #2: The property model

Now you want to create the product entity Property model using an interface named Product, which will contain:

  • The product entity properties, declared through a suitable PathProperty type, where each property path name correspond to the products database table column name. A property caption message is specified for each property and later used by the UI components.

  • The PropertySet which represents the product entity, where the the ID property is declared as identifier property.

  • A DataTarget named products to be used with the Datastore API to refer to the products database table.

public interface Product {

  public static final NumericProperty<Long> ID = NumericProperty.longType("id").message("Product ID"); (1)

  public static final StringProperty SKU = StringProperty.create("sku").message("SKU");

  public static final StringProperty DESCRIPTION = StringProperty.create("description").message("Description");

  public static final StringProperty CATEGORY = StringProperty.create("category").message("Category");

  public static final NumericProperty<Double> UNIT_PRICE = NumericProperty.doubleType("price").message("Price")
      // not negative value validator
      .withValidator(Validator.notNegative()); (2)

  public static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn").message("Withdrawn")
      // set a property value converter from Integer model type to Boolean
      .converter(PropertyValueConverter.numericBoolean(Integer.class)); (3)

  // Product property set
  public static final PropertySet<?> PRODUCT = PropertySet
      .builderOf(ID, SKU, DESCRIPTION, CATEGORY, UNIT_PRICE, WITHDRAWN).identifier(ID).build();

  // "products" DataTarget
  public static final DataTarget<?> TARGET = DataTarget.named("products");

}
1 To each property is assigned a caption through the message(…​) builder method
2 Add a property value validator to check the price is not negative: this validator will be inherited from input type UI components
3 Set a property value converter from Integer model type (the database column type) to Boolean property type
See the property model tutorial to learn the basics about the Holon Platform property model.

Step #3: UI configuration

Now it’s time to configure the Vaadin application UI using the Holon Platform Vaadin module and the Spring Boot auto-configuration support.

First of all, you wan to add the Spring Boot starter dependency to the project pom:

<dependency>
  <groupId>com.holon-platform.vaadin</groupId>
  <artifactId>holon-starter-vaadin</artifactId>
</dependency>

Next you want to create the Vaddin application entry point, i.e. the UI class:

@SpringUI (1)
@SpringViewDisplay (2)
@Theme("valo") (3)
public class UI extends com.vaadin.ui.UI {

  @Override
  protected void init(VaadinRequest request) {
  }

}
1 Declare the UI class to be automatically detected and configured by Spring
2 Use the UI itself as view display, i.e. as the View content container
3 Set valo as theme

Then create a simple Spring Boot Application class, which will be used to enable the auto-configuration features and run the application using an embedded servlet container:

@SpringBootApplication
public class Application {

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

}

Step #4: Create the application views

After the configuration phase has ended, it’s time to create the application views, i.e. the virtual pages which represent the application functions and contents.

Using Spring Boot, the Holon Platform ViewNavigator is automatically configured and setted up. This will allow us to use some of the ViewNavigator features, such as the @DefaultView configuration, the @OnShow view lifecycle hook, the @ViewParameter handling and the ability to automatically open a view in a application window.

See the Holon Vaadin UI reference manual for detailed documentation about the Holon Platform Vaadin module and the ViewNavigator API.

We’re going to create three views:

  • Home : the home view, listing all the available products and allowing to display the product data by clicking on a row

  • View : displays a product in an application window, providing the edit and delete operations

  • Manage : manages the product data, providing an input form to create or update a product

Each view is enabled as a Spring bean, so dependency injection and bean scope configuration are available.

The Home view

The Home view is the application default view, i.e. the view displayed when a navigation path is not specified, for example at first application display, for this reason it’s annotated with @DefaultView.

The @UIScope annotation on the Home view class is used to declare the Spring scope of the View, which in this case is the ui scope, so that the View instance lifecycle will match the application UI lifecyle.

By default, the View bean lifecycle matches the navigation step range, i.e. a new instance of the View is created when the view in displayed and destroyed when hidden (the navigation proceeds to another View).

This view lists the available products using a Holon Platform PropertyListing component. The PRODUCT PropertySet is configured as the listing property set, so the listing columns will be generated for all the properties that are part of the PRODUCT property set.

The product property captions (configured through the message(…​) property builder method in the Product model) will be used by default as column headers.

A Datastore is obtained by injection and used as the listing data source, providing the product TARGET as query target. Since the PRODUCT property set declares the ID property as identifier, its values will be used as item identifiers in the listing.

An item click listener is configured so that when to user clicks a listing row, the application View named view is opened, providing the id parameter value which represents the ID property value of the product to display. To navigate to such view, the Holon Platform ViewNavigator is used, which is statically obtained from the current platform context.

Furthermore, a "Add new" button is added at the top of the View content, to allow the user to create a new product. When the user clicks the button, the manage

So you want to create a Home class this way:

@DefaultView
@UIScope
@SpringView(name = "home") (1)
public class Home extends VerticalLayout implements View {

  private static final long serialVersionUID = 1L;

  @Autowired
  private Datastore datastore;

  @PostConstruct
  public void init() {
    Components.configure(this)
        // set full to view content
        .fullSize().spacing().add(Components.button().caption("Add new").styleName(ValoTheme.BUTTON_PRIMARY)
            // navigate to "manage" view
            .onClick(e -> ViewNavigator.require().toView("manage").navigate()).build())
        // build and add listing
        .addAndExpandFull(Components.listing.properties(PRODUCT)
            // setup data source using Datastore with 'products' table name target and product ID as pk
            .dataSource(datastore, TARGET, ID)
            // froze the ID column
            .frozenColumns(1)
            // set the ID column width and style
            .width(ID, 120).style(ID, "id-column")
            // when user clicks on a row, open the 'view' named View providing product id parameter
            .withItemClickListener((i, p, x, e) -> ViewNavigator.require().toView("view")
                .withParameter("id", i.getValue(ID)).navigate())
            // set full size and build
            .fullSize().build());
  }

}
1 The @SpringView annotation is used to declare the view as a Spring bean, enable the view in the ViewNavigator and assign the view name

The View view

The view application View is used to display the product data, using the Holon Platform PropertyViewForm, which is able to display a set of Property values. The PropertyViewForm is configured with the PRODUCT property set and built using the fluent component builder this way:

Components.view.form().fullSize().properties(PRODUCT).build()

Then we add two buttons at the bottom of the view:

  • Edit: To edit the product data. When the user clicks the button, the ViewNavigator is used to navigate to the manage view, providing an id parameter with the value of the displayed product ID property

  • Delete: To delete the displayed product. When the user clicks the button, a question dialog is opened to request confirmation. If the user clicks yes on the question dialog, the current product is deleted using the Datastore API with the PropertyBox provided by the PropertyViewForm. Then the navigateBack() method of the ViewNavigator is used to navigate back to the previous View.

A view parameter is declared using the @ViewParameter annotation on a View field. The field is named id and it’s of type Long. The Holon Platform ViewNavigator will automatically bind the view parameter value named id, provided with the navigation request, into that field.

The id parameter is used to obtain the id of the product to display. The method annotated with @OnShow is invoked when the View is displayed and loads in the PropertyViewForm the PropertyBox of the product with such id using the Datastore API.

Finally, the view is annotated with @WindowView, instructing the ViewNavigator to display the View contents in an application (modal) Window. The window can be configured using the annotation and the @Caption annotation can be used to customize the Window header message.

@WindowView(windowWidth = "500px", windowHeigth = "400px")
@Caption("View product")
@SpringView(name = "view")
public class View extends VerticalLayout implements com.vaadin.navigator.View {

  private static final long serialVersionUID = 1L;

  @ViewParameter
  private long id;

  @Autowired
  private Datastore datastore;

  private final PropertyViewForm viewForm;

  public View() {
    super();
    Components.configure(this)
        // set margins and size full to view content
        .margin().fullSize()
        // add view form using Product property set
        .addAndExpandFull(viewForm = Components.view.form().fullSize().properties(PRODUCT).build()).add(
            // horizontal layout as bottom toolbar
            Components.hl().fullWidth().spacing().styleName(ValoTheme.WINDOW_BOTTOM_TOOLBAR)
                // EDIT action
                .add(Components.button().caption("Edit").fullWidth().styleName(ValoTheme.BUTTON_PRIMARY)
                    // navigate to "manage" view providing product id parameter
                    .onClick(e -> ViewNavigator.require().toView("manage").withParameter("id", id)
                        .navigate())
                    .build())
                // DELETE action
                .add(Components.button().caption("Delete").fullWidth()
                    .styleName(ValoTheme.BUTTON_DANGER)
                    // ask confirmation before delete
                    .onClick(e -> Components.questionDialog().message("Are you sure?")
                        .callback(answeredYes -> {
                          // if confirmed, delete the current form product PropertyBox
                          datastore.delete(TARGET, viewForm.getValue());
                          // navigate back
                          ViewNavigator.require().navigateBack();
                        }).open())
                    .build())
                .build());
  }

  @OnShow
  public void onShow() {
    // set the view form product value
    viewForm.setValue(
        // load product using id parameter
        datastore.query().target(TARGET).filter(ID.eq(id)).findOne(PRODUCT)
            // throw an exception if not found
            .orElseThrow(() -> new DataAccessException("Product not found: " + id)));
  }

}

The Manage view

The last view is the manage view, who will take care of product data input, allowing to create a new product or update an existing one.

A Holon Platform PropertyInputForm component is used to allow the user to input the product property values and it is configured using the PRODUCT property set. This way, an input component for each property of the set is created and displayed on the UI.

Some additional configuration is provided to the input form:

  • The product ID property is setted as read-only, so that it’s value cannot be modified by the user, since the product id value is auto-generated by the database

  • The SKU property is configured as required: a validator to check the value is not null will be automatically added and the required indicator symbol will be displayed next to the input field

  • A validator is configured for the DESCRIPTION property, to check the value is minimum three characters long

Any Property specific validator is inherited by the input form, so, for example, the not negative price validator configured for the UNIT_PRICE property in the Product model class will be inherited by the corresponding form input component.

The product property captions (configured through the message(…​) property builder method in the Product model) will be used by default as the form input component captions.

A Save action button is provided to allow the user to save a new product or confirm product data update. When the user clicks the button, the Datastore API is used to save the product data, obtained as a PropertyBox form the input form. Then the user is redirected to the application home view, using the navigateToDefault() method of the ViewNavigator.

Just like the View class, the product id to use when the product data has to be updated is provided using a @ViewParameter field named id of type Long, automatically setted by the ViewNavigator according to the navigation request, if available.

At view display, the @OnShow annotated method checks if the id parameter value is available, and if so retrieve the product data which corresponds to given id using the Datastore API and loads it into the PropertyInputForm.

The manage view is annotated with @VolatileView, excluding it form the navigation history, so that it won’t be recalled when navigating back in history.

@VolatileView
@SpringView(name = "manage")
public class Manage extends VerticalLayout implements com.vaadin.navigator.View {

  private static final long serialVersionUID = 1L;

  @ViewParameter
  private Long id;

  @Autowired
  private Datastore datastore;

  private PropertyInputForm form;

  private Button clearButton;

  @PostConstruct
  public void init() {
    Components.configure(this)
        // set margins and size full to view content
        .margin().fullSize().addAndExpandFull(
            // add a form using Product property set
            form = Components.input.form().fullSize().properties(PRODUCT)
                // set id as read-only
                .readOnly(ID)
                // set SKU as required
                .required(SKU)
                // add a validator to check DESCRIPTION with minimum 3 characters
                .withValidator(DESCRIPTION, Validator.min(3))
                // build the form
                .build())
        .add(Components.hl().margin().spacing()
            // SAVE action
            .add(Components.button().caption("Save").styleName(ValoTheme.BUTTON_PRIMARY)
                .onClick(e -> save()).build())
            // CLEAR action
            .add(clearButton = Components.button().caption("Clear")
                // clear the form
                .onClick(e -> form.clear()).build())
            .build());
  }

  @OnShow
  public void load() {
    // if id parameter is not null, we are in edit mode
    if (id != null) {
      // load the product data
      form.setValue(datastore.query().target(TARGET).filter(ID.eq(id)).findOne(PRODUCT)
          // throw an exception if a product with given id was not found
          .orElseThrow(() -> new DataAccessException("Data not found: " + id)));
    }
    // enable the Clear button if not in edit mode
    clearButton.setVisible(id == null);
  }

  private void save() {
    // check valid and get PropertyBox value
    form.getValueIfValid().ifPresent(value -> {

      // save and notify
      datastore.save(TARGET, value, DefaultWriteOption.BRING_BACK_GENERATED_IDS);
      // notify the saved id
      Notification.show("Saved [" + ((id != null) ? id : value.getValue(ID)) + "]");

      // go back home
      ViewNavigator.require().navigateToDefault();
    });

  }

}

Step #5: Run the application

Since we used Spring Boot to configure the web application, just run the Application class to start the embedded servlet container and deploy the application. Next, open the following URL in your web browser to display the application:

Summary

You’ve learned how to use the Holon Platform Vaadin module to create and run a web application which allows to manage a simple data entity, leveraging on the Property model and the Datastore API to configure the UI components and manage the data persistence in a RDBMS.

See also

The source code of this tutorial is available on GitHub.

See the Vaadin Flow web application tutorial to learn how to use Vaadin Flow to build and run a simple web application.

See the Holon Platform tutorials to explore other available tutorials.