Introduction

This tutorial covers the basics about the Holon platform Property model definition and management.

What You’ll learn

  • How to use the Property interface to define a data model

  • How to use a PropertySet to represent a model entity definition

  • How to work with VirtualProperty s

  • How to deal with Property configuration and presentation

  • How to use a PropertyBox to collect, inspect and transport the property values

  • How to obtain a PropertySet from a Java bean class

What You’ll need

  • About 30 minutes

  • A favorite text editor or IDE

  • JDK 1.8 or later

Reference documentation and source code

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>

3. Declare the holon-core artifact dependency:

<dependencies>

  <dependency>
    <groupId>com.holon-platform.core</groupId>
    <artifactId>holon-core</artifactId>
  </dependency>

</dependencies>

Step #1: The property model

Let’s suppose we have to manage a data entity which represents a simple product, with the following attributes:

  • The product ID (represented by a Java Long type)

  • The product description (represented by a Java String type)

  • The product category (represented by a Java String type)

  • The product unit price (represented by a Java Double type)

  • The product status (whether it was withdrawn from market or not) (represented by a Java Boolean type)

We’ll model the product entity using the Holon Platform Property model, using an interface named Product to collect the properties (create this class in the standard Maven src/main/java folder, using a package name of your choice):

public interface Product {

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

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

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

  public static final NumericProperty<Double> UNIT_PRICE = NumericProperty.doubleType("price");

  public static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn");

}

We use the PathProperty property type, which binds the property to the path which represents the attribute of the data entity. This could be, for example, the name of the column of a RDBMS table which represents the product data, or the attribute names of a JSON object, and so on. The Property model is completely abstract and not bound to any specific persistence representation or technology.

Each property declares the Java type of the value which represents, to allow type-consistent operations for property value management.

In this example, for each data model attribute the most suitable PathProperty sub type will be used, to rely on the simplified property creation methods and to provide a type specific query expression type.

See the Datastore API documentation and tutorials to learn how to bind a property model to a persistence source.

Step #2: Collect the properties in a PropertySet

Next, we define a PropertySet, which can be used to collect the properties in a suitable structure and to use them as a single entity.

Furthermore, the ID property will be declared as property set identifier property. The property set identifiers declaration can be useful for some specific Datastore implementations and data management structures.

public interface Product {

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

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

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

  public static final NumericProperty<Double> UNIT_PRICE = NumericProperty.doubleType("price");

  public static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn");

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

}
1 The PRODUCT property set represents the product entity
2 The ID property is declared as identifier property
In many of the sample code snippets of this tutorial the static fields of the Product class will be statically imported to make the code more readable and concise. To statically import the Product class fields you can use an import declaration like this: import static your.package.name.Product.*

Let’s create a unit test class to be used as a playground to try some of the operations provided by the Property and PropertySet APIs. The class has to be created under the default Maven test folder, i.e. src/test/java.

You can use JUnit to perform unit tests, declaring it as a depencency in the following way:

<dependency>
  <groupId>junit</groupId>
  <artifactId>junit</artifactId>
  <version>4.12</version>
  <scope>test</scope>
</dependency>

We’ll name the test class TestPropertyModel and create a property set test method like this:

@Test
public void propertySet() {
  for (Property<?> property : PRODUCT) { (1)
    // ...
  }

  PRODUCT.forEach(property -> { (2)
    // ...
  });

  boolean contains = PRODUCT.contains(ID); (3)
  assertTrue(contains);

  int size = PRODUCT.size(); (4)
  assertEquals(5, size);

}
1 Use a for-each loop to obtain the properties of the set
2 The same operation using the forEach() method and a lambda expression
3 Check whether the property set contains the ID property
4 Get the property set size (the number of properties of the set)

You can run the unit tests using the test Maven goal (mvn test) or using your favorite IDE command.

Step #3: Assign property captions

The Property interface extends the standard Holon Platform Localizable interface, which allows to represent a generic message, which can be localized using, for example, the Holon Platform LocalizationContext API.

A Localizable object is represented by a default message and an optional message code, which can be used to obtain a localized message according to a specific language.

We can use the Localizable message and message code attributes to assign a caption to a Property, which will represent the property description and which can be used in each level of an application stack, for example to show the property caption in a UI.

The message(…​) and messageCode(…​) property builder methods can be used to declare the property captions:

public interface Product {

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

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

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

  public static final NumericProperty<Double> UNIT_PRICE = NumericProperty.doubleType("price").message("Price")
      .messageCode("product.price");

  public static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn").message("Withdrawn")
      .messageCode("product.withdrawn");

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

}

Now we can inspect the property captions and use, for example, the Holon Platform LocalizationContext API to localize the property caption, for example:

@Test
public void propertyCaptions() {
  String caption = ID.getMessage(); (1)
  String captionMessageCode = ID.getMessageCode(); (2)

  assertEquals("Product ID", caption);
  assertEquals("product.id", captionMessageCode);

  String localizedCaption = LocalizationContext.translate(ID); (3)

}
1 Get the ID property default caption
2 Get the ID property caption message code
3 Use the LocalizationContext (which must be available as a Context resource and localized) to localize the ID property caption
See Holon Platform the Internationalization reference documentation for information about the LocalizationContext API

Step #4: Property validation

Properties support value validation through the Holon Platform Validator interface. One or more validators can be bound to a Property and invoked using the validate() method.

See Holon Platform the Validator reference documentation for information about data validation.

The following validation strategy will be defined:

  • The ID property value must not be null

  • The DESCRIPTION property value must be less than 500 characters long

  • The UNIT_PRICE property value must not be negative

public interface Product {

  public static final NumericProperty<Long> ID = NumericProperty.longType("id").message("Product ID")
      .message("Product ID").messageCode("product.id")
      // not null value validator
      .withValidator(Validator.notNull()); (1)

  public static final StringProperty DESCRIPTION = StringProperty.create("description").message("Description")
      .messageCode("product.description")
      // max 500 characters value validator
      .withValidator(Validator.max(500)); (2)

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

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

  public static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn").message("Withdrawn")
      .messageCode("product.withdrawn");

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

}
1 Add a not null validator to the ID property
2 Add a max validator to the DESCRIPTION property
3 Add a not negative validator to the UNIT_PRICE property

The validate() property method triggers the property validator and a ValidationException is thrown when the value validation fails.

Just like the Property caption, the ValidationException is localizable, i.e. it supports a localization message code, so the validation failure message can be localized using, for example, the LocalizationContext API:

try {
  DESCRIPTION.validate("A description"); (1)
} catch (ValidationException e) {
  String localizedMessage = LocalizationContext.translate(e); (2)
}
1 Validate the DESCRIPTION property against the given value
2 If the validation fails, a ValidationException is thrown and the validation failure message can be localized using the LocalizationContext API

Step #5: Using a PropertyBox

The PropertyBox interface represents the Holon Platform default property values container and manager. A PropertyBox instance is declared providing its property set, which represents the set of the properties that the PropertyBox manages, making available methods to read and write such property values.

The methods to read and write the property values are strongly typed, according to the property value type.

The PropertyBox object supports property validation, which is enabled by default. This means that when a property value is setted into a PropertyBox instance, any property Validator will be triggered, and a ValidationException is thrown in case of failure.

The setInvalidAllowed(…​) method can be used to disable property value validation.

Next we create a test method to show how to build and use the PropertyBox API:

@Test
public void propertyBox() {

  PropertyBox box = PropertyBox.create(PRODUCT); (1)
  box.setValue(ID, 1L);
  box.setValue(DESCRIPTION, "The product 1");
  box.setValue(CATEGORY, "C1");
  box.setValue(UNIT_PRICE, 12.77);
  box.setValue(WITHDRAWN, false);

  box = PropertyBox.builder(PRODUCT) (2)
      .invalidAllowed(true) (3)
      .set(ID, 1L).set(DESCRIPTION, "The product 1").set(CATEGORY, "C1").set(UNIT_PRICE, 12.77)
      .set(WITHDRAWN, false).build();

  Long id = box.getValue(ID); (4)

  assertEquals(Long.valueOf(1), id);
  assertEquals("The product 1", box.getValue(DESCRIPTION));
  assertEquals("C1", box.getValue(CATEGORY));

  Optional<Double> price = box.getValueIfPresent(UNIT_PRICE); (5)
  assertTrue(price.isPresent());
  assertEquals(Double.valueOf(12.77), price.get());

  assertTrue(box.containsValue(ID)); (6)

  PropertyBox cloned = box.cloneBox(); (7)

  assertEquals(Long.valueOf(1), cloned.getValue(ID));

}
1 Create a PropertyBox using the PRODUCT property set and set the property values
2 PropertyBox creation using the fluent builder
3 Set invalidAllowed to true to disable property value validation
4 Read the value of the ID property, which will of Long type according to the ID property type
5 The getValueIfPresent method version allows to obtain a property value as an Optional
6 Check whether the box contains the ID property and a not-null value for such property
7 Clone the PropertyBox into a new one

As said before, PropertyBox fully supports property validation. The overall PropertyBox validation can be performed using the validate() method: property validators are invoked for each of the property value contained in the box, and a ValidationException is thrown in case of validation failure.

@Test
public void propertyBoxValidation() {

  PropertyBox box = PropertyBox.builder(PRODUCT).set(ID, 1L).set(DESCRIPTION, "The product 1").set(CATEGORY, "C1")
      .set(UNIT_PRICE, 12.77).set(WITHDRAWN, true).build();

  box.validate(); (1)

}
1 Validate the PropertyBox

Step #6: Virtual properties

The VirtualProperty interface can be used to declare properties which are not directly connected with a value in read and write directions (a typical data model value), but rather they provide a calculated value.

The PropertyValueProvider functional interface must be used to provide the virtual property value and it’s getPropertyValue method supports a PropertyBox as argument, from which to obtain any other property value which can be used to generate the virtual property value.

For this reason, a VirtualProperty should be used in a suitable context, where the current PropertyBox can be provided to the virtual property value provider function. Obviously, the PropertyBox itself provides full support for virtual properties, providing its contents to the value provider method.

As an example, we’ll create a virtual property, named CATEGORY_DESCRIPTION, which provides a description for the product CATEGORY property value. In this example, the provided description is simply the category value (the code) with a "_description" suffix. In a real world scenario, the category description could be obtain, for example, by querying a data source.

Next, we’ll add the CATEGORY_DESCRIPTION property to the product property set. This is not required, but in this example we want to make the CATEGORY_DESCRIPTION property part of the product entity.

public interface Product {

  public static final NumericProperty<Long> ID = NumericProperty.longType("id").message("Product ID")
      .message("Product ID").messageCode("product.id")
      // not null value validator
      .withValidator(Validator.notNull());

  public static final StringProperty DESCRIPTION = StringProperty.create("description").message("Description")
      .messageCode("product.description")
      // max 500 characters value validator
      .withValidator(Validator.max(500));

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

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

  public static final BooleanProperty WITHDRAWN = BooleanProperty.create("withdrawn").message("Withdrawn")
      .messageCode("product.withdrawn");

  public static final VirtualProperty<String> CATEGORY_DESCRIPTION = VirtualProperty.create(String.class, (1)
      propertyBox -> propertyBox.getValueIfPresent(CATEGORY).map(category -> category + "_description") (2)
          .orElse("[No category]"));

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

}
1 Create a VirtualProperty which provides a String type value
2 The property value provider simply returns the category value with the _description suffix or [No category] if no category value is available from current PropertyBox
3 The CATEGORY_DESCRIPTION virtual property is added to the property set

Below a unit test example of the use of the CATEGORY_DESCRIPTION virtual property:

@Test
public void virtualProperty() {

  PropertyBox box = PropertyBox.builder(PRODUCT).set(ID, 1L).set(CATEGORY, "C1").build(); (1)

  String categoryDescription = box.getValue(CATEGORY_DESCRIPTION); (2)

  assertEquals("Category C1", categoryDescription);

}
1 Create a PropertyBox, setting the C1 value for the CATEGORY property
2 Get the CATEGORY_DESCRIPTION virtual value from the box

Step #7: Property configuration

Taking the example above, let’s assume we would like to use a service to obtain the category description (maybe performing a query on a data source) and we would like to generalize the dataset structure like this:

  • A dataset is a list a values, identified by a String code and a corresponding description

  • Each dataset is identified by a String name

We suppose the product category is bound to a dataset named CATEGORY, and a DatasetService is available to obtain the description of a dataset value, given the dataset name.

The DatasetService interface could be defined as follows:

public interface DatasetService {

  /**
   * Get the dataset value description for given dataset <code>name</code>.
   * @param datasetName Dataset name to which the value belongs
   * @param value The value for which to obtain the description
   * @return The description of the value
   */
  String getDescription(String datasetName, String value);

}

And a simple implementation:

public class DatasetServiceImpl implements DatasetService {

  @Override
  public String getDescription(String datasetName, String value) {
    // here it should be written the "real" logic to retrieve the value description, maybe from a data source
    if ("CATEGORY".equals(datasetName)) {
      return "Category " + value;
    }
    return null;
  }

}

Now, to declare the dataset name to which a property value is bound, we’ll use the property configuration, setting a parameter named DATASET with a value which represents the dataset name.

Regarding the product CATEGORY property, the parameter value will be CATEGORY:

public static final StringProperty CATEGORY = StringProperty.create("category").message("Category")
    .messageCode("product.category") //
    .withConfiguration("DATASET", "CATEGORY"); (1)
1 Add a property configuration parameter to declare the dataset name to which the value is bound

To obtain the DatasetService instance, the Holon Platform context resources will be used. We register a DatasetServiceImpl instance as a ClassLoader-scoped resource, using the DatasetService class name as resource key.

For example:

Context.get().classLoaderScope()
    .ifPresent(scope -> scope.put(DatasetService.class.getName(), new DatasetServiceImpl()));
See Holon Platform the Context reference documentation for information about context resources.

Finally, we modify the CATEGORY_DESCRIPTION virtual property definition to use the DatasetService to obtain the the category value description:

public static final VirtualProperty<String> CATEGORY_DESCRIPTION = VirtualProperty.create(String.class,
    propertyBox -> propertyBox.getValueIfPresent(CATEGORY)
        // get the DatasetService as Context resource
        .map(category -> Context.get().resource(DatasetService.class)
            .orElseThrow(() -> new IllegalStateException("The DatasetService resource is missing"))
            // get the value description using DatasetService "getDescription" method
            .getDescription("CATEGORY", category))
        // if the value is not present return a default description
        .orElse("[No category]"));

Step #8: Property value presentation

The Holon platform provides a standard way to present the value of a Property as a String using a PropertyValuePresenter.

See Holon Platform the Property presentation reference documentation for more information.

A default presenter is provided by the platform and automatically registered in the default registry. For example, for boolean property types, the true or false String is returned according to the property value.

Assume we want to change this behaviour only for the WITHDRAWN product property in the following way:

  • When true, return "The product was withdrawn"

  • When false, return "The product is available"

We can create a custom PropertyValuePresenter like this:

public class WithdrawnPropertyPresenter implements PropertyValuePresenter<Boolean> {

  @Override
  public String present(Property<Boolean> property, Boolean value) {
    return value ? "The product was withdrawn" : "The product is available";
  }

}

Let’s make a unit test method to see how the presenter behaves:

@Test
public void propertyPresenter() {

  String presentation = WITHDRAWN.present(Boolean.TRUE); (1)
  assertEquals("true", presentation);

  PropertyValuePresenterRegistry.get().register(p -> WITHDRAWN.equals(p), new WithdrawnPropertyPresenter()); (2)

  presentation = WITHDRAWN.present(Boolean.TRUE); (3)
  assertEquals("The product was withdrawn", presentation);

  PropertyBox box = PropertyBox.builder(PRODUCT).set(ID, 1L).set(WITHDRAWN, true).build();

  presentation = box.present(WITHDRAWN); (4)
  assertEquals("The product was withdrawn", presentation);

}
1 Presentation using the default presenter: returns the true String
2 Register the WithdrawnPropertyPresenter bound only to the WITHDRAWN property using the given condition
3 Present the true value again: now we should obtain the "The product was withdrawn" String
4 The same value presentation using a PropertyBox: the presented value will be the one currently available form the PropertyBox

Step #9: Get a Property set from a Java bean

The last step of this tutorial shows how to obtain a Property set from a Java bean, using standard Holon Platform introspectors.

Let’s define a simple Java bean which represents the product entity:

public class Product {

  private Long id;

  private String description;

  private String category;

  private Double unitPrice;

  private boolean withdrawn;

  public Product() {
    super();
  }

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getDescription() {
    return description;
  }

  public void setDescription(String description) {
    this.description = description;
  }

  public String getCategory() {
    return category;
  }

  public void setCategory(String category) {
    this.category = category;
  }

  public Double getUnitPrice() {
    return unitPrice;
  }

  public void setUnitPrice(Double unitPrice) {
    this.unitPrice = unitPrice;
  }

  public boolean isWithdrawn() {
    return withdrawn;
  }

  public void setWithdrawn(boolean withdrawn) {
    this.withdrawn = withdrawn;
  }

}

The bean class PropertySet can be obtained using the create(…​) method of the BeanPropertySet interface. A Property, with a consistent type, will be created for each valid bean property detected in the bean class.

The BeanPropertySet API provides methods to obtain the Property which corresponds to a bean property using the bean property name, and to convert a bean instance into a PropertyBox and vice-versa.

We’ll use the unit test class to try the BeanPropertySet operations:

@Test
public void beanProperties() {

  // Obtain the PropertySet of the TestBean class
  final BeanPropertySet<Product> BEAN_PROPERTIES = BeanPropertySet.create(Product.class);

  // A BeanPropertySet is a standard PropertySet
  PropertySet<?> set = BEAN_PROPERTIES;
  int size = set.size();

  // expect 5 properties
  assertEquals(5, size);

  // Get a bean property as a typed PathProperty
  PathProperty<Long> ID = BEAN_PROPERTIES.property("id", Long.class);

  assertNotNull(ID);

  // check all expected properties are in the set
  assertTrue(BEAN_PROPERTIES.contains(ID));
  assertTrue(BEAN_PROPERTIES.contains(BEAN_PROPERTIES.property("description")));
  assertTrue(BEAN_PROPERTIES.contains(BEAN_PROPERTIES.property("category")));
  assertTrue(BEAN_PROPERTIES.contains(BEAN_PROPERTIES.property("unitPrice")));
  assertTrue(BEAN_PROPERTIES.contains(BEAN_PROPERTIES.property("withdrawn")));

  // The @Caption annotation can be used to set property (localizable) captions
  String caption = ID.getMessage();
  assertEquals("Product ID", caption);

  String translationMessageCode = ID.getMessageCode();
  assertEquals("product.id", translationMessageCode);

  // Read and write single bean property values
  Product bean = new Product();
  bean.setDescription("Bean description");

  // write the ID property value
  BEAN_PROPERTIES.write(ID, Long.valueOf(2), bean);
  // write the ID property value using the "id" path
  BEAN_PROPERTIES.write("id", Long.valueOf(2), bean);

  // read the ID property value
  Long idValue = BEAN_PROPERTIES.read(ID, bean);
  assertEquals(Long.valueOf(2), idValue);

  // The PropertyBox API is fully supported to get and set bean property values

  // read the bean instance as a PropertyBox
  PropertyBox box = BEAN_PROPERTIES.read(bean);

  assertEquals(Long.valueOf(2), box.getValue(ID));
  assertEquals("Bean description", box.getValue(BEAN_PROPERTIES.property("description")));

  // write a PropertyBox into a TestBean instance
  Property<Double> PRICE = BEAN_PROPERTIES.property("unitPrice", Double.class);

  PropertyBox box2 = PropertyBox.builder(BEAN_PROPERTIES).set(ID, 3L).set(PRICE, Double.valueOf(12.65)).build();

  Product bean2 = BEAN_PROPERTIES.write(box2, new Product());

  assertEquals(Long.valueOf(3), bean2.getId());
  assertEquals(Double.valueOf(12.65), bean2.getUnitPrice());

}

Property caption, configuration and validation

The Holon Platform provides annotations which can be used on bean fields to declare the property captions and to set simple property configuration parameters. Furthermore, the standard Java Bean Validation (JSR 303) constraint annotations are supported to declare property value validators.

So, you modify the Product bean class to reflect the property captions, configuration and validation to obtain the same property model configuration created in the previous tutorial steps, using:

  • The @Caption annotation to declare the property captions

  • The @Config annotation to declare the DATASET configuration parameter

  • The @NotNull, @Max and @PositiveOrZero to add property validation

public class Product {

  @NotNull
  @Caption(value = "Product ID", messageCode = "product.id")
  private Long id;

  @Max(500)
  @Caption(value = "Description", messageCode = "product.description")
  private String description;

  @Config(key = "DATASET", value = "CATEGORY")
  @Caption(value = "Category", messageCode = "product.category")
  private String category;

  @PositiveOrZero
  @Caption(value = "Price", messageCode = "product.price")
  private Double unitPrice;

  @Caption(value = "Withdrawn", messageCode = "product.withdrawn")
  private boolean withdrawn;

  // getters and setters omitted

Summary

You’ve learned how to use the Property model to define and configure a simple data entity, which can be represented through a PropertySet and managed using a PropertyBox to collect, inspect and trasport the property values.

Futhermore, you’ve learned how to use property value presenters to display a Property value.

Finally, you’ve learned how to obtain and configure a Property model from a Java bean class.

See also

The source code of this tutorial is available here.

See the Holon Platform tutorials to explore other available tutorials.