Copyright © 2016-2017

Copies of this document may be made for your own use and for distribution to others, provided that you do not charge any fee for such copies and further provided that each copy contains this Copyright Notice, whether distributed in print or electronically.

1. Introduction

The Holon Platform Core module represents the platform foundation, providing the definition of the overall architecture, base structures and APIs.

2. Obtaining the artifacts

The Holon Platform uses Maven for projects build and configuration. All the platform artifacts are published in the Maven Central Repository, so there is no need to explicitly declare additional repositories in your project pom file.

At the top of each section of this documentation you will find the Maven coordinates (group id, artifact id and version) to obtain the artifact(s) as a dependency for your project.

A BOM (Bill Of Materials) pom is provided to import the available dependencies for a specific version in your projects. The Maven coordinates for the core BOM are the following:

Maven coordinates:

<groupId>com.holon-platform.core</groupId>
<artifactId>holon-bom</artifactId>
<version>5.0.0</version>

The BOM can be imported in a Maven project in the following way:

<dependencyManagement>
  <dependencies>
    <dependency>
      <groupId>com.holon-platform.core</groupId>
      <artifactId>holon-bom</artifactId>
      <version>5.0.0</version>
      <type>pom</type>
      <scope>import</scope>
    </dependency>
  </dependencies>
</dependencyManagement>

2.1. Using the Platform BOM

The Holon Platform provides an overall Maven BOM (Bill of Materials) to easily obtain all the available platform artifacts.

3. Core API, services and components

3.1. Introduction

The holon-core artifact is the Holon platform core API and implementation asset, defining and providing the main platform architecture concepts and structures. All other platform artifacts derive from this one and declares it as a dependency.

Maven coordinates:

<groupId>com.holon-platform.core</groupId>
<artifactId>holon-core</artifactId>
<version>5.0.0</version>

3.2. Context

The entry point of the context API is the Context interface.

The Context behaves as a generic resources registry and provider. A resource can be any Java class instance, and it’s identified by a String key. The Context allows resource registration and retrieving in a static way, using a classloader-scoped default singleton instance of the registry, which can be obtained using the get() method:

Context currentContext = Context.get(); (1)
1 Get the current Context instance

3.2.1. Scopes

The Context is organized in scopes, represented by the ContextScope interface. Each scope act as a sub-registry of resources, with it’s own namespace. A scope is identified by a name and has an assigned priority level. The priority level is an integer number, following standard priority conventions, where the highest priority corresponds to the lowest value, and vice-versa.

When a resource is requested from Context, the lookup process is the following:

  • Each registered scope is queried to obtain the resource, starting from the scope with the highest priority

  • The returned resource instance is the one obtained from the first scope which provides a resource instance bound to the requested key, if any.

A registered scope can be obtained from Context using:

Get a registered ContextScope
Optional<ContextScope> scope = Context.get().scope("scopeName"); (1)

scope = Context.get().scope("scopeName", aClassLoader); (2)
1 Get a scope by name using the default ClassLoader
2 Get a scope by name using a specific ClassLoader

The ContextScope interface can be used to obtain, register and remove a scoped resource. Resource registration and removal are optional operations, so the concrete scope implementation could not support them, throwing a UnsupportedOperationException.

Default Scopes

The platform provides two default scopes, automatically registered and made available to current Context:

  1. A ClassLoader-bound scope, which handles resources as singleton instances for the reference ClassLoader, that is at most one resource instance for a given key is present in the scope for a specific ClassLoader. This scope is registered with a low precedence order. The scope name is provided by the constant CLASSLOADER_SCOPE_NAME;

  2. A Thread-bound scope, which binds resources to current Thread using ThreadLocal variables. This scope is registered with a high precedence order. The scope name is provided by the constant THREAD_SCOPE_NAME.

The Context interface provides some useful methods to access the special Thread-bound scope, since it could be highly mutable during an application lifecycle:

Using the current Thread scope
Optional<ContextScope> threadScope = Context.get().threadScope(); (1)

threadScope = Context.get().threadScope(aClassLoader); (2)

Context.get().executeThreadBound("resourceKey", resourceInstance, () -> {
  // do something (3)
});

Context.get().executeThreadBound("resourceKey", resourceInstance, () -> {
  // do something (4)
  return null;
});
1 Get the Thread scope using the default ClassLoader
2 Get the Thread scope using a specific ClassLoader
3 Execute a Runnable operation, binding the resource instance with given resourceKey to current Thread before execution and removing the binding after operation execution
4 Execute a Callable operation, binding the resource instance with given resourceKey to current Thread before execution and removing the binding after operation execution
Context extension: adding Scopes

Additional ContextScope implementations can be added to the default platform Context by using standard Java service extensions.

1. Create a class which implements the ContextScope interface:

public class MyContextScope implements ContextScope {

  @Override
  public String getName() {
    return "MY_SCOPE_NAME"; (1)
  }

  @Override
  public int getOrder() {
    return 100; (2)
  }

  @Override
  public <T> Optional<T> get(String resourceKey, Class<T> resourceType) throws TypeMismatchException {
    return Optional.empty(); (3)
  }

  @Override
  public <T> Optional<T> put(String resourceKey, T value) throws UnsupportedOperationException { (4)
    throw new UnsupportedOperationException(); // implement this method to allow resource registration
  }

  @Override
  public <T> Optional<T> putIfAbsent(String resourceKey, T value) throws UnsupportedOperationException { (4)
    throw new UnsupportedOperationException(); // implement this method to allow resource registration
  }

  @Override
  public boolean remove(String resourceKey) throws UnsupportedOperationException { (4)
    throw new UnsupportedOperationException(); // implement this method to allow resource removal
  }

}
1 The scope name: must be unique among all registered context scopes
2 The scope ordering number: the lower is the value, the higher is the scope priority in resources lookup
3 Implement a meaningful logic to provide the resource identified by given resourceKey and of the given resourceType type, if available in scope
4 If the scope allows direct resource registration, implement the resource management methods

2. Create a file named com.holonplatform.core.ContextScope containing the fully qualified class name(s) of the scope implementation and put it under the META-INF/services folder of your project to register the scope in the default Context.

3.2.2. Obtaining Context resources

Most of the times the Context is used to request a resource using a key, obtaining the most suitable resource instance according to registered scopes and current application environment configuration. To obtain a resource, the resource(…​) methods are provided:

Get a Context resource
Optional<ResourceType> resource = Context.get().resource("resourceKey", ResourceType.class); (1)

resource = Context.get().resource("resourceKey", ResourceType.class, aClassLoader); (2)

resource = Context.get().resource(ResourceType.class); (3)
1 Get the resource identified by the given resourceKey and of the specified type, using the default ClassLoader
2 Get the resource identified by the given resourceKey and of the specified type, using the specified ClassLoader
3 Get the resource of the specified type using the default ClassLoader. The resource key is assumed to be the fully qualified resource class name
The platform standard interfaces which are candidates to be Context resources, provides a convenience getCurrent() static method to obtain the current implementation available from Context, if present.

3.2.3. Default ClassLoader

When referring to the default ClassLoader, it means the ClassLoader obtained by using the following lookup strategy:

  • The current Thread context ClassLoader, if available

  • The ClassLoader with which the platform core classes are loaded, if available

  • The System ClassLoader if no other available

3.3. Configuration and parameters

Holon platform relies on some common structures to define and provide configuration properties and parameters used by platform modules.

3.3.1. Configuration

Configuration properties definition, provision and management is supported by the following three API interfaces:

  • ConfigProperty: represents a configuration property, identified by a String key and with a specified type;

  • ConfigPropertyProvider: provides the values for a set of configuration properties:

  • ConfigPropertySet: a configuration property set definition, bound to one or more providers to provide the property values.

Configuration property

A configuration property is represented by the ConfigProperty interface. A configuration property is identified by a String key and is bound to a predefined type.

ConfigProperty definition
ConfigProperty<String> property = ConfigProperty.create("test", String.class); (1)

String key = property.getKey(); (2)

Class<String> type = property.getType(); (3)
1 Create a configuration property of String type with given key
2 Get the configuration property key
3 Get the configuration property type
Configuration property provider

The ConfigPropertyProvider interface represents a value provider for a set of configuration properties, allowing to read the configuration properties values from different sources.

Each concrete implementation is able to read configuration properties values from a specific source, for example an in-memory key-value map or a properties file. Automatic type conversions from String property source values to required configuration property types are performed when applicable.

The platform provides some useful builtin property values providers as shown below:

Builtin configuration property providers
Map<String, Object> values = new HashMap<>();
ConfigPropertyProvider provider = ConfigPropertyProvider.using(values); (1)

Properties properties = new Properties();
provider = ConfigPropertyProvider.using(properties); (2)

provider = ConfigPropertyProvider.using("config.properties", ClassUtils.getDefaultClassLoader()); (3)

provider = ConfigPropertyProvider.usingSystemProperties(); (4)
1 Provider which uses an in memory key-value map as property values source
2 Provider which uses a Properties instance as property values source
3 Provider which uses a properties File as property values source
4 Provider which uses Java System properties as property values source
Configuration property set

The ConfigPropertySet interface represents a configuration property set bound to one or more ConfigPropertyProvider property source.

Each set is identified by a String name, used as a prefix for all the configuration properties of the set. Property name segments are separated by convention by a dot, so, for each property key, the property set will lookup for a property named set_name*.*property_name in the property source.

Platform elements which support a configuration property set provide a specific ConfigPropertySet extension to list all supported configuration properties and, in some cases, helper methods to obtain frequently used configuration properties values.

3.3.2. ParameterSet

A ParameterSet is the representation of a generic parameter name and value map.

It provides methods to inspect the parameter set and obtain parameter values of a specific type and completely supports ConfigProperty representation as a parameter reference.

ParameterSet examples
final ConfigProperty<String> property = ConfigProperty.create("test", String.class);

ParameterSet set = ParameterSet.builder()
    .parameter("testParameter", 1L) (1)
    .parameter(property, "testValue") (2)
    .build();

boolean present = set.hasParameter("testParameter"); (3)
present = set.hasNotNullParameter("testParameter"); (4)

set.getParameter(property).ifPresent(p -> p.toString()); (5)
1 Add a Long value parameter using a String parameter name
2 Add a parameter value using a ConfigProperty. Since the configuration property is of String type, only a String type value is admitted
3 Check if the parameter set contains a parameter identified by a name
4 Check if the parameter set contains a parameter identified by a name and it’s value is not null
5 Check if the parameter set contains a parameter identified by given configuration property: if present, perform an operation

3.4. Data validation

The main entry point to configure and perform data validation in Holon Platform is the Validator interface.

3.4.1. Validator

A Validator is an interface implemented by a class which performs the validation of a value. The Validator is generalized on the value type which the validator is able to validate and can be used as a functional interface, providing the single method validate(T value), which is responsible for the actual validation operation and throws a ValidationException if the given value is not valid.

The ValidationException is localizable, supporting invalid value message localization and can act as a container for multiple validation exceptions.

Example validators
Validator<String> validator = v -> { (1)
  if (v.length() < 10)
    throw new ValidationException("Value must be at least 10 characters");
};

validator = Validator.create(v -> v.length() >= 10, "Value must be at least 10 characters"); (2)

validator = Validator.create(v -> v.length() >= 10, "Value must be at least 10 characters",
    "messageLocalizationCode"); (3)
1 Create a Validator for String value types which checks if the value is at least 10 characters long
2 Create the same Validator created using the Validator.create() builder method
3 Create the same Validator created using the Validator.create() builder method and providing an invalid value message localization code

3.4.2. Bultin validators

The platform provides a set of validators for the most common use cases. Each of the builtin validators supports a localizable invalid value message or provides a default invalid value message if a custom one is not specified.

The available bultin validators can be obtained using the corresponding static builder method of the Validator interface:

  • isNull: checks that the value is null

  • notNull: checks that the value is not null

  • notEmpty: checks that the value is neither null nor empty

  • notBlank: checks that the value is neither null nor empty, trimming spaces

  • max: checks that the value is lower than or equal to a max value (for Strings, arrays and collections the size/length is checked against given max value)

  • min: checks that the value is greater than or equal to a min value (for Strings, arrays and collections the size/length is checked against given min value)

  • pattern: checks that the value matches a regular expression

  • in: checks that the value is one of the values of a specified set

  • notIn: checks that the value is not one of the values of a specified set

  • notNegative: checks that a numeric value is not negative

  • digits: checks that a numeric value is within an accepted range of integral/fractional digits

  • past: checks that a date type value is in the past

  • future: checks that a date type value is in the future

  • lessThan: checks that a value is less than another value

  • lessOrEqual: checks that a value is less than or equal to another value

  • greaterThan: checks that a value is greater than another value

  • greaterOrEqual: checks that a value is greater than or equal to another value

  • email: checks that the value is a valid e-mail address using RFC822 format rules

Example bultin validators
try {
  Validator.notEmpty().validate("mustBeNotEmpty"); (1)
  Validator.notEmpty("Value must be not empty", "myLocalizationMessageCode").validate("mustBeNotEmpty"); (2)
} catch (ValidationException e) {
  // invalid value
  System.out.println(e.getLocalizedMessage());
}
1 Uses the builtin notEmpty validator to validate a value, using the default invalid value message
2 Uses the builtin notEmpty validator to validate a value, using a custom invalid value message and localization message code

3.4.3. Validatable and ValidatorSupport

The ValidatorSupport interface is implemented by classes which supports adding and removing validators.

The Validatable interface declares the support for value validation, using the Validator interface, for a class. The validate(T value) checks the validity of the given value against every registered validator, and throws a ValidationException with a single or multiple validation error message if a given value is not valid.

See the Properties section as an example of Validatable usage.

3.5. StringValuePresenter

A StringValuePresenter deals with String representation of a generic Object, according to object type and using the current context for example for messages localization.

Presentation parameters can be used to tune the presentation strategy.

3.5.1. Default StringValuePresenter

The default StringValuePresenter can be obtained using the getDefault() static method.

The default presentation strategy is organized according to the type of the value to present, with the following rules:

  • CharSequence: the value toString() representation is used

  • Boolean: boolean values are represented using the default boolean localization rules of the current LocalizationContext, if available. Otherwise, String.valueOf(value) is used.

    See Internationalization for further information on internationalization and LocalizationContext

  • Localizable: the value is localized using current LocalizationContext, if available.

    See Internationalization for further information on internationalization and LocalizationContext

  • Enum: The enum value name is used by default. If the enumeration is Localizable, the value is localized using current LocalizationContext, if available. The @Caption annotation annotation is supported on enumeration values for localization.

  • Temporal and Date: The current LocalizationContext is used to format the value, if available. Otherwise, a default short date format is used with the default Locale.

  • Number: The current LocalizationContext is used to format the value, if available. Otherwise, the default number format for the default Locale is used.

  • Any other type: the value toString() representation is used

  • Arrays and collections: Each element of the array/collection is presented using the rules described above, then the values are joined together in a single String using a separator character.

The default separator character for array/collection presentation is ;. Use holon.value-presenter.values-separator parameter to change the separator character to use.

Presentation parameters:

The default StringValuePresenter supports the following parameters to setup and tune the presentation:

Table 1. Default presentation parameters
Name Constant Type Meaning

holon.value-presenter. values-separator

MULTIPLE_VALUES_SEPARATOR

String

The separator character to use for arrays/collections presentation

holon.value-presenter. max-length

MAX_LENGTH

Integer number

Limit the max length of the presented String

holon.value-presenter. decimal-positions

DECIMAL_POSITIONS

Integer number

Specify the decimal positions to use to present numeric type values

holon.value-presenter. disable-grouping

DISABLE_GROUPING

Boolean (true/false)

Disable the use of grouping symbol for numeric type values

holon.value-presenter. hide-zero-decimals

HIDE_DECIMALS_WHEN_ALL_ZERO

Boolean (true/false)

Hide number decimals when all decimal positions (if any) are equal to zero

holon.value-presenter. percent-style

PERCENT_STYLE

Boolean (true/false)

Use a percent-style format for numeric decimal values

holon.value-presenter. temporal-type

TEMPORAL_TYPE

TemporalType enumeration

Set the temporal time format (Date, time or date and time) to use to present Date and Calendar values

For Property value presentation, the presentation parameters are read from the property configuration attributes.
Value presentation examples
enum MyEnum {

  @Caption("The value 1")
  VALUE1,

  @Caption(value = "The value 2", messageCode = "message.value2")
  VALUE2

}

public void present() {
  String presented = StringValuePresenter.getDefault().present("stringValue"); (1)
  presented = StringValuePresenter.getDefault().present("stringValue",
      ParameterSet.builder().parameter(StringValuePresenter.MAX_LENGTH, 6).build()); (2)
  presented = StringValuePresenter.getDefault().present(MyEnum.VALUE1); (3)
  presented = StringValuePresenter.getDefault().present(new MyEnum[] { MyEnum.VALUE1, MyEnum.VALUE2 }); (4)
}
1 Return stringValue
2 Return string
3 Return The value 1, using the @Caption annotation
4 Return The value 1;The value 2

3.6. Internationalization

The internationalization architecture of the Holon platform relies upon the LocalizationContext interface, which is the main entry point for localization of messages, numbers and date/time elements.

3.6.1. Localizable messages

A localizable message is represented using the following attributes:

  • A default message: This is the default message to use if a localization attribute or provider is not available

  • A localization message code: The symbolic message code to use as identifier to provide message translations

  • Optional message arguments: A set of arguments to be used to replace a conventional placeholder in the message String with actual values

The Localizable interface is available to represent a localizable message, providing a convenience fluent builder to create Localizable instances.

Building a Localizable
Localizable localizable = Localizable.builder().message("defaultMessage").messageCode("message.code").build(); (1)

localizable = Localizable.builder().message("message &").messageCode("message.code").messageArguments("test") (2)
    .build();
1 Build a Localizable with a defaultMessage and a message localization code
2 Build a Localizable using a localization argument too

3.6.2. @Caption annotation

The Caption annotation can be used to provide the localizable message to use as the caption (i.e. the short description or explanatory label of an element) of an element.

The relevant annotation attributes are:

  • value: The default message to use as caption

  • messageCode: The symbolic message code to use as identifier to provide message translations

The support for the @Caption annotation must be declared and documented by the classes/elements which support it.

For example, the default StringValuePresenter, used also as default Properties value presenter supports the @Caption annotation for enum values presentation.

3.6.3. Message providers

To perform actual messages localization the MessageProvider functional interface is used. A MessageProvider provides a message translation for a specified message localization identifier and a Locale representing the language/country for which the translation is required.

The platform makes available a default MessageProvider using properties files as message localization containers, which can be created using the static fromProperties(String…​ basenames) method.

Properties files are resolved using a configured basenames as prefix, and Locale language, country and variant separated by an underscore as file name. Files must have the .properties extension.

The basenames follow the java.util.ResourceBundle conventions: essentially, a fully-qualified classpath location. If it doesn’t contain a package qualifier, it will be resolved from the classpath root. Note that the JDK’s standard ResourceBundle treats dots as package separators: this means that test.messages is equivalent to test/messages.

As an example, suppose to have a messages folder under the classpath root containing the following files:

  • messages_en_US_var.properties: This file will be used for a Locale with en as language, US as country and var as variant

  • messages_en_US.properties: This file will be used for a Locale with en as language, US as country and no variant

  • messages_en.properties: This file will be used for a Locale with en as language and a country different from US

  • messages_it.properties: This file will be used for a Locale with it as language, ignoring country or variant

  • messages.properties: This is the default file to use as fallback if no other match is found for a Locale

A message localization properties file simply contains a list of the localization (translation) of the available message localization identifiers (or message codes), e.g. test.msg=translation.

3.6.4. LocalizationContext

The LocalizationContext interface is the main entry point for localization of messages, numbers and date/time elements.

3.6.5. Building a LocalizationContext

The simplest way to build a LocalizationContext is to use the fluent builder:

LocalizationContext localizationContext = LocalizationContext.builder()
    .messageProvider(MessageProvider.fromProperties("messages").build()) (1)
    .messageProvider(MessageProvider.fromProperties("messages2").build()) (2)
    .messageArgumentsPlaceholder("$") (3)
    .withDefaultDateTemporalFormat(TemporalFormat.MEDIUM) (4)
    .withDefaultTimeTemporalFormat(TemporalFormat.FULL) (5)
    .withDefaultBooleanLocalization(Boolean.TRUE, Localizable.builder().messageCode("boolean.true").build()) (6)
    .withDefaultBooleanLocalization(Boolean.FALSE,
        Localizable.builder().messageCode("boolean.false").build()) (7)
    .withInitialSystemLocale() (8)
    .withInitialLocale(Locale.US) (9)
    .build();
1 Add a MessageProvider using properties files located under the messages folder (see Message providers)
2 Add a MessageProvider using properties files located under the messages2 folder (see Message providers)
3 Use the $ character as message localization arguments placeholder (replacing the default & character)
4 Use the medium format as default date format style
5 Use the full format as default time format style
6 Use the boolean.true message code to localize the true boolean values
7 Use the boolean.false message code to localize the false boolean values
8 Initially Localize the LocalizationContext using the default system Locale
9 Initially Localize the LocalizationContext using the US Locale

3.6.6. Obtaining a LocalizationContext

If the LocalizationContext is registered as a Context resource using the default context resource key, i.e. the fully qualified LocalizationContext class name, the current LocalizationContext can be obtained by using the convenience getCurrent() method.

3.6.7. Localizing a LocalizationContext

Before using a LocalizationContext, you must ensure that it is localized, i.e. bound to a specific Locale. This will be the Locale used for the localization of messages, numbers and date/time elements. To localize a LocalizationContext, use the localize(…​) method of the LocalizationContext interface, providing a Locale instance.

To have more control on context localization, a Localization object can be used, which allows to setup:

  • A parent Localization, i.e. the Localization to use as fallback when a localization operation cannot be successfully performed using current localization, for example because a message translation is not available. This allows the creation of a hierarchy of `Localization`s;

  • The default decimal positions to use to format a localized numeric decimal value, if decimal positions are not explicitly given;

  • The default date format style

  • The default time format style

LocalizationContext localization
LocalizationContext localizationContext = LocalizationContext.getCurrent()
    .orElseThrow(() -> new IllegalStateException("Missing LocalizationContext")); (1)

localizationContext.localize(Locale.US); (2)
boolean localized = localizationContext.isLocalized(); (3)

localizationContext.localize(Localization.builder(Locale.JAPAN).defaultDecimalPositions(2)
    .defaultDateTemporalFormat(TemporalFormat.FULL).build()); (4)
1 Require a LocalizationContext to be available as context resource
2 Localize the LocalizationContext using the US Locale
3 Check the LocalizationContext is localized
4 Localize the LocalizationContext using a Localization

3.6.8. Using the LocalizationContext

The LocalizationContext provides several methods to perform localizations of messages, dates and numbers.

  • For numbers formatting, the NumberFormatFeature enumeration can be used to tune the format style

  • For date and times formatting, the TemporalFormat enumeration can be used to specify the format style

LocalizationContext ctx = LocalizationContext.builder()
    .messageProvider(MessageProvider.fromProperties("messages").build()).withInitialLocale(Locale.US)
    .build();

ctx.getLocale().ifPresent(l -> System.out.println(l)); (1)

String localizedMessage = ctx.getMessage("test.message", "defaultMessage"); (2)
localizedMessage = ctx
    .getMessage(Localizable.builder().message("defaultMessage").messageCode("test.message").build()); (3)

ctx.format(2.56); (4)
ctx.format(0.5, NumberFormatFeature.PERCENT_STYLE); (5)
ctx.format(5600.678, 2); (6)

NumberFormat nf = ctx.getNumberFormat(Integer.class); (7)

ctx.format(new Date(), TemporalType.DATE); (8)
ctx.format(new Date(), TemporalType.DATE_TIME, TemporalFormat.LONG, TemporalFormat.LONG); (9)

ctx.format(LocalDate.of(2017, Month.MARCH, 15)); (10)
ctx.format(LocalDateTime.of(2017, Month.MARCH, 15, 16, 48), TemporalFormat.FULL, TemporalFormat.SHORT); (11)

DateFormat df = ctx.getDateFormat(TemporalType.DATE); (12)
DateTimeFormatter dtf = ctx.getDateTimeFormatter(TemporalType.DATE_TIME); (13)
1 Print the current LocalizationContext Locale
2 Localize a message providing the message localization code and the default message to use if no translation is available for the current LocalizationContext Locale
3 Localize a message using a Localizable
4 Format a number using default styles and localization settings
5 Format a number using the percent style
6 Format the given number using 2 decimal places
7 Get the LocalizationContext NumberFormat for Integer numbers localization
8 Format a Date considering the date value of DATE type (without time)
9 Format a Date considering the date value of DATE_TIME type (including time) and using the LONG style for both date and time parts
10 Format a LocalDate with default style
11 Format a LocalDateTime using FULL style for the date part and SHORT style for the time part
12 Get the DateFormat to use to format `Date`s without time
13 Get the DateTimeFormatter to use to format temporals with date and time

3.7. Properties

The properties architecture is a central concept in the Holon platform. A property represent a data attribute in a general and abstract way, allowing to:

  • Collect all relevant features and configurations of the data attribute in a single point, to avoid duplications and inconsistency between application layers;

  • Abstract the property definition from the concrete data representation and persistence model, to favor loose coupling and independence from underlying data structures;

  • Use a common structure for data attributes definition which can be shared by different distributed application layers;

  • Provide common operations and functionalities, such as value converters and validators;

  • Provide bultin naming and localization features;

  • Use the property as an abstract data model reference to build queries and to transport data model values.

A property is represented by the Property interface. Provides a type and it is generalized on such type, which represents the value type handled by the property.

3.7.1. Configuration

A PropertyConfiguration class is bound to each property, representing a common place to handle property configuration attributes using a general purpose structure which extends ParameterSet to provide operations to inspect and use configuration parameter values. This structure can be used also for application-specific properties configuration and platform extensions parameters collector.

The property configuration is accessible from the Property.getConfiguration() method. To set the property configuration attributes, the property builder configuration(String parameterName, Object value) methods can be used.

The PropertyConfiguration interface provides a special TemporalType attribute which can be used to specify the nature (date, time or date and time) of generic java temporal types, such as Date and Calendar. This attribute can be used from a number of platform services and structures to perform consistent operations on property value, such as presentation, rendering or persistence data manipulation.

The Holon platform fully supports the new Java 8 Date and Time API, which represents a big step forward compared to the previous date and time support classes, to address the shortcomings of the older java.util.Date and java.util.Calendar types. It is strongly recommended to use the new java.time.* classes to manage date and times, such as LocalDate, LocalTime, LocalDateTime and so on. This way, in addition to achieving a more robust and consistent code, there no need to use the TemporalType property configuration attribute to ensure consistency in property value manipulation and presentation.
Date type property configuration example
PathProperty<Date> dateProperty = PathProperty.create("oldDateTypeProperty", Date.class)
    .temporalType(TemporalType.DATE_TIME) (1)
    .configuration("myAttribute", "myValue"); (2)

PropertyConfiguration cfg = dateProperty.getConfiguration(); (3)
Optional<String> value = cfg.getParameter("myAttribute", String.class); (4)
1 Set the property TemporalType
2 Set a custom configuration attribute
3 Get the property configuration
4 Get the myAttribute configuration attribute

3.7.2. Converters

Each Property supports a PropertyValueConverter, which can be used to perform property value conversions when obtaining the property value from the concrete data model and back. The two conversion methods (from the property value type to the data model value type and vice-versa) should be symmetric, so that chaining these together returns the original result for all inputs.

Example converter from String to Integer and back
PropertyValueConverter<Integer, String> converter = new PropertyValueConverter<Integer, String>() {

  @Override
  public Integer fromModel(String value, Property<Integer> property) throws PropertyConversionException {
    return (value != null) ? Integer.parseInt(value) : null; (1)
  }

  @Override
  public String toModel(Integer value, Property<Integer> property) throws PropertyConversionException {
    return (value != null) ? String.valueOf(value) : null; (2)
  }

  @Override
  public Class<Integer> getPropertyType() {
    return Integer.class;
  }

  @Override
  public Class<String> getModelType() {
    return String.class;
  }

};
1 Convert a String model value into the Integer property value type
2 Convert the Integer property value type into the String model value type

Some useful builtin converters are provided:

  • Numeric boolean converter: perform conversions from/to a numeric data model type to a boolean property type using the following convention: null or 0 numeric values will be converted as false boolean values, any other value will be converted as the true boolean value;

  • Enum by ordinal converter: perform conversions from/to a Integer data model type to an Enum property type using the the enumeration ordinal values;

  • Enum by name converter: perform conversions from/to a String data model type to an Enum property type using the the enumeration name values;

  • LocalDate value converter: perform conversions from/to Date type data model values and Java 8 LocalDate temporal type property types;

  • LocalDateTime value converter: perform conversions from/to Date type data model values and Java 8 LocalDateTime temporal type property types.

Builtin converters
PropertyValueConverter.numericBoolean(Integer.class); (1)
PropertyValueConverter.localDate(); (2)
PropertyValueConverter.localDateTime(); (3)
PropertyValueConverter.enumByOrdinal(); (4)
PropertyValueConverter.enumByName(); (5)
1 Numeric boolean converter using a Integer type data model value
2 LocalDate converter
3 LocalDateTime converter
4 Enum by ordinal converter
5 Enum by name converter

3.7.3. Localization (caption)

The Property interface extends Localizable to allow property caption localization. The property caption is a kind of property description, which can be used also in UI application layers to provide the property description to the user.

See Internationalization for additional details about the Holon platform internationalization architecture.

3.7.4. Validation

The Property interface extends Validatable and supports property value validation using Validators. See the Data validation section for detailed information about validators definition and usage.

3.8. Path

Properties which refer to a concrete data model attribute extend the Path interface, which represents a named path to a data model attribute and supports hierarchical structures. Each path is identified by a String name, representing the connection with the concrete data model.

A path is typed and the path type represents the type of the value identified by such path in the data model.

Path examples
Path<String> stringPath = Path.of("pathName", String.class); (1)

String name = stringPath.getName(); (2)
boolean root = stringPath.isRootPath(); (3)

Path<String> hierarchicalPath = Path.of("subName", String.class).parent(stringPath); (4)
String fullName = hierarchicalPath.fullName(); (5)
1 Create a String type path with the name pathName
2 The path name is pathName
3 The path is a root path because it has no parent
4 Create a path named subName and set pathName as parent path
5 The path full name will be pathName.subName

3.9. PathProperty

The central interface to define a Property bound to a data model attribute is the PathProperty interface, which extends Path to provide a binding to the symbolic path of the data model attribute.

The create(…​) method can be used for the definition of a PathProperty, providing property configuration attributes, converters, validators and property localization messages through a fluent builder.

Usually a PathProperty is defined as a static constant, immutable during the application runtime.

PathProperty definition
public final static PathProperty<Long> ID = PathProperty.create("id", Long.class) (1)
    .configuration("test", 1) (2)
    .validator(Validator.notNull()) (3)
    .message("Identifier") (4)
    .messageCode("property.id"); (5)

public final static PathProperty<Boolean> VALID = PathProperty.create("valid", Boolean.class) (6)
    .converter(PropertyValueConverter.numericBoolean(Integer.class)); (7)
1 Create a PathProperty named id (this is the path name) bound to a Long value type
2 Add a configuration parameter named test with value 1
3 Add a validator to check that the property value is not null
4 Set the property caption message
5 Set the property caption localization message code
6 Create a PathProperty named valid (this is the path name) bound to a Boolean value type but which refers to a Integer data model attribute
7 Set the converter to perform conversion between Integer data model values and Boolean property values and vice-versa

A PathProperty can be used in Query clauses and projections, and the PathProperty interface provides several method to easily create query clauses and aggregations.

3.10. VirtualProperty

A VirtualProperty is a Property which is not directly bound to a data model attribute, but which instead provides its value through a PropertyValueProvider.

The PropertyValueProvider is a functional interface which implementation must be declared when the virtual property is created and is used to provide a virtual or calculated property value. The PropertyValueProvider takes a PropertyBox as argument, allowing the use of other properties to compose and provide the virtual property value. For this reason, the most suitable use of a VirtualProperty is within a PropertyBox context.

See PropertyBox for further information.

VirtualProperty definition
public final static VirtualProperty<Integer> ALWAYS_ONE = VirtualProperty.create(Integer.class, propertyBox -> 1); (1)

public final static PathProperty<String> NAME = PathProperty.create("name", String.class); (2)
public final static PathProperty<String> SURNAME = PathProperty.create("surname", String.class); (3)
public final static VirtualProperty<String> FULL_NAME = VirtualProperty.create(String.class,
    propertyBox -> propertyBox.getValue(NAME) + " " + propertyBox.getValue(SURNAME)); (4)
1 Create a VirtualProperty of Integer type which always returns the value 1
2 PathProperty definition representing a person name attribute
3 PathProperty definition representing a person surname attribute
4 Create a VirtualProperty of String type providing the person full name, concatenating the person name and surname values read from the current PropertyBox

3.11. PropertySet

The PropertySet interface represents an immutable set of Property and provides methods for the definition and inspection of the set of properties. It extends the Java standard Iterable interface, representing an iterable set of properties.

The most common use case of a property set is to aggregate the properties which refer to a data model entity, even combining path properties with virtual properties.

Property sets can be used as Query projections: the query selection will be the set of the properties defined in the PropertySet and the query result will be a PropertyBox with the same properties of the projected property set and the query results bound to each property.

PropertySet examples
final PathProperty<String> NAME = PathProperty.create("name", String.class);
final PathProperty<String> SURNAME = PathProperty.create("surname", String.class);
final PathProperty<Long> ID = PathProperty.create("id", Long.class);

PropertySet<Property> set = PropertySet.of(NAME, SURNAME); (1)
set = PropertySet.builder().add(NAME).add(SURNAME).build(); (2)

boolean contains = set.contains(NAME); (3)
set.forEach(p -> p.toString()); (4)
String captions = set.stream().map(p -> p.getMessage()).collect(Collectors.joining()); (5)

PropertySet<Property> newSet = PropertySet.builder().add(set).add(ID).build(); (6)
int size = newSet.size(); (7)

newSet = PropertySet.builder().add(set).remove(SURNAME).build(); (8)
1 Create a PropertySet containing NAME and SURNAME properties
2 Create a PropertySet containing NAME and SURNAME properties using the fluent builder
3 Check if PropertySet contains the property NAME
4 Use the forEach operation to invoke the toString method for each property of the set
5 Use a stream of the properties of the set to join the property captions in a String
6 Create a new PropertySet as the conjunction of the properties of the first set and the ID property
7 Obtain the set size, i.e. the number of properties in the set
8 Create a new PropertySet from the first one but excluding the SURNAME property

3.12. PropertyBox

A PropertyBox represents a container of Property values and it is bound to a specific (and immutable) PropertySet.

PropertyBox provides methods set and retrieve the values for the properties of set to which it is bound, handling property value validation and conversions according to Property configuration and ensuring a consistent behaviour when using a VirtualProperty.

The PropertyBox abstraction is the base structure to transport and provide property values, used by the Holon platform every time a set of property values comes into play, for example in Query results projections involving more than one property.

Using a PropertyBox allows to preserve a strong independence from the underlying (and possibly mutable) concrete data model, representing a data model entity (when it can be considered a set of data attributes, i.e. properties, and their values) in a generalized fashion.

PropertyBox examples
final PathProperty<Long> ID = PathProperty.create("id", Long.class).validator(Validator.notNull());
final PathProperty<String> NAME = PathProperty.create("name", String.class).validator(Validator.notBlank());

final PropertySet<?> PROPERTIES = PropertySet.of(ID, NAME);

PropertyBox propertyBox = PropertyBox.create(ID, NAME); (1)
propertyBox = PropertyBox.create(PROPERTIES); (2)

propertyBox.setValue(ID, 1L); (3)
propertyBox.setValue(NAME, "testName"); (4)

propertyBox = PropertyBox.builder(PROPERTIES).invalidAllowed(false).set(ID, 1L).set(NAME, "testName").build(); (5)

propertyBox.validate(); (6)

Long id = propertyBox.getValue(ID); (7)
String name = propertyBox.getValueIfPresent(NAME).orElse("default"); (8)

boolean containsNotNullId = propertyBox.containsValue(ID); (9)

PropertyBox ids = propertyBox.cloneBox(ID); (10)
1 Create an empty PropertyBox using ID and NAME properties as property set
2 Create an empty PropertyBox with the same property set but using PROPERTIES property set
3 Set the value for the ID property: the setValue method is generalized on property type, so only consistent value types are accepted (a Long in this case)
4 Set the value for the NAME property: the setValue method is generalized on property type, so only consistent value types are accepted (a String in this case)
5 Create a PropertyBox using the fluent builder using the PROPERTIES property set and setting the property values
6 Validate the value of the properties currently present in the box, invoking configured property Validators
7 Get the value for the ID property: the getValue method is generalized on property type, so a consistent value type is returned (Long in this case)
8 Get the Optional NAME property value, using the default String if a value for that property is not present in PropertyBox
9 Check if a value for the ID property is present in PropertyBox
10 Clone the PropertyBox, creating a new PropertyBox with a property set composed only by the ID property and copying the values of the properties which was present in the source PropertyBox and the destination property set too

3.13. Property value presentation

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

The PropertyValuePresenter is functional interface aimed to provide a String representation of the value associated to a Property. The property value presenters are organized in a registry (the PropertyValuePresenterRegistry), which collects all available presenters and provides the most suitable presenter for a given property, using the conditions, expressed as Predicate, the presenters were registered with.

A default property value presenter is provided by the platform and automatically registered in the registry.

The default property value presenter uses a default implementation of StringValuePresenter to convert property values into String. See StringValuePresenter for further information on the presentation strategy.
The default property value presenter uses the PropertyConfiguration attributes as property presentation parameters. So you can set default StringValuePresenter presentation parameters as property configuration parameters to use them for property presentation.

This way, the registry uses a fallback strategy to obtain the most suitable presenter for a property, searching for the presenter associated to the condition which best matches a given property, or falling back to another presenter consistent with the property, if available. If a specific presenter is not found, the default presenter is used.

The registry supports a priority indication for a PropertyValuePresenter, which can be expressed by using standard javax.annotation.Priority annotation on presenter implementation class, where lower values corresponds to higher priority. Priority can be used when a set of conditions are not clearly one more restrictive than others in the same registry, so an explicit lookup order has to be defined.

The Property interface provides a convenience present(T value) method to present given property value using the current PropertyValuePresenterRegistry, i.e. the registry available as a Context resource, if available, or the default registry associated to the current ClassLoader otherwise.

Property value presentation
final PathProperty<Long> ID = PathProperty.create("id", Long.class);

String stringValue = ID.present(1L); (1)

stringValue = PropertyValuePresenterRegistry.get().getPresenter(ID)
    .orElseThrow(() -> new IllegalStateException("No presenter available for given property"))
    .present(ID, 1L); (2)
1 Present the 1 value for the ID property using the current Context presenters registry or the default one
2 The same operation made using the PropertyValuePresenterRegistry directly

3.13.1. PropertyValuePresenter registration

The registration of a new PropertyValuePresenter can be made in two ways:

1. Using the PropertyValuePresenterRegistry:

PropertyValuePresenter registration example
PropertyValuePresenter<LocalTime> myPresenter = (p, v) -> v.getHour() + "." + v.getMinute(); (1)

PropertyValuePresenterRegistry.get().register(p -> LocalTime.class.isAssignableFrom(p.getType()), myPresenter); (2)
1 Create a presenter for LocalTime type properties which represents the value as hours.minutes
2 Register the presenter binding it to the Predicate wich corresponds to the condition “the property type is LocalTime”

2. Using the standard Java service extensions:

Create a file named com.holonplatform.core.property.PropertyValuePresenter containing the fully qualified class name(s) of the PropertyValuePresenter implementation and put it under the META-INF/services folder of your project to register the presenter in the default PropertyValuePresenterRegistry.

When a PropertyValuePresenter is registered using service extensions an always true condition is used, i.e. the presenter is available for any property. Use this method only for general purpose presenters. The javax.annotation.Priority annotation can be used on presenter’s implementation class to assign a priority order to the presenter.

3.14. Property rendering

A further property handling concept is made available by the Holon platform: the property renderers.

PropertyRenderer is responsible to render a Property as a specific rendering class type, declared by the getRenderType() method.

The property renderers are organized in a registry (the PropertyRendererRegistry), which collects all available renderers and provides the most suitable renderer for a given property and a specific rendering type, using the conditions, expressed as `Predicate`s, the renderers were registered with.

This paradigm can be used to provide property representation or management objects in a standard way, organizing the renderers by rendering type and gathering them together in a common registry, to made them available to application layers.

No default PropertyRenderer is provided by the core platform module, because the renderers are very related to the specific application logic or UI technology.

The registry supports a priority indication for a PropertyRenderer, which can be expressed using standard javax.annotation.Priority annotation on renderer implementation class, where lower values corresponds to higher priority. Priority can be used when a set of conditions is not clearly more restrictive than another, so an explicit lookup order has to be defined.

The Property interface provides two convenience methods to render the property value using the current PropertyRendererRegistry, i.e. the registry available as a Context resource, if available, or the default registry associated to the current ClassLoader otherwise. These methods are:

  • render(Class renderType): Renders the property as given renderType object type. Throws a NoSuitableRendererAvailableException if no PropertyRenderer is available for this property and given rendering type

  • renderIfAvailable(Class renderType): Renders the property as given renderType object type if a suitable PropertyRenderer for the required renderType is available from the PropertyRendererRegistry obtained from current Context or from the default one for the current ClassLoader. If a suitable renderer is not available, an empty Optional is returned.

Property renderers
class MyRenderingType { (1)

  private final Class<?> propertyType;

  public MyRenderingType(Class<?> propertyType) {
    this.propertyType = propertyType;
  }

}

public void render() {
  PropertyRenderer<MyRenderingType, Object> myRenderer = PropertyRenderer.create(MyRenderingType.class,
      p -> new MyRenderingType(p.getType())); (2)

  PropertyRendererRegistry.get().register(p -> true, myRenderer); (3)

  final PathProperty<Long> ID = PathProperty.create("id", Long.class);

  MyRenderingType rendered = ID.render(MyRenderingType.class); (4)
}
1 Define a custom rendering class (in a UI layer, this could be for example a field to manage property value)
2 Create a renderer for MyRenderingType, available for any property type
3 Register the renderer binding it to an always true condition, so it will be available for any property
4 Render the ID property as MyRenderingType type

3.14.1. PropertyRenderer registration

The registration of a new PropertyRenderer can be made in two ways:

1. Using the PropertyRendererRegistry:

Property renderers can be registered to a PropertyRendererRegistry using the register method and providing a condition to bind the renderer only to a specific kind/set of properties.

2. Using the standard Java service extensions:

Create a file named com.holonplatform.core.property.PropertyRenderer containing the fully qualified class name(s) of the PropertyRenderer implementation and put it under the META-INF/services folder of your project to register the renderer in the default PropertyRendererRegistry.

When a PropertyRenderer is registered using service extensions an always true condition is used, i.e. the renderer is available for any property. The javax.annotation.Priority annotation can be used on the renderer’s implementation class to assign a priority order to the renderer.

3.15. Java Beans and the Property architecture

The Holon platform offers a wide support to treat standard Java Beans properties as Properties abstraction.

The properties of a Java Bean class can be represented as `PathProperty`s, where the property path name corresponds to the bean property name, and are collected in a bean-related property set represented by the BeanPropertySet interface.

A BeanIntrospector can be used to inspect the properties of a Java Bean class and obtain the corresponding BeanPropertySet.

3.15.1. BeanIntrospector

The BeanIntrospector interface provides methods to inspect a Java Bean class and obtain informations about bean properties, representing them as a PathProperty where the path name of each property corresponds to the bean property name.

Nested bean classes are supported, keeping the property hierarchy intact, i.e. the parent property of a PathProperty obtained from the bean property of a nested class will be the bean property to which the nested class refers to. To access a nested property by path name, the conventional dot notation is supported. For example parentProperty.nestedProperty.

The detected bean properties are collected and returned as a BeanPropertySet.

BeanIntrospector introspector = BeanIntrospector.get(); (1)
BeanPropertySet<MyBean> properties = introspector.getPropertySet(MyBean.class); (2)
1 Get the current BeanIntrospector, i.e. the instance registered as a Context resource, or the default instance if not available in context
2 Introspect given bean class and obtain a BeanPropertySet which contains all detected bean properties
BeanPropertyPostProcessor

A BeanPropertyPostProcessor can be used to integrate the bean instrospection strategy and process the properties which will become part of resulting BeanPropertySet, for example to modify or add property configuration attributes, validators, converters and so on.

The BeanPropertyPostProcessor functional interface method processBeanProperty is called for every detected and valid bean property, provided as builder to participate in the property building process.

The registration of a BeanPropertyPostProcessor can be performed in two ways:

1. Registration using BeanIntrospector: The addBeanPropertyPostProcessor method can be used to register a BeanPropertyPostProcessor.

2. Registration using the standard Java service extensions: BeanPropertyPostProcessor registration can be performed also using default Java extension services, providing a com.holonplatform.core.beans.BeanPropertyPostProcessor file under the META-INF/services folder containing the fully qualified BeanPropertyPostProcessor concrete class names to register.

BeanPropertyPostProcessor registration example
BeanIntrospector.get().addBeanPropertyPostProcessor((property, cls) -> property.configuration("test", "testValue")); (1)
1 Register a BeanPropertyPostProcessor which adds a test property configuration attribute to all processed properties
The javax.annotation.Priority annotation can be used on a BeanPropertyPostProcessor implementation class to assign a priority order within the registered processors list, where lower values corresponds to higher priority.
Builtin bean property processing annotations

The Holon platform provides a number of annotations, processed by the default set of BeanPropertyPostProcessor, automatically registered in the BeanIntrospector, to tune the property definitions and add property configuration elements such as validators and localizable captions.

The following annotations can be placed on bean property class attributes to be processed during the bean class introspection:

  • @Ignore: Can be used to skip a bean class attribute during the introspection process, which will not be part of the resulting BeanPropertySet

  • @Caption: The default platform internationalization caption annotation is supported to set the property localizable caption

  • @Sequence: Set the property sequence order within the BeanPropertySet

  • @Config: a repeteable annotation to specify a configuration key and its value to be setted in the property configuration. Only String type configuration values are supported by this annotation, use your own BeanPropertyPostProcessor to perform more advanced property configuration setup operations

  • @Converter: Setup a Converters for the property, providing either a builtin converter or the PropertyValueConverter class to use.

  • Validators: One or more Validator can be added to a bean property using one of the following methods:

    • Using the standard javax.validation.constraints bean validation API annotations. The supported annotations are: @Null, @NotNull, @Size, @Min, @Max, @DecimalMin, @DecimalMax, @Digits, @Future, @Past, @Pattern;

    • Using the additional platform validation annotations: @NotEmpty (CharSequence not null and not empty), @NotBlank (CharSequence not null and not empty trimming spaces), @NotNegative (Number not negative) and @Email (String is a valid e-mail address);

    • Using the repeteable @Validator annotation, specifying the custom validator class to use.

For bean validation API and builtin validation annotations, the message attribute is used to obtain the invalid value message to associate to the validator and, by convention, if the message is included between braces is considered as a localization message code, otherwise as a simple, not localizable, message. The @ValidationMessage annotation can be used instead to provide a different, localizable, invalid value message. If such annotation is present, the message attribute is ignored.

3.15.2. BeanPropertySet

The BeanPropertySet interface represents a set of `PathProperty`s which corresponds to the properties of a Java Bean class, where the path name of each property of the set corresponds to the bean property name.

For nested bean classes, the parent property of the nested PathProperty will be the bean property the nested class refers to, and the full path of the nested property will be the property path hierachy separated by a dot character, for example parentProperty.nestedProperty.

It extends the default PropertySet structure and provides additional functionalities to:

  • Obtain a property by name

  • Read and write single property values to and from an instance of the Java Bean class bound to the set

  • Read the property values from an instance of the Java Bean class bound to the set and obtain such values as a PropertyBox

  • Write the property values contained in a PropertyBox to an instance of the Java Bean class bound to the set

BeanPropertySet examples
class MyNestedBean {

  private String nestedName;

  public String getNestedName() {
    return nestedName;
  }

  public void setNestedName(String nestedName) {
    this.nestedName = nestedName;
  }

}

class MyBean {

  private Long id;
  private boolean valid;
  private MyNestedBean nested;

  public Long getId() {
    return id;
  }

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

  public boolean isValid() {
    return valid;
  }

  public void setValid(boolean valid) {
    this.valid = valid;
  }

  public MyNestedBean getNested() {
    return nested;
  }

  public void setNested(MyNestedBean nested) {
    this.nested = nested;
  }

}

public static final BeanPropertySet<MyBean> PROPERTIES = BeanIntrospector.get().getPropertySet(MyBean.class); (1)

public void propertySet() {
  Optional<PathProperty<Long>> idProperty = PROPERTIES.<Long>getProperty("id"); (2)
  PathProperty<Long> id = PROPERTIES.requireProperty("id", Long.class); (3)

  PathProperty<String> nestedName = PROPERTIES.requireProperty("nested.nestedName"); (4)

  // read
  MyBean instance = new MyBean();
  instance.setId(1L);

  Long value = PROPERTIES.read("id", instance); (5)
  PropertyBox box = PROPERTIES.read(instance); (6)
  value = box.getValue(PROPERTIES.requireProperty("id")); (7)

  // write
  instance = new MyBean();
  PROPERTIES.write("nested.nestedName", "test", instance); (8)

  MyBean written = PROPERTIES
      .write(PropertyBox.builder(PROPERTIES).set(PROPERTIES.requireProperty("id"), 1L).build(), new MyBean()); (9)
}
1 Get the BeanPropertySet of the MyBean class
2 Get the PathProperty which corresponds to the id bean property name, obtaining an Optional which is empty if the property name is not found within the bean property set
3 Get the required PathProperty which corresponds to the id bean property name: if not found, an exception is thrown
4 Get the nested property which corresponds to the full path nested.nestedName
5 Read the value of the property with the id path name from given bean instance (1)
6 Read all the values of the bean property set from given bean instance, obtaining a PropertyBox which contains the read values
7 Read the value of the id property from the PropertyBox obtained in previous read operation (1)
8 Write the test value to the property with path nested.nestedName in given bean instance
9 Write all the values of given PropertyBox to the given bean instance

3.15.3. BeanIntrospector cache

By default, the BeanIntrospector uses an internal cache of processed bean class adn property sets, to boost introspecton operations and obtain better performace.

If memory consuption issues are detected, the internal cache can be disabled setting holon.beans.introspector-cache-enabled configuration property to false. To set the configuration property, either a System property or a default holon.properties file can be used.

3.16. Datastore

The Datastore interface is the main entry point to manage data persistence in a technology/platform/vendor independent way.

The Datastore data management strategy relies on the Holon platform Properties architecture to express data model attributes in a generic and implementation-independent way, using PropertyBox as carrier for data model attributes and entity values.

A concrete Datastore implementation could provide a more specialized interface providing functionalities expressly related to the persistence technology/model to which the Datastore refers.

The Datastore interface provides the following operations:

  • Refresh: Refresh the data of a data model entity

  • Insert: Insert a data model entity into the persistence store

  • Update: Update a data model entity

  • Save: Insert or update a data model entity, depending on the exitence of the entity

  • Bulk operations on data model (bulkInsert, bulkUpdate and bulkDelete) to execute batch persistence operations

  • Query execution using the Query interface for query definition and execution.

Each operation which involves a possible persistence store data modification returns an OperationResult object, which provides information about the operation outcome, like the number of the elements affected by the execution of the operation or the auto-generated key values, if the concrete persistence store supports this feature.

3.16.1. Expressions and resolvers

Main Datastore operations relies on the core Expression type to configure and model data access and manipulation statements, which will be converted into concrete commands and requests to the underlying persistence store engine in its specific language.

To take part in the Datastore data access and manipulation statement definition processes, the ExpressionResolver interface can be used. This interface is designed to resolve, i.e. translate in a more convenient or suitable form, an Expression type into another Expression type.

An ExpressionResolver declares the expression type which is able to process, and the expression type which provides as resolution result. An ExpressionResolver can return an empty optional if it is not able to resolve given specific expression: this way, the resultion process must proceed to the next available resolver for given expression and resolution type.

When an Expression must be resolved, the Datastore detects all registered resolvers which declare to resolve the given expression type and provide a consistent resolution type. The resolvers are invoked sequentially, and the first valid resolved expression is taken (if any), interrupting the resolution process.

To provide a resolution sequence, i.e. a resolver invokation sequence, an ExpressionResolver class can be annotated with the standard javax.annotation.Priority annotation, where lower values corresponds to higher priority.

During the resolution process, a ResolutionContext object is provided to each ExpressionResolver. The resolution context provides a resolve method which allows to trigger a nested expression expression resolution operation. Each concrete Datastore implementation could also provide a more specialized ResolutionContext interface.

Tipically, an ExpressionResolver can be used to:

  • Translate a custom expression into a well-known Datastore expression (for example, a custom query filter class into a standard QueryFilter object);

  • Translate an expression attribute into one which can be correctly interpreted by a specific Datastore (for example, a symbolic data target name into a concrete persistence model structure name, such as a table name in a RDBMS);

  • Implement specific expressions for a concrete Datastore implementation.

To register or unregister an ExpressionResolver, the addExpressionResolver(…​) and removeExpressionResolver(…​) Datastore methods can be used.

See below for more informations about ExpressionResolver usage.

3.16.2. Property configuration and converters

The Datastore operations fully support property values conversions using the standard Converters. If a property declares a value converter, it will be used to perform conversions from the property type to the data model attribute type and back.

A converter can be used to adapt specific data types, use custom types or to face common conversion needs, such as enumeration property types mapped to integer or text data model types.

The Configuration structure can be used by a Datastore to detect the TemporalType associated to a java.util.Date or java.util.Calendar property type (through the getTemporalType() method), and ensure the consistency of the data manipulation and access operations which involve such kind of property.

The Holon platform Datastore fully supports the new Java 8 Date and Time API, which represents a big step forward compared to the previous date and time support classes, to address the shortcomings of the older java.util.Date and java.util.Calendar types. It is strongly recommended to use the new java.time.* types for date and time properties, such as LocalDate, LocalTime, LocalDateTime and so on. This way, in addition to achieving a more robust and consistent code, there no need to use the TemporalType property configuration attribute to ensure consistency in property value handling.

3.16.3. DataTarget

The DataTarget interface is used by Datastore and Query to refer to an entity of the persistence model in an abstract and independent way from the concrete persistence layer. From the DataTarget point of view, and entity has the meaning of a collection of data model attributes, and it is represented by a Path, i.e. by a symbolic name.

Examples of DataTarget representations are:

  • The name of a table in a RDBMS

  • The class of a JPA Entity

  • The document collection name in a document-oriented database

Concrete Datastore implementations could provide more specific DataTarget object types to identify an entity by using specific target types for the concrete persistence model which represent.

The DataTarget interface provides static methods to create data targets using the default name representation:

DataTarget<String> target = DataTarget.named("test");

All the Datastore operations involving persistent entity structures use a DataTarget to identify the target persistent structure and PropertyBox to provide and obtain the data model attribute values.

3.16.4. Data manipulation

The Datastore interface provides the most common data manipulation operations, listed in the table below.

Each operation throws a DataAccessException if an error occurs during operation execution.

Each operation supports configurable write options, represented by the WriteOption marker interface. Tipically, write options are specific of the underlying persistence model and each concrete Datastore implementation provides a set of suitable write options. See the specific datastores documentation for further information.

The DefaultWriteOption enumeration provides the write options available for any datastore. By now, a single default write option is defined:

BRING_BACK_GENERATED_IDS: Bring back any auto-generated id value into the PropertyBox which was subject of a data manipulation operation, if a corresponding Property (using the property name) is available in the box property set.

This option is only meaningful for datastores bound to a persistence engine which provides id auto-generation.
Operation Purpose Return

refresh(DataTarget target, PropertyBox propertyBox)

Refresh the values of the properties of given PropertyBox, reloading them from the persistence store

The refreshed PropertyBox

insert(DataTarget target, PropertyBox propertyBox, WriteOption…​ options)

Insert a new data entity, identified by given target and represented by given PropertyBox, into the persistence store

The OperationResult. If one or more data model attribute was auto-generated by the concrete persistence store, such values are returned by the getInsertedKeys method.

update(DataTarget target, PropertyBox propertyBox, WriteOption…​ options)

Update an existing data entity, identified by given target and represented by given PropertyBox, into the persistence store

The OperationResult.

save(DataTarget target, PropertyBox propertyBox, WriteOption…​ options)

Insert a new data entity (identified by given target and represented by given PropertyBox) into the persistence store if the entity does not exists, or update it if the entity is present in the persistence store.

The OperationResult.

delete(DataTarget target, PropertyBox propertyBox, WriteOption…​ options)

Remove a data entity, identified by given target and represented by given PropertyBox, from the persistence store

The OperationResult.

bulkInsert(DataTarget target, PropertySet<?> propertySet, WriteOption…​ options)

Obtain an interface which can be used to perform a bulk insert of data entities identified by given target, represented by PropertyBox instances. Only the properties contained in given propertySet will be taken into account to perform insert operations.

The BulkInsert interface to provide the PropertyBox instances to insert and execute the bulk operation.

bulkUpdate(DataTarget target, WriteOption…​ options)

Obtain an interface which can be used to perform a bulk update of data entities identified by given target, to change a set of property values according to a set of restriction predicates to identify the set of data entities to update

The BulkUpdate interface to provide the properties and the value to update, the restriction predicates, and execute the bulk operation.

bulkDelete(DataTarget target, WriteOption…​ options)

Obtain an interface which can be used to perform a bulk delete of data entities identified by given target, providing a set of restriction predicates to identify the set of data entities to remove

The BulkDelete interface to provide the PropertyBox instances, configure the restriction predicates and execute the bulk operation.

Datastore operations examples
final PathProperty<String> A_PROPERTY = PathProperty.create("propertyPath", String.class);
final DataTarget<String> TARGET = DataTarget.named("test");

final Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation

PropertyBox data = PropertyBox.builder(A_PROPERTY).set(A_PROPERTY, "aValue").build();

OperationResult result = datastore.save(TARGET, data); (1)

result = datastore.insert(TARGET, data); (2)
result = datastore.update(TARGET, data); (3)

PropertyBox refreshed = datastore.refresh(TARGET, data); (4)
datastore.delete(TARGET, refreshed); (5)

// Bulk operations
result = datastore.bulkInsert(TARGET, PropertySet.of(A_PROPERTY))
    .add(PropertyBox.builder(A_PROPERTY).set(A_PROPERTY, "aValue1").build())
    .add(PropertyBox.builder(A_PROPERTY).set(A_PROPERTY, "aValue2").build())
    .add(PropertyBox.builder(A_PROPERTY).set(A_PROPERTY, "aValue3").build()).execute(); (6)

result = datastore.bulkUpdate(TARGET).set(A_PROPERTY, "updated").filter(A_PROPERTY.isNull()).execute(); (7)

result = datastore.bulkDelete(TARGET).filter(A_PROPERTY.isNull()).execute(); (8)
1 Save the PropertyBox containing given property value using the defined DataTarget (insert a new entity if not present in the persistence store or update it if exists, using the specific persistence model strategy to identify entities)
2 Insert the given PropertyBox data into the persistence store using the defined DataTarget
3 Update the given PropertyBox data into the persistence store using the defined DataTarget
4 Refresh the PropertyBox using the defined DataTarget, reloading data attribute values
5 Remove the entity which corresponds to given PropertyBox (using the specific persistence model strategy to identify entities)
6 Execute a bulk insert operation using the defined DataTarget, inserting given PropertyBox elements
7 Execute a bulk update operation using the defined DataTarget, setting the property value to updated when that property value is null
8 Execute a bulk delete operation using the defined DataTarget, removing entities for which the given property value is null

3.17. Query

The Query interface can be used to execute queries on the persistence data structures, using Propertiess to refer to the data model attributes and PropertyBoxs to represent and obtain the query results.

Using the Query abstraction, a query can be performed on the data model in a generic and implementation-independent way.

A Query supports the following clauses and configuration attributes:

  • The DataTarget on which the query has to be performed

  • The query restrictions, expressed as QueryFilter clauses

  • The query results sorting, expressed as QuerySort clauses

  • The query results aggregation, expressed as Aggregations clauses

  • The query results paging, providing a result set limit and offset

  • Generic query configuration parameters

  • Support for DataTargetResolver and [QueryClauseResolver] registration

The query clauses which involve the reference to data model attributes (filtering, sorting, aggregation) are expressed using the Properties abstraction.

3.17.1. Query definition

A Query instance is obtained using the the Datastore query() method.

The Query interface provides methods to configure and define the query configuration and clauses, using a fluent builder pattern. The query configuration setting and clauses definitions are described below.

QueryFilter

The QueryFilter interface represents a query results restriction.

Most common restriction predicates representations are provided by the core platform classes. The following predicates are available:

  • Is null / is not null: The value of a property is null / not null

  • equal / not equal: The value of a property is equal / not equal to a given value

  • less than / less than or equal: The value of a property is less than / less than or equal to a given value

  • greater than / greater than or equal: The value of a property is greater than / greater than or equal to a given value

  • between: The value of a property is included between a minimum and a maximum value

  • in / not in: The value of a property is present / not present in a set of values

  • contains: The value of a String property contains given text, ignoring case or not

  • startsWirth: The value of a String property contains starts with text, ignoring case or not

  • endsWith: The value of a String property contains ends with text, ignoring case or not

Negation, conjunction and disjunction of predicates:

  • not: Negation of a predicate

  • and: Conjunction of predicates (represents the AND logical operation)

  • or: Disjunction of predicates (represents the OR logical operation)

The QueryFilter predicates can be obtained in two ways:

1. Using the static methods provided by the QueryFilter interface, specifying the Properties the predicate refers to when required.

Examples using QueryFilter
final PathProperty<String> PROPERTY = PathProperty.create("test", String.class);
final PathProperty<String> ANOTHER_PROPERTY = PathProperty.create("another", String.class);

QueryFilter restriction = QueryFilter.isNotNull(PROPERTY); // is not null
restriction = QueryFilter.isNull(PROPERTY); // is null
restriction = QueryFilter.eq(PROPERTY, "value"); // equal to a value
restriction = QueryFilter.eq(PROPERTY, ANOTHER_PROPERTY); // equal to a property
restriction = QueryFilter.neq(PROPERTY, "value"); // not equal
restriction = QueryFilter.lt(PROPERTY, "value"); // less than
restriction = QueryFilter.loe(PROPERTY, "value"); // less than or equal
restriction = QueryFilter.gt(PROPERTY, "value"); // greater than
restriction = QueryFilter.goe(PROPERTY, "value"); // greater than or equal
restriction = QueryFilter.between(PROPERTY, "value1", "value2"); // between
restriction = QueryFilter.in(PROPERTY, "value1", "value2", "value3"); // in
restriction = QueryFilter.nin(PROPERTY, "value1", "value2", "value3"); // not in
restriction = QueryFilter.startsWith(PROPERTY, "V", false); // starts with 'v'
restriction = QueryFilter.startsWith(PROPERTY, "v", true); // starts with 'v', ignoring case
restriction = QueryFilter.endsWith(PROPERTY, "v", false); // ends with 'v'
restriction = QueryFilter.contains(PROPERTY, "v", false); // contains 'v'
QueryFilter restriction2 = QueryFilter.contains(PROPERTY, "v", true); // contains 'v', ignoring case

// negation
QueryFilter negation = QueryFilter.not(restriction);
negation = restriction.not();

// conjuction
QueryFilter conjuction = restriction.and(restriction2);
conjuction = QueryFilter.allOf(restriction, restriction2).orElse(null);

// disjunction
QueryFilter disjunction = restriction.or(restriction2);
disjunction = QueryFilter.anyOf(restriction, restriction2).orElse(null);

2. For Properties related predicates, using directly the static methods provided by the Property interface.

QueryFilter s can be added to a Query using the filter(QueryFilter filter) query builder method, which adds a filter predicate to the query. If a QueryFilter has already been associated to the query, the new filter is added to the query in conjunction with the existing predicates, i.e. using a AND logical operation.
Examples using Property
final PathProperty<String> PROPERTY = PathProperty.create("test", String.class);
final PathProperty<String> ANOTHER_PROPERTY = PathProperty.create("another", String.class);

QueryFilter restriction = PROPERTY.isNotNull(); // is not null
restriction = PROPERTY.isNull(); // is null
restriction = PROPERTY.eq("value"); // equal to a value
restriction = PROPERTY.eq(ANOTHER_PROPERTY); // equal to a property
restriction = PROPERTY.neq("value"); // not equal
restriction = PROPERTY.lt("value"); // less than
restriction = PROPERTY.loe("value"); // less than or equal
restriction = PROPERTY.gt("value"); // greater than
restriction = PROPERTY.goe("value"); // greater than or equal
restriction = PROPERTY.between("value1", "value2"); // between
restriction = PROPERTY.in("value1", "value2", "value3"); // in
restriction = PROPERTY.nin("value1", "value2", "value3"); // not in
restriction = PROPERTY.startsWith("v"); // starts with
restriction = PROPERTY.startsWithIgnoreCase("v"); // starts with ignoring case
restriction = PROPERTY.endsWith("v"); // ends with
restriction = PROPERTY.endsWithIgnoreCase("v"); // ends with ignoring case
restriction = PROPERTY.contains("v"); // contains
QueryFilter restriction2 = PROPERTY.containsIgnoreCase("v"); // contains ignoring case

// negation
QueryFilter negation = PROPERTY.eq("value").not();

// conjuction
QueryFilter conjuction = PROPERTY.isNotNull().and(PROPERTY.eq("value"));

// disjunction
QueryFilter disjunction = PROPERTY.isNull().or(PROPERTY.eq("value"));
QuerySort

The QuerySort interface represents a query results sorting directive, referred to a Properties.

The SortDirection enumeration is used to declare the sort direction (ascending or descending).

A list of QuerySort can be composed to create a sort declaration which involves more than one Property, for each declaring the sort direction.

The QuerySort directives can be obtained in two ways:

1. Using the static methods provided by the QuerySort interface, specifying the Properties to which the sort refers.

Examples using QuerySort
final PathProperty<String> PROPERTY = PathProperty.create("test", String.class);
final PathProperty<String> ANOTHER_PROPERTY = PathProperty.create("another", String.class);

QuerySort sort = QuerySort.asc(PROPERTY); // sort ASCENDING on given property
sort = QuerySort.desc(PROPERTY); // sort DESCENDING on given property

QuerySort sort2 = QuerySort.of(ANOTHER_PROPERTY, SortDirection.ASCENDING); // sort ASCENDING on given property
sort2 = QuerySort.of(ANOTHER_PROPERTY, true); // sort ASCENDING on given property

QuerySort.of(sort, sort2); // sort using 'sort' and 'sort2' declarations, in the given order

2. Using directly the static methods provided by the Property interface.

Examples using Property
final PathProperty<String> PROPERTY = PathProperty.create("test", String.class);
final PathProperty<String> ANOTHER_PROPERTY = PathProperty.create("another", String.class);

QuerySort sort = PROPERTY.asc(); // sort ASCENDING on given property
sort = PROPERTY.desc(); // sort DESCENDING on given property

PROPERTY.asc().and(ANOTHER_PROPERTY.desc()); // sort ASCENDING on PROPERTY, than sort DESCENDING on
                        // ANOTHER_PROPERTY
QuerySort can be added to a Query using the sort(QuerySort sort) query builder method, which adds a sort declaration to the query. If one or more QuerySort has already been associated to the query, the new sort is added at the end of the query sorts list.
Aggregations

Aggregation clause:

A Query supports a results aggregation clause, represented by the QueryAggregation interface. This interface provides a fluent builder to create aggregation clauses.

The aggregation clause can be used to specify:

  • Which `Path`s must be used to aggregate the query results, i.e. grouping the results by the values of the specified paths;

  • The optional restrictions to apply on the aggregation path values, expressed by using standard QueryFilter predicates.

Query results aggregation semantics can be slightly different from one Datastore implementation to another. Each Datastore implementation should ensure a consistent query execution behaviour, but in some situations it may not be possible to perform the aggregation operation for some query configurations. For example, many RDBMS engines do not allow to project a query result which is not part of the query aggregation clause unless an aggregation function is used.
Examples of aggregation
final PathProperty<Integer> PROPERTY = PathProperty.create("test", Integer.class);
final PathProperty<String> ANOTHER_PROPERTY = PathProperty.create("another", String.class);

Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation

Stream<PropertyBox> results = datastore.query().target(DataTarget.named("testTarget")).aggregate(PROPERTY)
    .stream(PROPERTY, ANOTHER_PROPERTY.max()); (1)

results = datastore.query().target(DataTarget.named("testTarget"))
    .aggregate(QueryAggregation.builder().path(PROPERTY).filter(PROPERTY.isNotNull()).build())
    .stream(PROPERTY, ANOTHER_PROPERTY.max()); (2)
1 Aggregate the results grouping by PROPERTY
2 Aggregate the results grouping by PROPERTY and restrict the aggregation property values to not null values

Aggregation functions:

A set of basic results aggregation functions are provided by the default Datastore. An aggregation function is related to a Path and allows to aggregate the query results related to such path with a specific aggregation semantic.

A Datastore function is represented by the ../api/holon-core/com/holonplatform/core/query/QueryFunction.html[QueryFunction^] interface and the following builtin aggregation functions are provided:

  • ../api/holon-core/com/holonplatform/core/query/QueryFunction.Count.html[Count^] : counts the results

  • ../api/holon-core/com/holonplatform/core/query/QueryFunction.Min.html[Min^] : return the smallest value

  • ../api/holon-core/com/holonplatform/core/query/QueryFunction.Max.html[Max^] : return the larger value

  • ../api/holon-core/com/holonplatform/core/query/QueryFunction.Avg.html[Avg^] : return the average value

  • ../api/holon-core/com/holonplatform/core/query/QueryFunction.Sum.html[Sum^] : return the sum of the values

An aggregation function can be used as a query projection or expression, and can be created in two ways:

1. Using the static methods provided by the FunctionExpression interface, by specifying the Path to apply the function to. 2. Using directly the static methods provided by the PathProperty interface.

Examples of aggregation functions
final PathProperty<Integer> PROPERTY = PathProperty.create("test", Integer.class);

// Using AggregationProperty
FunctionExpression<Long> expression = FunctionExpression.count(PROPERTY);

// Using the property
FunctionExpressionProperty<Integer> sp = PROPERTY.sum();
Paging results

Query supports results pagination, allowing to set:

  • limit:: results limit, i.e. the max number of results to provide

  • offset:: 0-based offset from which to fetch the query results within the total results set

Pagination example
final PathProperty<Integer> PROPERTY = PathProperty.create("test", Integer.class);

Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation

Stream<Integer> values = datastore.query().target(DataTarget.named("testTarget")).limit(100).offset(0)
    .stream(PROPERTY); (1)
values = datastore.query().target(DataTarget.named("testTarget")).limit(100).offset(100).stream(PROPERTY); (2)
1 Limit the query results to 100 and fetch the results starting from 0 (i.e. fetch the first page where a page is a window of 100 results)
2 Limit the query results to 100 and fetch the results starting from 100 (i.e. fetch the second page where a page is a window of 100 results)
Query results projection

Query results are obtained using a projection, represented by the QueryProjection interface.

The Query interface extends the QueryResults interface, which provides methods to execute the query and obtain the query results.

Two main methods are provided to obtain the query results:

  • stream(QueryProjection<R> projection): Obtain the query results as a Stream, using given projection to define the type of the objects contained in the stream

  • count(): Count the results of the query

Two additional convenience methods are made available by the QueryResults interface:

  • findOne(QueryProjection<R> projection): Get a single expected result from the query, which type matches the projection type. If more than one result is obtained from query execution, a QueryNonUniqueResultException is thrown

  • list(QueryProjection<R> projection): Convenience method to obtain query results Stream as a List

Builtin projections:

The platform provides the following query projections:

  • PathProperty and AggregationProperty are query projections, mapping the result type into the property type

  • PropertySetProjection uses a set of Property to map the query results into PropertyBox objects containing the values of the properties of the set returned by the query execution.

The QueryResults interface provides convenience methods helpful to use the PropertySetProjection query projection:

  • stream(Iterable<P> properties) and stream(Property…​ properties): accept a set of Properties and return the query results as a stream of PropertyBox, each containing the values of the properties of the set obtained from query execution

  • list(Iterable<P> properties): Same as above, but returning the results as a List instead of a Stream

  • findOne(Iterable<P> properties) and findOne(Property…​ properties): accept a set of Properties and return the unique query result as an Optional PropertyBox containing the values of the properties of the set obtained from query execution

Any concrete Datastore implementation could provide additional projections related to the specific Datastore persistence model.
Query projection examples
final PathProperty<Integer> PROPERTY = PathProperty.create("test", Integer.class);
final PathProperty<String> ANOTHER_PROPERTY = PathProperty.create("another", String.class);

Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation

Stream<Integer> values = datastore.query().target(DataTarget.named("testTarget")).stream(PROPERTY); (1)
Optional<Integer> value = datastore.query().target(DataTarget.named("testTarget")).findOne(PROPERTY); (2)
Stream<PropertyBox> boxes = datastore.query().target(DataTarget.named("testTarget")).stream(PROPERTY,
    ANOTHER_PROPERTY); (3)
List<PropertyBox> list = datastore.query().target(DataTarget.named("testTarget")).list(PROPERTY,
    ANOTHER_PROPERTY); (4)
Optional<PropertyBox> box = datastore.query().target(DataTarget.named("testTarget")).findOne(PROPERTY,
    ANOTHER_PROPERTY); (5)
Optional<Integer> sum = datastore.query().target(DataTarget.named("testTarget")).findOne(PROPERTY.sum()); (6)
1 Stream of values using PROPERTY as projection. The Stream type is the same of the property type (Integer)
2 Optional unique value using PROPERTY as projection. The value type is the same of the property type (Integer)
3 Stream of PropertyBox containing the values of the set of properties composed by PROPERTY and ANOTHER_PROPERTY
4 List of PropertyBox containing the values of the set of properties composed by PROPERTY and ANOTHER_PROPERTY
5 Optional unique PropertyBox containing the values of the set of properties composed by PROPERTY and ANOTHER_PROPERTY
6 Unique value of the sum aggregation function applied to PROPERTY

Bean projection:

The BeanProjection interface can be used to obtain the query results as Java Bean instances, providing the mapping bean class to be used. The query results are mapped to the bean class properties by matching the query selection Path names with the bean property names.

Bean projection examples
class MyBean {

  private Integer code;
  private String text;

  public Integer getCode() {
    return code;
  }

  public void setCode(Integer code) {
    this.code = code;
  }

  public String getText() {
    return text;
  }

  public void setText(String text) {
    this.text = text;
  }

}

public void beanProjection() {
  Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation

  Stream<MyBean> results = datastore.query().target(DataTarget.named("testTarget"))
      .stream(BeanProjection.of(MyBean.class)); (1)
  Optional<MyBean> result = datastore.query().target(DataTarget.named("testTarget"))
      .findOne(BeanProjection.of(MyBean.class)); (2)

  final PathProperty<Integer> CODE = PathProperty.create("code", Integer.class);
  final PathProperty<String> TEXT = PathProperty.create("text", String.class);

  results = datastore.query().target(DataTarget.named("testTarget"))
      .stream(BeanProjection.of(MyBean.class, CODE, TEXT)); (3)
}
1 Stream of values using MyBean as projection, using all the bean property names as query selection
2 Optional unique value using MyBean as projection, using all the bean property names as query selection
3 Stream of values using MyBean as projection, using provided CODE and TEXT properties as query selection

3.17.2. Configuration

The DatastoreConfigProperties interface represents and provides the available configuration properties which can be used to configure a generic Datastore.

The interface extends a default ConfigPropertySet bound to the property name prefix holon.datastore.

The available configuration properties are listed below:

Table 2. Datastore configuration properties
Name Type Meaning

holon.datastore. trace

Boolean (true / false)

Enable/disable Datastore operations tracing in log (for example, the concrete query definitions)

The DatastoreConfigProperties can be loaded from a number of sources using the default ConfigPropertySet builder interface:

DatastoreConfigProperties config = DatastoreConfigProperties.builder().withDefaultPropertySources().build(); (1)

config = DatastoreConfigProperties.builder().withSystemPropertySource().build(); (2)

Properties props = new Properties();
props.put("holon.datastore.trace", "true");
config = DatastoreConfigProperties.builder().withPropertySource(props).build(); (3)

config = DatastoreConfigProperties.builder().withPropertySource("datastore.properties").build(); (4)
1 Read the configuration properties from default property sources (i.e. the holon.properties file)
2 Read the configuration properties from System properties
3 Read the configuration properties from a Properties instance
4 Read the configuration properties from the datastore.properties file
Multiple Datastore configuration

When multiple Datastore configuration is required and properties are read from the same source, a data context id can be used to discern one Datastore configuration property set form another.

From the property source point of view, the data context id is used as a suffix after the configuration property set name (holon.datastore) and before the specific property name.

For example, let’s say we have a configuration property set for two different datastores as follows:

holon.datastore.one.trace=true

holon.datastore.two.trace=false

In order to provide the configuration for two Datastore instances, one bound to the one configuration property set and the other bound to the two configuration property set, the DatastoreConfigProperties can be obtained as follows, specifying the data context id when obtaining the builder:

DatastoreConfigProperties config1 = DatastoreConfigProperties.builder("one")
    .withPropertySource("datastore.properties").build();

DatastoreConfigProperties config2 = DatastoreConfigProperties.builder("two")
    .withPropertySource("datastore.properties").build();

3.17.3. Common Datastore configuration properties

The DatastoreConfigProperties interface is a ConfigPropertySet bound to the property name prefix holon.datastore, which provides common Datastore configuration properties:

Name Type Meaning

holon.datastore. trace

Boolean (true/false)

Enable or disable Datastore operations tracing (for example, logging the concrete query/operations executed in native persistence store language).

holon.datastore. dialect

String

The fully qualified class name of the dialect to be used, if the concrete Datastore supports dialects.

3.17.4. Relational Datastores

Regarding the relational Datastore implementations, i.e. Datastores bound to a RDBMS, some additional components are provided to express typical relational concepts concerning query definition and execution.

Sub-query

The SubQuery interface can be used to represent a sub-query, which can be used in a query definition to express query restrictions (filters) that involve a sub-query as filter operand.

To create a SubQuery, the create(…​) static method of the SubQuery interface can be used. The SubQuery definition process (target, restrictions, ordering and so on) is the same of a normal Query definition process, sharing the same query builder interface. In addition, a SubQuery must provide a QueryProjection to define the sub query selection type.

A SubQuery is a QueryExpression, allowing to use it as a QueryFilter operand.

When a SubQuery is used in a query, to avoid property/column names ambiguity, it is strongly recommended to provide a parent DataTarget for the query properties. The parent DataTarget of a Property can be setted using the parent(…​) method of the property builder or directly using the property(…​) methods provided by the DataTarget interface to create a Property with the given DataTarget as parent.
Datastore datastore = getDatastore(); // this is supposed to be a relational Datastore implementation

final DataTarget TARGET1 = DataTarget.named("testTarget1");
final PathProperty<Integer> PROPERTY1 = TARGET1.property("test", Integer.class);

final DataTarget TARGET2 = DataTarget.named("testTarget2");
final PathProperty<Integer> PROPERTY2 = TARGET2.property("test", Integer.class);

SubQuery<Integer> subQuery = SubQuery.create(datastore).target(TARGET2).filter(PROPERTY1.goe(1))
    .select(PROPERTY1); (1)

Stream<Integer> results = datastore.query().target(TARGET1).filter(PROPERTY2.in(subQuery)).stream(PROPERTY2); (2)
1 Create a SubQuery
2 Use the SubQuery as the right operand of a IN query filter

Two convenience methods are provided by the SubQuery interface to create EXISTS and NOT EXISTS filter predicates. In this case, the sub-query selection projection is not required, and by default a 1 literal value will be used as selection.

Datastore datastore = getDatastore(); // this is supposed to be a relational Datastore implementation

final DataTarget TARGET1 = DataTarget.named("testTarget1");
final PathProperty<Integer> PROPERTY1 = TARGET1.property("test", Integer.class);

final DataTarget TARGET2 = DataTarget.named("testTarget2");
final PathProperty<Integer> PROPERTY2 = TARGET2.property("test", Integer.class);

Stream<Integer> results = datastore.query().target(TARGET1)
    .filter(SubQuery.create(datastore).target(TARGET2).filter(PROPERTY2.eq(PROPERTY1)).exists())
    .stream(PROPERTY2); (1)

results = datastore.query().target(TARGET1)
    .filter(SubQuery.create(datastore).target(TARGET2).filter(PROPERTY2.eq(PROPERTY1)).notExists())
    .stream(PROPERTY2); (2)
1 A query with a filter using a EXISTS SubQuery predicate
2 A query with a filter using a NOT EXISTS SubQuery predicate
Alias and Joins

The RelationalTarget interface can be used to express alias and joins for a DataTarget.

A RelationalTarget is a DataTarget itself, and provides methods to assign an alias name to the query target and to create joins with other targets.

final DataTarget<String> TARGET = DataTarget.named("testTarget");

RelationalTarget<String> RT = RelationalTarget.of(TARGET); (1)
RelationalTarget<String> RT2 = RT.alias("aliasName"); (2)
1 Create a RelationalTarget using given TARGET
2 Create a new RelationalTarget from the previous one, assigning an alias name to it

The following join types are supported:

  • INNER JOIN: returns all rows when there is at least one match in BOTH tables represented by the source DataTarget and the joined DataTarget;

  • LEFT JOIN: returns all rows from the left table (represented by the source DataTarget), and the matched rows from the right table (represented by the joined DataTarget);

  • RIGHT JOIN: returns all rows from the right table (represented by the joined DataTarget), and the matched rows from the left table (represented by the source DataTarget);

Only one level of joins is supported, i.e. only the source DataTarget represented by the RelationalTarget can be joined with other targets, and no sub join is supported.

The Join interface represents the join expression, supporting an alias name definition and a ON clause definition, to express any join restriction/filter predicate.

A RelationalTarget is created form a conventional DataTarget using the of(DataTarget target) static method.

When joins are used in a query, to avoid property/column names ambiguity, it is strongly recommended to provide a parent DataTarget for the query properties. The parent DataTarget of a Property can be setted using the parent(…​) method of the property builder or directly using the property(…​) methods provided by the DataTarget interface to create a Property with the given DataTarget as parent.
final DataTarget TARGET1 = DataTarget.named("testTarget1");
final PathProperty<Integer> PROPERTY1 = TARGET1.property("test", Integer.class);

final DataTarget TARGET2 = DataTarget.named("testTarget2");
final PathProperty<Integer> PROPERTY2 = TARGET2.property("test", Integer.class);

RelationalTarget<String> RT = RelationalTarget.of(TARGET1) (1)
    .join(TARGET2, JoinType.INNER).on(PROPERTY2.eq(PROPERTY1)).add(); (2)

RT = RelationalTarget.of(TARGET1).innerJoin(TARGET2).on(PROPERTY2.eq(PROPERTY1)).add(); (3)
RT = RelationalTarget.of(TARGET1).leftJoin(TARGET2).on(PROPERTY2.eq(PROPERTY1)).add(); (4)
RT = RelationalTarget.of(TARGET1).rightJoin(TARGET2).on(PROPERTY2.eq(PROPERTY1)).add(); (5)

Stream<Integer> results = getDatastore().query().target(RT).stream(PROPERTY1); (6)
1 Create a RelationalTarget using TARGET1
2 Join (using a INNER join type) the TARGET1 with the TARGET2, using a ON clause to express the join condition
3 Join (using a INNER join type) the TARGET1 with the TARGET2, using a ON clause to express the join condition
4 Join (using a LEFT join type) the TARGET1 with the TARGET2, using a ON clause to express the join condition
5 Join (using a RIGHT join type) the TARGET1 with the TARGET2, using a ON clause to express the join condition
6 Use the created RelationalTarget as a query target

3.17.5. Datastore extension

Datastores provides two main entry points for extension purposes:

  • Datastore components and operations extension through ExpressionResolver, which can be used to add custom expressions in order to extend or modify the data manipulation operations and query definition and execution strategies.

  • Additional Datastore operations and functionalities relying on the DatastoreCommodity concept, through the registration of a DatastoreCommodityFactory.

Common ExpressionResolver s

For the most common query use cases, a set of builtin ExpressionResolver are made available to extend the main query clauses, providing custom clause representations to be translated into a standard query clause which the Datastore can handle.

An ExpressionResolver can be registered or unregistered in a Datastore implementation using the addExpressionResolver(…​) and removeExpressionResolver(…​) methods.

See concrete Datastore implementations documentation for additional information about any other ExpressionResolver based extension capabilities and resolvers registration options.
DataTargetResolver

A DataTargetResolver can be defined and registered in Datastore to resolve a DataTarget unknown to the concrete Datastore implementation, by translating it into one which the Datastore can recognize and handle.

Tipically, a DataTargetResolver can be defined to resolve a DataTarget with a symbolic name into a specific Datastore data target.

DataTargetResolver example
ExpressionResolver resolver = DataTargetResolver.create(DataTarget.class,
    (target, context) -> "test".equals(target.getName())
        ? Optional.of(DataTarget.named("wellKnownTargetName")) : Optional.empty()); (1)

Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation
datastore.addExpressionResolver(resolver); (2)
1 Create a resolver which translates the symbolic test name in another named target with the wellKnownTargetName name
2 Register the resolver in the Datastore
Custom QueryFilters

You can define and register custom QueryFilter implementations, to provide new predicates as a combination of simple standard predicates, possibly relying on parameters or application specific conditions, or to express specific filtering conditions related to a concrete persistence model to which a Datastore is bound.

A custom QueryFilter can be registered in a Datastore using standard Expressions and resolvers interface.

A convenience QueryFilterResolver interface is provided to facilitate the creation of a QueryFilter resolver.

A typical custom QueryFilter definition process takes place with the following steps:

  1. First af all, you have to define your custom filter representation, providing a class which implements the QueryFilter interface (and, optionally, an interface which extends QueryFilter and represents your custom filter API);

  2. Then create a class which implements QueryFilterResolver, generalized on your custom filter class/interface, whose purpose is to resolve the custom filter, transforming it into a QueryFilter that can be handled by the concrete Datastore. Two examples are: one of the builtin QueryFilter as described above, or a persistence model specific filter type recognized by a specific Datastore implementation.

  3. Finally, register the QueryFilterResolver in the Datastore instance, using the addExpressionResolver(…​) method.

When the custom filter is defined and registered, it can be used anywhere in the query or bulk operations clauses obtained from the Datastore in which the resolver is registered.

Custom QueryFilter example
class MyFilter implements QueryFilter { (1)

  final PathProperty<String> property;
  final String value;

  public MyFilter(PathProperty<String> property, String value) {
    this.property = property;
    this.value = value;
  }

  @Override
  public void validate() throws InvalidExpressionException {
    if (value == null)
      throw new InvalidExpressionException("Value must be not null");
  }

}

class MyFilterResolver implements QueryFilterResolver<MyFilter> { (2)

  @Override
  public Class<? extends MyFilter> getExpressionType() {
    return MyFilter.class;
  }

  @Override
  public Optional<QueryFilter> resolve(MyFilter expression, ResolutionContext context)
      throws InvalidExpressionException {
    return Optional
        .of(expression.property.isNotNull().and(expression.property.contains(expression.value, true))); (3)
  }

}

final static PathProperty<String> PROPERTY = PathProperty.create("testProperty", String.class);

public void customFilter() {
  Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation
  datastore.addExpressionResolver(new MyFilterResolver()); (4)

  Stream<String> results = datastore.query().target(DataTarget.named("test"))
      .filter(PROPERTY.isNotNull().and(new MyFilter(PROPERTY, "testValue"))).stream(PROPERTY); (5)
}
1 Custom filter definition, implementing QueryFilter
2 Custom filter resolver class
3 The resolver translates a MyFilter into a predicate composed by well-known standard QueryFilter
4 The resolver is registered in the Datastore, enabling the use of the MyFilter type filter in query and bulk operations clauses
5 Use of a MyFilter in a query execution
Custom QuerySorts

A custom QuerySort can be registered in a Datastore using standard Expressions and resolvers interface.

A convenience QuerySortResolver interface is provided to facilitate the creation of a QuerySort resolver.

A typical custom QuerySort definition process takes place with the following steps:

  1. First af all, you have to define your custom sort representation, providing a class wich implements the QuerySort interface (and, optionally, an interface which extends QuerySort and represents your custom filter API);

  2. Then create a class which implements QuerySortResolver, generalized on your custom sort class/interface, whose purpose is to resolve the custom sort, transforming it into a QuerySort declaration which the concrete Datastore can handle. For example, one of the builtin QuerySort as described above, or a persistence model specific sort type recognized by a specific Datastore implementation.

  3. Finally, register the QuerySortResolver in the Datastore instance, using the addExpressionResolver(…​) method.

When the custom sort is defined and registered, it can be used anywhere in the query clauses obtained from the Datastore in which the resolver is registered.

Custom QuerySort example
class MySort implements QuerySort { (1)

  @Override
  public void validate() throws InvalidExpressionException {
  }

}

class MySortResolver implements QuerySortResolver<MySort> { (2)

  final PathProperty<String> P1 = PathProperty.create("testProperty1", String.class);
  final PathProperty<Integer> P2 = PathProperty.create("testProperty2", Integer.class);

  @Override
  public Class<? extends MySort> getExpressionType() {
    return MySort.class;
  }

  @Override
  public Optional<QuerySort> resolve(MySort expression, ResolutionContext context)
      throws InvalidExpressionException {
    return Optional.of(P1.asc().and(P2.desc())); (3)
  }

}

public void customSort() {
  Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation
  datastore.addExpressionResolver(new MySortResolver()); (4)

  Stream<String> results = datastore.query().target(DataTarget.named("test")).sort(new MySort()).stream(PROPERTY); (5)
}
1 Custom sort definition, implementing QuerySort
2 Custom sort resolver class
3 The resolver translates a MySort into a sort composed by well-known standard QuerySort
4 The resolver is registered in the Datastore, enabling the use of the MySort type sort in query clauses
5 Use of a MySort in a query execution
Datastore commodities definition and registration

Using the DatastoreCommodity representation, a Datastore can be extended by adding new operations and functionalities, represented by a class which implements the DatastoreCommodity interface.

A DatastoreCommodity must be provided using a DatastoreCommodityFactory implementation, which has to be registered in the target Datastore through the registerCommodity(DatastoreCommodityFactory<X, C> commodityFactory) method.

Each commodity factory is bound to a specific DatastoreCommodity type, provided by the getCommodityType() factory method, and can use a DatastoreCommodityContext to create and configure the commodity instance when requested. Typically, each concrete Datastore implementation provides a specific DatastoreCommodityContext extension, to provide useful Datastore context and configuration references.

A DatastoreCommodity can be obtained from Datastore using the create(Class<C> commodityType) method. A DatastoreCommodityFactory bound to the requested commodity type must be available, i.e. previuosly registered in Datastore, in order to obtain the commodity instance.

The Query object itself is a DatastoreCommodity, obtained by the convenience Datastore query() method. Each concrete Datastore implementation register a default factory to provide such commodity.

See concrete Datastore implementations documentation for additional information about any specific available commodity types and registration options.

3.17.6. Available Datastores

By now, the holon platform provides two default Datastore implementations:

  • JDBC Datastore: using the Java Database Connectivity (JDBC) specification to access a relational database

  • JPA Datastore: using the Java Persistence API specification to access a relational database

3.18. Multi tenancy support

The core Holon platform module provides the TenantResolver interface, which acts as default platform strategy representation to obtain the String which identifies the current tenant in a multi-tenant enviroment.

The interface provides a getCurrent() convenience method to obtain the current TenantResolver registered in Context, if available.

Other specific platform modules use this interface to provide their multi-tenancy related functionalities. See specific modules documentation for further details.

3.19. Utilities

The core Holon platform module provides some utility interfaces/classes which can be used in applications development.

3.19.1. Initializer

The Initializer interface can be used to perform a lazy initialization of a generic value (with the same type of the generic Initializer type) and provides some static methods to create Initializer implementations:

Initializer<String> intzr = Initializer.using(() -> "test"); (1)
String lazyInited = intzr.get(); (2)
1 Create an Initializer using a Supplier to provide the lazy-initialized value
2 Only the first time the get() method is invoked, the value is initialized using given Supplier and than is returned to the caller

3.19.2. SizedStack

The SizedStack class is a java.util.Stack extension which supports a max stack size, given at construction time.

When the stack size exceeds the max size, the eldest element is removed before adding a new one on the top of the stack.

4. HTTP and REST

The holon-http artifact provides base HTTP protocol support to the Holon platform, dealing with HTTP messages and providing support for RESTful web services invocation through a client API.

Maven coordinates:

<groupId>com.holon-platform.core</groupId>
<artifactId>holon-http</artifactId>
<version>5.0.0</version>

4.1. HTTP messages

The Holon platform provides an implementation-independent representation of the Hypertext Transfer Protocol request and response messages, used by other platform modules to deal with HTTP-based operations.

The HTTP request and response message representations are based on the Message interface, which represent a generic message consisting of a map of message headers (identified by a textual header name) and a payload which represents the content delivered in the message.

For an HTTP message, the message header values are represented as a List of Strings, since HTTP supports multiple values for each header. The message payload is represented as a String.

4.1.1. HttpRequest

The HttpRequest interface represents a HTTP request message and provides the following informations and operations:

  • The HTTP method describing the desired action to be performed

  • The fully qualified name of the client host or the last proxy that sent the request

  • The request message path

  • The request URI query parameters, if any

  • The request cookies, if any

  • An InputStream of the request message body (payload)

4.1.2. HttpResponse

The HttpResponse interface represents a HTTP response message and provides the following informations and operations:

  • The HTTP status code of the response, also represented with the convenience HttpStatus enumeration

  • A builder to create default HttpResponse instances

4.2. RESTful client

The Holon platform provides an implementation-independent representation of a client to deal with a RESTful web services API, using the HTTP protocol.

The client provides a fluent builder to compose and execute a RESTful service invocation, using template variable substitution, supporting base authentication methods, common headers configuration and request entities definition.

The client is represented by the RestClient interface and its main features are:

  • Support for a default target request base URI

  • Support for default request headers

  • Support for URI template variable substitutions

  • Support for request URI query parameters

  • Convenience methods to setup common request message headers, such as

    • Accepted response media types

    • Acceptable languages

    • Acceptable encodings

    • Acceptable charsets

    • Configure a Cache-Control header

  • Convenience method to setup authorization headers(Basic and Bearer types)

  • Perform the request invocation with a specific HTTP method providing a request message entity

  • Convenience methods to perform most common invocations using one of the GET, POST, PUT, PATCH, DELETE, OPTIONS, TRACE or HEAD methods

4.2.1. Obtain a RestClient instance

Concrete RestClient implementations are obtained from a RestClientFactory, registered using Java service extensions through a com.holonplatform.http.rest.RestClientFactory file under the META-INF/services folder.

A RestClient instance can be obtained using one of the create(…​) methods provided by the interface, either specifying the fully qualified class name of the RestClient implementation to obtain or using the default implementation according to the available RestClientFactory within the current ClassLoader (a specific ClassLoader can be used instead of the current one).

If more than one RestClientFactory is bound to the same RestClient implementation type, or if more than one RestClientFactory is available in the ClassLoader when the implementation class is not specified, the RestClientFactory to use to build the RestClient instance is selected according to the factory priority level, which can be specified using the Priority annotation, if available.
The forTarget(…​) static methods of the RestClient interface can be used as shorters to create a RestClient using the default implementation and setting a default base URI to use for the client requests.
RestClient creation examples
// Create a RestClient using the default available implementation for current ClassLoader
RestClient client = RestClient.create();

// Create a RestClient using a specific implementation class name
client = RestClient.create("com.holonplatform.jaxrs.client.JaxrsRestClient");

// Create a RestClient using the default available implementation and set a default base URI
client = RestClient.forTarget("https://host/api");

[[Available implementations]] ===== Available implementations

The RestClient implementations provided by the Holon Platform are are:

  • A JAX-RS based implementation, using a JAX-RS Client to perform invocations, available from the holon-jaxrs.html[Holon platform JAX-RS module];

  • A Spring based implementation, using a Spring RestTemplate to perform invocations;

4.2.2. Configure defaults

A RestClient instance supports some request default configuration attributes:

  • A default target, i.e. the default base URI which will be used for all the requests performed with the RestClient, unless overridden using the specific request configuration target method.

  • A set of default headers to be included in all the requests performed with the RestClient.

RestClient client = RestClient.create();

client.defaultTarget(new URI("https://rest.api.example")); (1)

client.withDefaultHeader(HttpHeaders.ACCEPT_LANGUAGE, "en-CA"); (2)
client.withDefaultHeader(HttpHeaders.ACCEPT_CHARSET, "utf-8"); (3)
1 Set the default target request base URI, which will be used as target URI for every request configured using request(), if not overridden using target(URI).
2 Add a default request header which will be automatically added to every invocation request message
3 Add another default request header

4.2.3. Build a request

To build a client request, the RequestDefinition is used, which represents both a fluent builder to configure the request message and an Invocation to perform the actual invocation and obtain a response.

4.2.4. Configure the request

The request can be configured using the RequestDefinition builder methods as described below.

Request URI

The request URI can be composed using:

  • A request target, i.e. the base URI of the request. If a default request target was configured for the RestClient instance, it will be overriden by the specific request target.

  • One ore more request *path*s, which will be appended to the base request target URI, adding slash characters to separate them from one another, if necessary.

RestClient client = RestClient.create();

RequestDefinition request = client.request().target(URI.create("https://rest.api.example")); (1)
request = request.path("apimethod"); (2)
request = request.path("subpath"); (3)
1 Set the request target, i.e. the base request URI
2 Set the request path, which will be appended to the base request URI
3 Append one more path to the request URI. The actual URI will be: https://rest.api.example/apimethod/subpath
URI template variable substitution values

The RestClient supports URI template variables substitution through the resolve(…​) method.

IMPORTART: URI templates variables substitution is only supported for the request URI components specified as path(…​) elements, not for the target(…​) base URI part.

client.request().target("https://rest.api.example").path("/data/{name}/{id}").resolve("name", "test")
    .resolve("id", 123); (1)

Map<String, Object> templates = new HashMap<>(1);
templates.put("id", "testValue");
request = client.request().target("https://rest.api.example").path("/test/{id}").resolve(templates); (2)
1 Subsitute two template variables values
2 Subsitute template variables values using a name-value map
URI query parameters

The RestClient supports URI query parameters specification, single or multi value, through the queryParameter(…​) methods.

client.request().queryParameter("parameter", "value").queryParameter("multiValueParameter", 1, 2, 3);
Request headers

HTTP headers can be added to the request using the generic header(String name, String…​ values) method (supporting single or multiple header values) or a set of frequently used headers convenience setter methods, such as accept, acceptLanguage (supporting Java Locale as arguments) and cacheControl.

The HttpHeaders interface can be used to refer to HTTP header names as constants.
The MediaType enumeration can be used for the Accept header value using the accept header setter method.
The CacheControl interface provides a fluent builder to build a Cache-Control header value and setting it for the request using the cacheControl header setter method.
client.request().header("Accept", "text/plain"); // Set an header providing name and value
client.request().header(HttpHeaders.ACCEPT, "text/plain"); // Set an header providing name using HttpHeaders and
                              // value
client.request().accept("text/plain", "text/xml"); // Set an Accept header with two values
client.request().accept(MediaType.APPLICATION_JSON); // Set an Accept header using convenience method and
                            // MediaType enumeration
client.request().acceptEncoding("gzip"); // Set an Accept-Encoding header
client.request().acceptCharset("utf-8"); // Set an Accept-Charset header
client.request().acceptCharset(Charset.forName("utf-8")); // Set an Accept-Charset header
client.request().acceptLanguage("en-CA"); // Set an Accept-Language header
client.request().acceptLanguage(Locale.US, Locale.GERMANY); // Set an Accept-Language header using Locales
client.request().cacheControl(CacheControl.builder().noCache(true).noStore(true).build()); // Set a
                                              // Cache-Control
                                              // header
Authorization headers

The RestClient provides two convenience request builder methods to set a request Authorization header using:

  • The Basic authorization scheme, providing a username and a password, using the authorizationBasic method.

  • The Bearer authorization scheme, providing a token, , using the authorizationBearer method.

client.request().authorizationBasic("username", "password"); // set an Authorization header using a Basic scheme
client.request().authorizationBearer("An389fz56xsr7"); // set an Authorization header using a Bearer scheme

4.2.5. Invoke the request and obtain a response

The RequestDefinition interface extends Invocation, which can be used to perform the actual invocation and obtain a response.

The Invocation interface provides a generic invocation method:

<T, R> ResponseEntity<T> invoke(HttpMethod method, RequestEntity<R> requestEntity, ResponseType<T> responseType)

This method acts as follows:

  • Accept the HttpMethod to use to perform the request

  • Accept a RequestEntity to provide an optional request message payload

  • Accept a ResponseType to declare wich type of response payload is expected, if any

  • It returns a ResponseEntity object, which represents the response message entity, including the HTTP status code and an optional reponse payload, which can be unmarshalled as a Java object

For non textual request or response payload types, any marshalling/unmarshalling strategy and implementation must be provided by the concrete RestClient. See the specific RestClient implementation documentation for additional informations.

4.2.6. Request invocation methods

In most cases, it is easier and faster to use other methods made available by the interface. Each method is relative to a specific request method and it is named accordingly. More than one method version is provided for each request method, providing the most suitable parameters and response types for for the most common situations.

For each HTTP request method (apart from the HEAD method), the RestClient interface makes available a set of invocation methods organized as follows:

  • A set of methods to optionally provide a request entity and to obtain a ResponseEntity (which provides the HTTP status code, the response header and the response payload). If the response is expected to contain an entity payload which has to be deserialized into a Java object, the response type can be specified, either as a simple or parametrized Java class.

Examples using the POST method
ResponseEntity<Void> response1 = RestClient.forTarget("https://rest.api.example/testpost").request()
    .post(RequestEntity.json(new TestData()));

ResponseEntity<TestData> response2 = RestClient.forTarget("https://rest.api.example/testpost").request()
    .post(RequestEntity.json(new TestData()), TestData.class);

ResponseEntity<List<TestData>> response3 = RestClient.forTarget("https://rest.api.example/testpost").request()
    .post(RequestEntity.json(new TestData()), ResponseType.of(TestData.class, List.class));
  • A set of method to directly obtain the deserialized response entity, named with the ForEntity suffix. This methods expects a successful response (i.e. a response with a 2xx HTTP status code), otherwise throwing a UnsuccessfulResponseException, which can be inspected to obtain the response status code and the response itself. This kind of methods returns an Optional, which will be empty for empty responses.

Examples using the GET method
try {

  Optional<TestData> data = RestClient.forTarget("https://rest.api.example/testget").request()
      .getForEntity(TestData.class);

  final ResponseType<List<TestData>> responseType = ResponseType.of(TestData.class, List.class);
  List<TestData> dataList = RestClient.forTarget("https://rest.api.example/testgetlist").request()
      .getForEntity(responseType).orElse(Collections.emptyList());

} catch (UnsuccessfulResponseException e) {
  // got a response with a status code different from 2xx
  int httpStatusCode = e.getStatusCode();
  e.getStatus().ifPresent(status -> System.err.println(status.getDescription()));
  ResponseEntity<?> theResponse = e.getResponse();
}

A set of convenience methods are provided for frequent needs and situations, for example:

  • A getForStream method to obtain a response InputStream.

  • A getAsList method, specifying the list elements type, to obtain a response entity content as a List of deserialized Java objects.

  • A postForLocation to post a request entity and obtain the Location response header value as a URI.

4.2.7. Request entity

The RequestEntity interface can be used to provide a request entity to the RestClient invocation methods, i.e. a request message payload as a Java object and a media type to set as request Content-Type header.

Depending on the RestClient implementation used, you must ensure the request media type is supported and suitable message body converters are available to deal with the Java object type and the media type of the request entity.

The RequestEntity interface provides a set of convenience static methods to build a request entity using the most common media types, such a text/plain, application/json, application/xml and application/x-www-form-urlencoded (also providing a fluent form data builder method).

RequestEntity<String> request1 = RequestEntity.text("test"); (1)

RequestEntity<TestData> request2 = RequestEntity.json(new TestData()); (2)

RequestEntity request3 = RequestEntity
    .form(RequestEntity.formBuilder().set("value1", "one").set("value2", "a", "b").build()); (3)
1 Build a text/plain type request entity
2 Build a application/json type request entity
3 Build a application/x-www-form-urlencoded type request entity, using the formBuilder method to build form data map

4.2.8. Response type

The ResponseType interface can be used to provide the expected response entity type to the RestClient invocation methods.

In addition to the simple Java class type, a parametrized type can be declared to use Java generic types. For example, to declare a List<TestData> response type the following code can be used:

ResponseType<List<TestData>> responseType = ResponseType.of(TestData.class, List.class);

4.2.9. Response entity

The ResponseEntity interface is used by RestClient to represent the response entity obtained as invocation result.

The ResponseEntity interface provides the HTTP status code, the response headers (with a set of convenience methods to inspect common HTTP headers) and a method to obtain the response entity (the message payload) as a Java object of the type which was expected as invocation result.

The RequestEntity.EMPTY constant cna be used to provide and empty request entity.

Depending on the RestClient implementation used, you must ensure the response media type is supported and suitable message body converters are available to deal with the Java object type and the media type of the response entity.

The ResponseEntity interface, in addition to the getPayload() method to obtain the Java entity object of the expected response entity type, provides methods (as(..)) to unmarshall the response body in an arbitrary java type, if supported by the underlying RestClient implementation.

ResponseEntity<TestData> response = RestClient.forTarget("https://rest.api.example/testget").request()
    .accept(MediaType.APPLICATION_JSON).get(TestData.class); (1)

HttpStatus status = response.getStatus(); (2)

Optional<TestData> entity = response.getPayload(); (3)

Optional<String> asString = response.as(String.class); (4)

String header = response.getHeaderValue(HttpHeaders.LAST_MODIFIED).orElse(null); (5)

long contentLength = response.getContentLength().orElse(-1L); (6)
1 Perform a GET request, setting the Accept header as application/json and declaring the TestData class as expected response entity Java type
2 Get the response status
3 Get the response entity payload as a Java object of the expected TestData type
4 Get the response entity payload as a String
5 Get the response Last-Modified header value
6 Get the response Content-Length header value as a long

4.2.10. Property and PropertyBox support

The REST client Invocation interface provides methods to handle request and response messages which involves PropertyBox objects as payload.

Tipically, a PropertyBox instance is marshsalled/unmarshsalled using the JSON format. The PropertyBox JSON serialization and deserialization support is provided by the holon-json.html[Holon platform JSON module], both for JAX-RS (Jersey and RestEasy) and Spring RestTemplate. The PropertyBox JSON support is automatically setted up when the Holon platform JSON module is available in classpath.

The property set which is involved in the request-response cycle can be configured in request definition using the propertySet(…​) methods.

Invocation using Properties examples
static final PathProperty<Integer> CODE = create("code", int.class);
static final PathProperty<String> VALUE = create("value", String.class);

static final PropertySet<?> PROPERTIES = PropertySet.of(CODE, VALUE);

public void propertiesInvocation() {
  RestClient client = RestClient.create();

  PropertyBox box = client.request().target("https://rest.api.example").path("/apimethod").propertySet(PROPERTIES)
      .getForEntity(PropertyBox.class).orElse(null); (1)

  List<PropertyBox> boxes = client.request().target("https://rest.api.example").path("/apimethod")
      .propertySet(PROPERTIES).getAsList(PropertyBox.class); (2)

}
1 GET request for a PropertyBox response using PROPERTIES property set
2 GET request for a list of PropertyBox response type using PROPERTIES property set

5. Authentication and Authorization

The holon-auth artifact provides a complete and highly configurable Authentication and Authorization architecture, integrated with all platform modules. HTTP messages authentication is supported, and several APIs are provided to manage accounts and their credentials, perform authentication in a Realm and check for permissions.

Maven coordinates:

<groupId>com.holon-platform.core</groupId>
<artifactId>holon-auth</artifactId>
<version>5.0.0</version>

5.1. Realm

The Realm interface represents a security abstraction providing operations for the principals authentication and authorization (permission check against principal grants stored in the realm).

A Realm may be identified by name, which should be unique within the same application.

The Realm interface is a composition of more specific interfaces, each dealing with a security concern and/or providing hooks to interact with the authentication and authorization process:

  • Authenticator: It performs the principal authentication, relying on an AuthenticationToken to obtain the necessary information to validate the authentication request, it checks the provided credentials and applies a consistent strategy to perform actual authentication

  • MessageAuthenticator: It performs the principal authentication using a generic Message (for example, an HTTP request message) from which the authentication informations can be obtained

  • Authorizer: It performs permission check against available principal grants

  • AuthenticationNotifier: It allows AuthenticationListener registration to be notified when a successfull authentication happens

The Realm interface provides a convenience getCurrent() static method to obtain the current Realm instance made available as `Context`resource, using default ClassLoader. See Context section for further information about context resources.

5.1.1. Authenticator

The Realm itself does not implement any authentication model or strategy, but delegates the specific authentication strategy to one or more concrete Authenticator registered in the Realm, relying on the AuthenticationToken type in order to discern which concrete Authenticator has to be used to handle the authentication process.

For this reason, at Realm configuration time or during the application lifecycle, the available Authenticators must be registered in Realm to provide the authentication capabilities, using the addAuthenticator(Authenticator<T> authenticator) method.

Each Authenticator declares the AuthenticationToken type it is bound to through the getTokenType() method.

To check if an AuthenticationToken type is supported by a Realm (i.e. an Authenticator bound to the token type is registered in Realm), the supportsToken(Class<? extends AuthenticationToken> authenticationTokenType) method can be used.

The Realm authentication process is structured as follows:

  1. The caller invokes the Realm authenticate(AuthenticationToken authenticationToken) method, providing a concrete authentication token;

  2. The Realm checks if a suitable Authenticator, which can handle given AuthenticationToken type, is registered. If not, an UnsupportedTokenException is thrown;

  3. The authenticate(AuthenticationToken authenticationToken) method is called on the specific Authenticator, performing the concrete authentication operation.

If an authentication operation is not successful, an AuthenticationException type is thrown. The concrete type of the exception gives more detailed informations on what went wrong.

Table 3. Available AuthenticationExceptions
Class Meaning

InvalidCredentialsException

Provided credentials are not valid or does not match the stored credentials

ExpiredCredentialsException

Provided credentials are expired

UnexpectedCredentialsException

An unexpected internal error occurs during credentials match

DisabledAccountException

Account is disabled

LockedAccountException

Account is locked

UnknownAccountException

Unknown account

InvalidCredentialsException

Provided credentials are not valid or does not match the stored credentials

UnsupportedTokenException

Unsupported authentication token type

UnsupportedMessageException

Unsupported authentication message

UnexpectedAuthenticationException

Generic authentication process failure

AuthenticationToken

The AuthenticationToken interface represents an authentication request, and provides the following methods:

  • getPrincipal(): the principal this authentication token refers to, i.e. the account identity submitted during the authentication process. The return type is a generic Object, since each authentication model could provide the principal information in a different way;

  • getCredentials(): the credentials submitted during the authentication process that verifies the submitted principal account identity;

Each AuthenticationToken type provides specific values as principal and credentials information, which the Authenticator can interpred and handle to perform the actual authentication.

Builtin AuthenticationTokens

1. Account credentials token: The account credentials token represents generic account authentication information, where an account is identified by a String type id (similar to a username) and a String type secret (similar to a password).

This token returns the account id from the getPrincipal() method, and the account secret from the getCredentials() method.

An account credentials token can be created by using the static accountCredentials(…​) method of the AuthenticationToken interface:

AuthenticationToken token = AuthenticationToken.accountCredentials("username", "password");

2. Bearer token: The bearer token represents a String type information which identifies (or it is bound to) a principal and can be used to perform the authentication or grant the access to a resource, checking the token validity. This kind of token is used, for example, in OAuth or JWT authentication and authorization models.

This token returns null from the getPrincipal() method, and the bearer token from the getCredentials() method.

A bearer token can be created using the static bearer(…​) method of the AuthenticationToken interface:

AuthenticationToken token = AuthenticationToken.bearer("Agr564FYda78dsff8Trf7");
Custom authentication tokens

You can use any custom AuthenticationToken to provide authentication request informations. To handle a custom AuthenticationToken in a Realm, the corresponding Authenticator implementation must be provided and registered.

class MyAuthenticationToken implements AuthenticationToken { (1)

  private final String principalName;

  public MyAuthenticationToken(String principalName) {
    super();
    this.principalName = principalName;
  }

  @Override
  public Object getPrincipal() {
    return principalName;
  }

  @Override
  public Object getCredentials() {
    return null;
  }

}

class MyAuthenticator implements Authenticator<MyAuthenticationToken> { (2)

  @Override
  public Class<? extends MyAuthenticationToken> getTokenType() {
    return MyAuthenticationToken.class; (3)
  }

  @Override
  public Authentication authenticate(MyAuthenticationToken authenticationToken) throws AuthenticationException {
    if (!"test".equals(authenticationToken.getPrincipal())) { (4)
      throw new UnknownAccountException();
    }
    return Authentication.builder(authenticationToken.principalName).build();
  }

}

public void authenticate() {
  Realm realm = Realm.builder().authenticator(new MyAuthenticator()).build(); (5)

  try {
    Authentication authc = realm.authenticate(new MyAuthenticationToken("test")); (6)
  } catch (AuthenticationException e) {
    // handle failed authentication
  }
}
1 Custom AuthenticationToken implementation
2 Custom Authenticator bound to the MyAuthenticationToken token type
3 The token type which the authenticator declares to support
4 Actual authentication: if something goes wrong, an exception which extends AuthenticationException must be thrown. Otherwise, return the Authentication object which represents the authenticated principal
5 Build a Realm and register the custom authenticator
6 Perform the authentication using a MyAuthenticationToken instance
Authentication

The result of the authentication operation performed using the authenticate(AuthenticationToken authenticationToken) method of an Authenticator is represented by the Authentication interface.

An Authentication object represents the authenticated principal, and extends the default java.security.Principal interface, inheriting the getName() method to obtain the name which identifies the principal.

In addition, the Authentication interface holds and provides the following informations:

  • The, optional, set of Permission granted to the authenticated principal

  • A isRoot() flag, to mark the authenticated principal as root principal, i.e. for which the permission checking is always skipped, assuming that any permission is granted to this principal

  • The optional scheme information the principal was authenticated with

  • It extends ParameterSet, representing a set of custom name-value parameters which can be used to provide additional, custom information related to the authenticated principal

An Authentication can be extended to provide more application-specific informations about the authenticated principal, if the parameter set support is not enough or too much generic.

Authentication build example
Authentication authc = Authentication.builder("userId").permission("VIEW").permission("MANAGE")
    .parameter("name", "John").parameter("surname", "Doe").build();

5.1.2. MessageAuthenticator

The MessageAuthenticator interface represents an intermediate authenticator, specialized for Message based authentication.

The authentication request is provided using a Message, for example a HTTP request message. Such message is translated into an AuthenticationToken using an AuthenticationTokenResolver registered in the MessageAuthenticator. From now on, the authentication process proceed as usual, using the obtained AuthenticationToken and a suitable Authenticator to accomplish the actual authentication operation in Realm.

Unlike the standard Authenticator, the MessageAuthenticator provides a specialized method which accepts a Message as authentication request representation:

authenticate(Message<?, ?> message, String... schemes)
AuthenticationTokenResolver

The AuthenticationTokenResolver is responsible for the authentication Message handling, and must provide a standard AuthenticationToken which represents the authentication message to be used to perform actual authentication in Realm.

An AuthenticationTokenResolver can optionally declare the authentication scheme it is bound to, in order to discern different messages authentication information within a set of messages of the same type, and select the most suitable AuthenticationTokenResolver relying on the authentication scheme name.

For example, within the set of the HTTP request messages type, different authentication schemes, such as Basic or Bearer, can be bound to a different AuthenticationTokenResolver, producing different AuthenticationToken s.

For HttpRequest message types, two builtin AuthenticationTokenResolver are provided:

AuthenticationTokenResolver<HttpRequest> basicResolver = AuthenticationToken.httpBasicResolver();
AuthenticationTokenResolver<HttpRequest> bearerResolver = AuthenticationToken.httpBearerResolver();

An AuthenticationTokenResolver must be registered in Realm using the addAuthenticationTokenResolver(…​) method.

AuthenticationTokenResolver registration and use
Realm realm = Realm.builder().resolver(AuthenticationToken.httpBasicResolver())
    .authenticator(new MyAuthenticator()).build(); (1)

HttpRequest request = null; // obtain the HttpRequest message in real world code
try {
  Authentication authc = realm.authenticate(request); (2)
} catch (AuthenticationException e) {
  // handle failed authentication
}
1 Register a HTTP Basic message resolver in Realm
2 Perform authentication using a HTTPRequest mesage

5.1.3. Authorizer

The Authorizer interface is responsible for authorization control, checking if one or more permissions are granted to a principal.

An Authorizer uses the Authentication representation to obtain the permissions granted to an authenticated principal, and provides several isPermitted…​ methods to perform permission control.

Permission

A permission is the representation of the access to a resource and can be expressed int two ways:

  • As a String: this can be compared to a Role name.

  • Using the Permission interface. This interface can be extended and implemented in different ways to represent more complex permissions than a simple role name. The Authorizer relies on the equals() and hashCode() implementation to perform Permission comparison.

A Permission object which uses a simple String name can be created using the Permission.create(String permission) static method.
Authorization checking

Each Authorizer can be bound to a specific Permission type, to create specialized authorizers which handle a specific permission type.

To enable authorization checking for a Realm, the available authorizers must be registered in realm using the addAuthorizer(…​) method.

The platform provides a default Authorizer which can be obtained using the Authorizer.create() static method. The default authorizer is bound to a generic Permission type and permission checking is performed by comparing the`Authentication` granted permissions with the permissions to be checked, using the Permission.equals(…​) method to compare a single permission to another.

The default Authorizer supports the Authentication.isRoot() state, always granting permissions to root authentications.

Authorization example
final Permission p1 = Permission.create("role1");
final Permission p2 = Permission.create("role2");

// build an Authentication and grant the two permissions to it
Authentication authc = Authentication.builder("test").permission(p1).permission(p2).build();

// Realm with default authorizer
Realm realm = Realm.builder().withDefaultAuthorizer().build();

// permission checking
boolean permitted = realm.isPermitted(authc, p1);
permitted = realm.isPermitted(authc, p1, p2);
permitted = realm.isPermitted(authc, "p1");
permitted = realm.isPermittedAny(authc, p1, p2);
permitted = realm.isPermittedAny(authc, "p1", "p2");

5.1.4. AuthenticationListener support

The Realm interface allows the registration of listeners to be notified when a successfull authentication happens, using the addAuthenticationListener(AuthenticationListener authenticationListener) method.

The AuthenticationListener method onAuthentication(Authentication authentication) is invoked when a principal is successfully authenticated, providing the Authentication representation object.

5.2. Credentials

The platform provides a support for authentication credentials management, relying on the following structures:

  • The Credentials interface to represent credentials data, i.e. the secret and the encoding informations related to it

  • The CredentialsContainer interface identifies an object which provides credentials data (for example, the stored account informations related to a principal and the credentials provided by a principal during and authentication process)

  • The CredentialsMatcher deals with credentials checking, and it is able to determine if two credentials data structures match (for example, if provided credentials match the account stored credentials)

5.2.1. Create and encode Credentials

The Credentials interface provides a builder to create and encode a Credentials representation.

The Credentials builder provides method to encode a secret by using a hashing algorithm (specifying also a salt and the hash iterations to be performed), specifing an optional expiry date and encode the secret using, for example, Base64.

Credentials build examples
Credentials credentials = Credentials.builder().secret("test").build(); (1)

credentials = Credentials.builder().secret("test").hashAlgorithm(Credentials.Encoder.HASH_MD5).build(); (2)

credentials = Credentials.builder().secret("test").hashAlgorithm(Credentials.Encoder.HASH_MD5).hashIterations(7)
    .salt(new byte[] { 1, 2, 3 }).build(); (3)

credentials = Credentials.builder().secret("test").hashAlgorithm(Credentials.Encoder.HASH_MD5).base64Encoded()
    .build(); (4)

credentials = Credentials.builder().secret("test").expireDate(new Date()).build(); (5)
1 Simple credentials using test as secret and no encodings
2 Credentials using test as secret and MD5 as hashing algorithm
3 Credentials using test as secret and MD5 as hashing algorithm, with a salt and 7 hashing iterations
4 Credentials using test as secret and MD5 as hashing algorithm, encoded using Base64
5 Simple credentials using test as secret and no encodings, specifying an expiry date

5.2.2. Credentials encoder

To encode credentials data, for example for storing purposes, the Credentials interface provides an Encoder interface, which can be obtained using the encoder() static method.

Credentials encoding examples
String encoded = Credentials.encoder().secret("test").buildAndEncodeBase64(); (1)

byte[] bytes = Credentials.encoder().secret("test").hashSHA256().build(); (2)

encoded = Credentials.encoder().secret("test").hashSHA256().salt(new byte[] { 1, 2, 3 }).buildAndEncodeBase64(); (3)

encoded = Credentials.encoder().secret("test").hashSHA512().charset("UTF-8").buildAndEncodeBase64(); (4)
1 Credentials using test as secret and Base64 encoded
2 Credentials using test as secret and SHA-256 as hashing algorithm, returned as bytes
3 Credentials using test as secret and SHA-256 as hashing algorithm, with a salt and Base64 encoded
4 Credentials using test as secret and SHA-512 as hashing algorithm, encoded using Base64 with the UTF-8 charset

5.2.3. Credentials matching

Credentials matching can be performed using a CredentialsMatcher.

The platform provides a default CredentialsMatcher implementation which can be obtained using the defaultMatcher() method on the CredentialsContainer interface.

The default credentials matcher tries to employ best-practices and common behaviours to perform credentials validation and matching:

  • Try to convert generic Object credentials data into a byte array:

    • It supports char[], String, File and InputStream for direct bytes conversion

    • It supports Credentials type, using the getSecret() method to obtain the bytes

  • If the provided credentials data are of Credentials type and an expiry date is provided, it checks the credentials are not expired

  • It check if the array of bytes obtained from the two credentials data structures match, hashing and/or decoding the credentials data if these informations are available (i.e. the stored credentials are of Credentials type)

5.3. Account

The platform provides an abstraction of an Account structure, which represents information about a principal.

The Account interface is used to represent a generic account, providing the following information:

  • The account id (as a String)

  • The account credentials (as a generic Object)

  • Whether the account is a root account, i.e. has any permission

  • Optional map of generic account details, identified by a String id

  • Optional set of permissions granted to the account

  • Whether the account is enabled

  • Whether the account is locked

  • Whether the account is expired

A builder is available to create Account instances:

Account.builder("accountId").enabled(true).locked(false).expired(false)
    .credentials(Credentials.builder().secret("pwd").hashAlgorithm(Credentials.Encoder.HASH_SHA_256)
        .base64Encoded().build()) // credentials
    .root(false) // not root (default)
    .permission("role1").permission("role2") // permissions
    .detail("name", "TheName") // principal name
    .detail("surname", "TheSurname") // principal surname
    .build();

5.3.1. AccountProvider

The AccountProvider interface can be used to provide Account instances using the account id, for example from a data store, by implementing the loadAccountById(String id) method.

The loading method returns an Optional, if empty means that an account with given id is not available.

5.3.2. Authenticator

A default Authenticator is provided to perform the account authentication by using an AccountProvider to load account data. The account authenticator is obtained by using the authenticator(…​) static method of the Account interface, choosing to use a custom credentials matcher or the default one.

The supported AuthenticationToken is the account credentials authentication token, obtained using the static accountCredentials(…​) method of the AuthenticationToken interface.

AccountProvider provider = id -> Optional.of(Account.builder(id).enabled(true)
    .credentials(Credentials.builder().secret("pwd").base64Encoded().build()).permission("role1").build()); (1)

Realm realm = Realm.builder().authenticator(Account.authenticator(provider)).withDefaultAuthorizer().build(); (2)

try {
  Authentication authc = realm.authenticate(AuthenticationToken.accountCredentials("test", "pwd")); (3)
} catch (AuthenticationException e) {
  // handle authentication failures
}
1 Create an AccountProvider to provide the Account instances according to the account id
2 Create a Realm with default authorizer and register an account Authenticator which uses the previously defined AccountProvider
3 Perform authentication using an account credentials authentication token

5.4. AuthContext

The AuthContext interface can be used to represent the current authentication/authorization context, i.e. acts as a holder of the current Authentication, if available, and provides methods to perform the actual authentication operations, relying on a specific Realm istance.

For its nature, an AuthContext is tipically bound to an application session or to the current request context for a backend services layer.

It provides methods to:

  • Check if a principal is currently authenticated in context and if so obtain the corresponding Authentication

  • Perform authentication on the reference Realm, either using an AuthenticationToken or a Message

  • De-authenticate the context, removing current Authentication

  • Perform authorization control on current Authentication, using the reference Realm authorizers

See the Realm section for detailed information about authenticators and authorizers.

When an AuthContext is de-authenticated, any Realm registered AuthenticationListener is notified, invoking onAuthentication(Authentication authentication) listeners method with a null authentication parameter value.
Authorization example
AccountProvider provider = id -> Optional.of(Account.builder(id).enabled(true)
    .credentials(Credentials.builder().secret("pwd").base64Encoded().build()).permission("role1").build()); (1)

Realm realm = Realm.builder().authenticator(Account.authenticator(provider)).withDefaultAuthorizer().build(); (2)

AuthContext context = AuthContext.create(realm); (3)

context.addAuthenticationListener(
    a -> System.out.println((a != null) ? "Authenticated: " + a.getName() : "Unauthenticated")); (4)

try {

  context.authenticate(AuthenticationToken.accountCredentials("test", "pwd")); (5)

  Authentication authc = context.getAuthentication()
      .orElseThrow(() -> new IllegalStateException("Context not authenticated")); (6)

  boolean permitted = context.isPermitted("role1"); (7)

  context.unauthenticate(); (8)

} catch (AuthenticationException e) {
  // handle authentication failures
}
1 Create an AccountProvider to provide the Account instances according to the account id
2 Create a Realm with default authorizer and register an account Authenticator which uses the previously defined AccountProvider
3 Create an AuthContext backed by the created realm
4 Add an AuthenticationListener to the context
5 Perform authentication by using an account credentials authentication token
6 Get the current context Authentication
7 Check if the current context Authentication has the permission named role1
8 Unauthenticate the context, i.e. remove the current Authentication
The AuthContext interface provides a convenience getCurrent() static method allowing to obtain the current AuthContext instance which is made available as `Context`resource, using the default ClassLoader. See Context section for further information about context resources.

5.5. @Authenticate annotation

The Authenticate can be used on classes or methods to require authentication for resource access.

The support for this annotation must be documented and it is available for other modules of the Holon platform.

The annotation supports optional schemes specification to provide the allowed authentication schemes to be used to perform principal authentication, and an optional redirectURI which can be used to redirect user interaction when the authentication succeeds or fails (the semantic and behaviour associated to the redirect URI is specific for every authentication delegate).

5.6. JWT support

5.6.1. Introduction

The holon-auth-jwt artifact provides the support for the JSON Web Token standard, integrating it in the Holon platform authentication and authorization architecture.

The jjwt library is used for JWT tokens parsing and building.

JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. The transmitted information can be digitally signed, in order to be verified and trusted by the parties.

When used for authentication, thanks to its very compact data representation and encoding, a JWT token can transport and provide not only the informations to perform authentication, but also the information obtained as a result of an authentication operation, such as principal's details and permissions.

Maven coordinates:

<groupId>com.holon-platform.core</groupId>
<artifactId>holon-auth-jwt</artifactId>
<version>5.0.0</version>

5.6.2. Configuration

To enable and use JWT authentication structures, some configuration parameters must be defined and provided to the objects which perform concrete operations on the JWT tokens.

The JWT configuration properties are represented by the JwtConfiguration interface, which can be instantiated as follows:

1. Using the builder provided by the JwtConfiguration interface:

JwtConfiguration cfg = JwtConfiguration.builder().issuer("MyIssuer") // set the JWT token issuer
    .expireTime(10000) // token expire time in milliseconds
    .includeDetails(true) // include the Authentication details in JWT token generation
    .includePermissions(true) // include the Authentication permissions in JWT token generation
    .signatureAlgorithm("HS256") // use HS256 as signature algorithm
    .sharedKey(new byte[] { 1, 2, 3 }) // shared key to use with the symmetric signing algorithm
    .build();

2. Using the configuration properties collected and represented by the JwtConfigProperties property set, extending a default ConfigPropertySet and bound to the property name prefix holon.jwt.

The available configuration properties are listed here below:

Table 4. JWT configuration properties
Name Type Meaning

holon.jwt. issuer

String

The JWT token issuer

holon.jwt. signature-algorithm

String

JWT signature algorithm name

holon.jwt. sharedkey-base64

String

JWT sign shared key (base64 encoded) for symmetric signing algorithms

holon.jwt. publickey-base64

String

JWT sign public key (base64 encoded) for RSA signing algorithms

holon.jwt. publickey-file

String

JWT sign public key (file name) for RSA signing algorithms

holon.jwt. privatekey-base64

String

JWT sign private key (base64 encoded) for RSA signing algorithms

holon.jwt. privatekey-file

String

JWT sign private key (file name) for RSA signing algorithms

holon.jwt. expire-ms

Integer number

JWT token expire time in milliseconds

holon.jwt. expire-seconds

Integer number

JWT token expire time in seconds

holon.jwt. expire-minutes

Integer number

JWT token expire time in minutes

holon.jwt. expire-hours

Integer number

JWT token expire time in hours

holon.jwt. expire-days

Integer number

JWT token expire time in days

holon.jwt. include-details

Booelan (true/false)

Whether to include Authentication details in JWT token as claims

holon.jwt. include-permissions

Booelan (true/false)

Whether to include Authentication permissions in JWT token as claims

JwtConfiguration example using configuration properties
JwtConfiguration cfg = JwtConfiguration
    .build(JwtConfigProperties.builder().withPropertySource("jwt.properties").build());

5.6.3. Building JWT tokens for an Authentication

To build JWT token the JwtTokenBuilder class is available, providing static methods to create a JWT token from an Authentication object, optionally using a JwtConfiguration class to provide token configuration attributes.

If enabled, the permissions and parameters of the Authentication will be written in the JWT token with the following behaviour:

  • The Authentication permissions will be written only for the permissions which provides a String representation through the Permission.getPermission() method. The serializable permissions are written as String array and associated to the claim name which corresponds to the constant AuthenticationClaims.CLAIM_NAME_PERMISSIONS value;

  • Each Authentication parameter will be written by using the parameter name as claim name and the parameter value as claim value.

JWT token building example using JwtConfiguration
JwtConfiguration configuration = JwtConfiguration
    .build(JwtConfigProperties.builder().withPropertySource("jwt.properties").build()); (1)

Authentication authc = Authentication.builder("test").permission("role1").parameter("name", "TestName").build(); (2)

String jwtToken = JwtTokenBuilder.buildJwtToken(configuration, authc, UUID.randomUUID().toString()); (3)
1 Build a JwtConfiguration instance using the jwt.properties file
2 Build an example Authentication
3 Build a JWT token by using given configuration and authentication, using a random id as token id

5.6.4. Performing authentication using JWT tokens

The JwtAuthenticator interface is provided as Authenticator to handle JWT token based authentication, using a JwtConfiguration to obtain JWT token configuration attributes.

The authentication request information are provided by using a BearerAuthenticationToken, where the Bearer value represents the complete JWT token serialization.

From an HTTPMessage point of view, the authentication token is obtained from an Authorization header using the Bearer scheme.

To obtain a JwtAuthenticator, the provided builder can be used. The authenticator, just like any other Authenticator, must be registered in a Realm to enable JWT token based authentication.

The JwtAuthenticator supports:

  • An optional set of allowed JWT issuers: If one ore more allowed issuer is setted, JWT Issuer claim (iss) will be required and checked during token authentication: if token issuer doesn’t match one of the given issuers, authentication will fail;

  • Optional required claims: If one or more required claim is configured, the specified claim must exist in JWT token, otherwise authentication will fail.

JWT token building example using JwtConfiguration
JwtConfiguration configuration = JwtConfiguration.builder().issuer("MyIssuer") // set the JWT token issuer
    .expireTime(10000) // token expire time in milliseconds
    .includeDetails(true) // include the Authentication details in JWT token generation
    .includePermissions(true) // include the Authentication permissions in JWT token generation
    .signatureAlgorithm("HS256") // use HS256 as signature algorithm
    .sharedKey(new byte[] { 1, 2, 3 }) // shared key to use with the symmetric signing algorithm
    .build();

// Build the Jwt authenticator using the JwtConfiguration
JwtAuthenticator jwtAuthenticator = JwtAuthenticator.builder().configuration(configuration)
    .issuer("allowedIssuer").requiredClaim("myClaim").build();

// Build a Realm and register the authenticator
Realm realm = Realm.builder().authenticator(jwtAuthenticator).withDefaultAuthorizer().build();

try {

  // Authentication using a bearer token
  Authentication authc = realm.authenticate(AuthenticationToken.bearer("TheJWTtokenHere..."));

  // Authentication using a message
  HttpRequest request = null; // expected an HttpRequest message with an 'Authorization' header with 'Bearer:
                // JWTtokenValue'
  authc = realm.authenticate(request);

} catch (AuthenticationException e) {
  // handle authentication failures
}

6. Spring framework integration

The holon-spring artifact provides integration with the Spring framework and auto-configuration features using Spring Boot for the Holon platform core module.

Maven coordinates:

<groupId>com.holon-platform.core</groupId>
<artifactId>holon-spring</artifactId>
<version>5.0.0</version>

6.1. Spring beans as context resources

The EnableBeanContext annotation can be used on Spring Configuration classes to configure a Context scope using the Spring ApplicationContext to provide context resources instances as Spring beans.

The scope registration priority order is an intermediate value between the default thread scope (highest priority) and classloader scope (lowest priority).

If the scope is registered, when a context resource is requested, using for example the Context.resource(String resourceKey, Class<T> resourceType) method, the Spring scope checks if a bean matches the requested resource key and type using the following strategy:

  • If a Spring bean with a name equal to the requested resource key and with the same required type is found, this is returned;

  • Otherwise, if the lookupByType() attribute value of the @EnableBeanContext annotation is true and a Spring bean of the required type, ignoring the its name, is present and only one candidate is available, this instance is returned.

The Spring scopes are respected, in the sense that when a resource is requested and a matching bean is found, the bean instance lookup is performed using standard Spring ApplicationContext methods, involving any registered and active Spring scope.

Spring context scope example
class TestResource {
}

@Configuration
@EnableBeanContext
class SpringConfig {

  @Bean(name = "testResource")
  public TestResource testResource() {
    return new TestResource();
  }

}

public void getContextResource() {
  // lookup using resurce name which matches the bean name
  Optional<TestResource> resource = Context.get().resource("testResource", TestResource.class);
  // lookup by type
  resource = Context.get().resource(TestResource.class);
}

6.2. EnvironmentConfigPropertyProvider

The EnvironmentConfigPropertyProvider is provided to build a ConfigPropertyProvider which uses the Spring Environment structure as property source.

EnvironmentConfigPropertyProvider example
@Autowired
Environment environment;

public void env() {
  // build a ConfigPropertyProvider using Spring Environment as property source
  ConfigPropertyProvider provider = EnvironmentConfigPropertyProvider.create(environment);

  String value = provider.getProperty("test.property.name", String.class);
}

6.3. Spring tenant scope

The Holon platform provides a Spring tenant scope which provides different bean instances depending on the current tenant id.

This scope relies on the default TenantResolver interface to obtain the current tenant id.

A bean of TenantResolver type must be configured and available in the current BeanFactory (i.e. in the current ApplicationContext) to use the scope. That bean will be used to obtain the current tenant id, using the getTenantId() method.

To enable the tenant scope, use the @EnableTenantScope annotation on Spring configuration classes.

The scope name is tenant, so Spring beans can be registered with this scope using either:

  • The default Spring @Scope("tenant") annotation

  • The convenience @ScopeTenant annotation

Spring tenant scope example
@Configuration
@EnableTenantScope
class TenantScopeConfig {

  @Bean
  public TenantResolver tenantResolver() {
    return () -> Optional.of("test"); // provide a meaningful current tenant id resolution strategy...
  }

  @Bean
  @ScopeTenant
  public TestResource testResource() {
    // a different instance of the bean will be provided for each tenant id
    return new TestResource();
  }

}

6.4. Datastore configuration

The Spring integration module provides a number of methods to extend and configure a Datastore using Spring beans when the Datastore is registered as a bean in the Spring context.

6.4.1. DatastoreResolver

The DatastoreResolver annotation can be used to annotate ExpressionResolver type beans to automatically register them into a Datastore.

The datastoreBeanName() annotation attribute can be used to uniquely identify the Datastore bean into which register the resolver, if more than one Datastore bean is present in Spring context.

6.4.2. DatastoreCommodityFactory

The DatastoreCommodityFactory annotation can be used to annotate DatastoreCommodityFactory type beans to automatically register them into a Datastore.

The datastoreBeanName() annotation attribute can be used to uniquely identify the Datastore bean into which register the factory, if more than one Datastore bean is present in Spring context.

Each concrete Datastore implementation could provide a specific DatastoreCommodityFactory base type to be used to register commodity factories in order to provide a specific DatastoreCommodityContext. See specific Datastore documentation for further information.

6.4.3. DatastorePostProcessor

The DatastorePostProcessor interface can be used to configure a Datastore, if registered as a Spring bean, right after it is initialized in the Spring context.

A Spring bean class implementing this interface is automatically detected and the method postProcessDatastore(Datastore, String) is called at Datastore bean initialization time. For example, the post processor can be used to register additional ExpressionResolver or DatastoreCommodityFactory.

The DatastorePostProcessor beans must be registered with a singleton scope in context.

6.5. RestClient implementation using Spring RestTemplate

The Spring integration module provides a RestClient implementation using Spring RestTemplate.

The Spring RestClient is represented by the SpringRestClient interface, which provides a create(RestTemplate restTemplate) method to create a RestClient instance using provided Spring RestTemplate.

When a RestTemplate instance is available in platform Context (for example if is available as Spring bean and the Spring application context support as a context scope is enabled - see Spring beans as context resources), a RestClientFactory is automatically registered to provide a SpringRestClient implementation using the RestTemplate.

@Autowired
RestTemplate restTemplate;

public void restclient() throws URISyntaxException {

  // Create a SpringRestClient
  RestClient client = SpringRestClient.create(restTemplate);

  // obtain the RestClient using default creation methods
  client = RestClient.create();

  client = RestClient.create(SpringRestClient.class.getName());
}

6.6. Spring Boot auto-configuration

The Holon platform provides Spring Boot auto-configuration features for the most of the platform modules, including the core module.

To enable Spring Boot auto-configuration the following artifact must be included in your project dependencies:

<groupId>com.holon-platform.core</groupId>
<artifactId>holon-spring-boot</artifactId>
<version>5.0.0</version>

Two auto-configuration features are provided:

1. Spring beans as context resources is enabled, registering the corresponding Context scope. This has the same effect as using the @EnableBeanContext annotation on Spring configuration classes, with the lookupByType() attribute set as true by default.

To disable this auto-configuration feature the EnableBeanContextAutoConfiguration class can be excluded:

@EnableAutoConfiguration(exclude={EnableBeanContextAutoConfiguration.class})

2. If a TenantResolver bean is registered in Spring ApplicationContext, the Spring tenant scope is automatically registered. This has the same effect as using the @EnableTenantScope annotation on Spring configuration classes.

To disable this auto-configuration feature the TenantScopeAutoConfiguration class can be excluded:

@EnableAutoConfiguration(exclude={TenantScopeAutoConfiguration.class})

6.6.1. Spring Boot starters

The following Spring Boot starter artifact is available to provide a quick project configuration setup using Maven dependency system:

Maven coordinates:

<groupId>com.holon-platform.core</groupId>
<artifactId>holon-starter</artifactId>
<version>5.0.0</version>

The starter provides the dependency to the holon-spring-boot artifact in addition to the one to the base Spring Boot starter (spring-boot-starter).

7. Loggers

By default, the Holon platform uses the SLF4J API for logging. The use of SLF4J is optional: it is enabled when the presence of SLF4J is detected in the classpath. Otherwise, logging will fall back to JUL (java.util.logging).

The following logger names are available:

  • com.holonplatform.core: the root core logger

    • presentation: for logs related to values presentation

    • i18n: for logs related to localization and internationalization

    • beans: for logs related to bean inspection and bean properties

    • property: for logs related to the Property architecture, including PropertyBox, property presenters and renderers

    • query: for logs related to Query definition and execution

    • datastore: for logs related to Datastore configuration and operation execution

  • com.holonplatform.jdbc: for logs related to JDBC support classes, such as DataSource builders

  • com.holonplatform.http: for logs related to HTTP support classes, such as RestClient

  • com.holonplatform.spring: for logs related to Spring integration

8. System requirements

8.1. Java

The Holon Platform core module requires Java 8 or higher.