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
-
See Holon Core module reference manual for detailed documentation
-
The source code which refers to 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.6</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 benull
-
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 theDATASET
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.