Copyright © 2016-2018
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.
1.1. Sources and contributions
The Holon Platform Core module source code is available from the GitHub repository https://github.com/holon-platform/holon-core.
See the repository README
file for information about:
-
The source code structure.
-
How to build the module artifacts from sources.
-
Where to find the code examples.
-
How to contribute to the module development.
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.1.1</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.1.1</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.
See Obtain the platform artifacts for details.
3. What’s new in version 5.1.x
3.1. Property model
-
The
Property
instance identification strategy can now be customized using specific equals and hashCode handlers. See Property naming and identity. -
The
PathProperty
interface now provides a set of sub types, to consistently handle property expressions which are type specific, for the main Java types (String, Numbers, Temporal types and Boolean). See PathProperty sub types. -
The
PropertySet
interface now supports a generic configuration container, likewise theProperty
interface. See PropertySet configuration. -
The
PropertySet
interface now supports identifier properties declaration, which can be used to provide a virtual primary key to distinguish aPropertyBox
instance from another (using the identifier property values), both at Java objects level (equals
andhashCode
) and at persistence architecture level. See Identifier properties. -
Just like the
Property
instances, thePropertyBox
instances identification strategy can now be customized using specific equals and hashCode handlers. Furthermore, if thePropertyBox
property set declares one or more identifier property, their values will be used by default to implement thePropertyBox
instance identification strategy. See PropertyBox instances identification. -
The new BeanPropertySetPostProcessor interface can be used to extend the BeanIntrospector Bean introspection strategy at Bean property set level, allowing for example to customize the Bean property set configuration. See BeanPropertySetPostProcessor.
-
The @DataPath annotation can be used on Bean classes to declare a data path mapping name different form the Bean class or property name, when the Bean class is used in a persistence context and it is bound to a data model definition. See @DataPath.
3.2. Datastore
-
A deep revision of the
Expression
based architecture, which is the foundation, above all, of theDatastore
API and the abstractQuery
engine. This lead to a more consistent and extensible architecture, along with considerable performance improvements. See the Query and Datastore API extensions sections of theDatastore
documentation. -
New standard
QueryFunction
implementations was made available for the coreDatastore
API. Besides String related functions, a set of temporal data types related functions is now available to obtain current date/time (withjava.time.*
types support) and to extract a temporal part (year, month, day, hour). See Temporal functions. -
A new
Transactional
API which can be used when aDatastore
implementation supports transactions, which can be managed at a higher level, in an abstract and implementation-independent way. TheTransactional
API allows to executeDatastore
operations whithin a transaction, taking care of the transaction lifecycle and providing transaction reference to perform commit and rollback operations. See Transactional Datastores. -
New
DataMappable
API to represent data model mappings. See DataMappable.
3.3. JWT authentication
-
The JWT configuration now supports a wider range of key sources, formats (such as the
PKCS#12
key store format) and encodings (such as thePEM
encoding) for private and public key declarations when an asymmetric signing algorithm is used. See JWT configuration.
3.4. Spring ecosystem integration
-
The
TenantScopeManager
API is now available to manage the tenant scoped beans lifecycle. See Tenant scoped beans lifecycle. -
The new
holon-spring-security
artifact provides integration between the Holon Platform authentication and authorization architecture and the Spring Security one. Furthermore, aholon-starter-security
Spring Boot starter is now available for quick project setup. See Spring Security integration.
3.5. Migrating from version 5.0.x
3.5.1. Deprecations
-
PropertySet API: join. Using this method causes the loss of any property set configuration and/or identifier property declaration. Use the default PropertySet builder instead.
-
BeanPropertySet API: create providing a parent path. The bean properties parent path will always be the
FinalPath
which represents the Bean class, with the bean fully qualified class name as path name. Use the defaultcreate(Class<? extends T> beanClass)
method instead. The same considerations are valid for thegetPropertySet
method of the BeanIntrospector API. -
BeanPropertySet/BeanPropertyInspector API: requireProperty methods. The methods name are changed in
property
for consistency with the naming conventions of the newBeanPropertyInspector
API. -
SubQuery API: creation methods providing a Datastore. The
Datastore
parameter is no longer required. Use the newcreate(…)
method versions which does not require theDatastore
parameter. -
JwtConfigProperties API: private and public key source configuration properties. The private and public key sources can now be specified using the
publickey.source
property and using thefile:
andclasspath:
prefixes to declare the source type. -
JwtTokenBuilder API: static token creation method. The
JwtTokenBuilder
is now an interface. The default implementation can be obtained using theget()
static method.
3.5.2. Property model
With the introduction of the PathProperty sub types, a PathProperty
declaration should be made using the most suitable sub type, if available.
This is also necessary to use the convenience QueryFilter
and QueryFunction
static builder methods to create an expression using the property itself. The expression builder methods are now organized by sub type, so, for example, the contains
method is only available for String
type properties and the StringProperty
sub type should by used in this case.
final StringProperty STR = StringProperty.create("name"); (1)
QueryFilter filter = STR.contains("value"); (2)
1 | Create a StringProperty path property type with the name path name |
2 | The StringProperty type makes available convenience expression builder methods according to the String property type, for example contains |
4. Core API, services and components
4.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.1.1</version>
4.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
interface 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 |
4.2.1. Obtaining Context resources
The Context
API can be used to statically obtain a resource, using a resource key to identify the requested resource type.
To obtain a resource, the resource(…)
methods are provided:
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.
|
To organize and provide the resource references in the most versatile and extensible way, the Context
API is organized in scopes. A context scope represents a resources registry, available to the Context
API in order to perform the context resources lookup and provide a consistent resource reference.
Furthermore, the context scopes are the entry points for context resources registration.
See the next section for details.
4.2.2. Scopes
The Context
API is organized in scopes, represented by the ContextScope interface.
Each scope acts 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 priority number.
When a resource is requested from the Context
, the lookup process is the following:
-
Each registered scope is queried to obtain the resource instance through a specified resource key, starting from the scope with the highest priority.
-
The actually returned resource instance is the one obtained from the first scope which provides a resource instance bound to the requested resource key, if any.
A reference to a registered scope can be obtained from the Context
using:
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
.
Example of resource registration:
Context.get().scope("myscope") (1)
.ifPresent(scope -> scope.put("myResourceKey", new ResourceType())); (2)
1 | Get the scope named myscope , if available |
2 | Register a new resource instance using myResourceKey as resource key |
4.2.3. Default Scopes
The Holon platform provides two default context scopes, automatically registered and made available to the Context
API:
-
A ClassLoader-bound scope, which handles resources as singleton instances within the reference
ClassLoader
, that is, at most one resource instance for a given resource key is present in the scope for a specificClassLoader
. This scope is registered with a low precedence order. The scope name is provided by the constant CLASSLOADER_SCOPE_NAME; -
A Thread-bound scope, which binds resources to the current
Thread
usingThreadLocal
variables. This scope is registered with a high precedence order. The scope name is provided by the constant THREAD_SCOPE_NAME.
The Context
API provides methods to directly obtain the default scopes:
Optional<ContextScope> scope = Context.get().classLoaderScope(); (1)
scope = Context.get().classLoaderScope(aClassLoader); (2)
Optional<ContextScope> threadScope = Context.get().threadScope(); (3)
threadScope = Context.get().threadScope(aClassLoader); (4)
1 | Get the default ClassLoader scope using the default ClassLoader |
2 | Get the default ClassLoader scope using a specific ClassLoader |
3 | Get the default Thread scope using the default ClassLoader |
4 | Get the default Thread scope using a specific ClassLoader |
The Context
interface provides some other useful methods to access the special Thread-bound scope, to perform an operation ensuring that a context resource is bound to the Thread scope before the operation begins and removed from the Thread scope just after the operation ends:
Context.get().executeThreadBound("resourceKey", resourceInstance, () -> {
// do something (1)
});
Context.get().executeThreadBound("resourceKey", resourceInstance, () -> {
// do something (2)
return null;
});
1 | Execute a Runnable operation, binding the resource instance with given resourceKey to the current Thread before execution and removing the binding after the operation execution |
2 | Execute a Callable operation, binding the resource instance with given resourceKey to the current Thread before execution and removing the binding after the operation execution |
4.2.4. Context extension: adding Scopes
Additional ContextScope
implementations can be added to the platform Context
API by using standard Java service extensions.
To create and register a new context scope, the following steps are required:
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 required resourceType type, if the resource is currently available from the 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. When a jar with a valid com.holonplatform.core.ContextScope file is available from classpath, the context scope is automatically registered and made available from the Context
API.
Some Holon platform modules provides specific context scope implementations, which are automatically registered when the module is available form classpath. See each module reference documentation to learn about the available additional platform context scopes. |
The core module itself provides a Spring bean factory based context scope, which uses the Spring framework bean registry to provide bean references as context resources. See the Spring ecosystem integration section for further information and the Spring context scope section for details. |
4.3. Configuration and parameters
Holon platform relies on some common structures to define and provide configuration properties and parameters used by platform modules.
Configuration properties definition, provision and management is supported by the following 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.
4.3.1. Configuration properties
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<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 |
4.3.2. 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 are performed when applicable.
The Holon platform provides some useful builtin property values providers as shown below:
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 |
4.3.3. 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 name using the pattern 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 property values.
4.3.4. ParameterSet
A ParameterSet is the representation of a generic parameters name and value map.
It provides methods to inspect the parameter set and obtain the parameter values.
The ConfigProperty
interface is fully supported and can be used as a typed parameter reprentation, avoiding type cast errors and always exposing the parameter value type.
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)
Optional<String> value = set.getParameter("testParameter", String.class); (5)
String val = set.getParameter("testParameter", String.class, "default"); (6)
Optional<String> configPropertyValue = set.getParameter(property); (7)
String configPropertyVal = set.getParameter(property, "default"); (8)
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 | Get a parameter value of String type |
6 | Get a parameter value of String type with default fallback value when the parameter value is not available |
7 | Get a parameter value using a ConfigProperty |
8 | Get a parameter value using a ConfigProperty , providing the default fallback value if not available |
4.4. Data validation
The main Holon Platform entry point to configure and perform data validation is the Validator interface.
4.4.1. Validator
The Validator interface can be implemented by a class which performs the validation of a value. A Validator
is generalized on the value type which the validator is able to validate.
The validate(T value)
method performs the actual validation of the specified value, throwing a ValidationException
if the value is not valid.
The ValidationException is localizable, supporting invalid value message localization. Furthermore, it can act as a container for multiple validation exceptions.
The Validator
interface provides static builder methods to create a Validator providing a condition predicate and with validation error message localization support.
See the Internationalization section for information about messages localization. |
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 | The same Validator created using the Validator.create() builder method |
3 | The same Validator created using the Validator.create() builder method and providing an invalid value message localization code |
4.4.2. Bultin validators
The Holon platform provides a set of validators for the most common use cases. Each of the builtin validators supports a localizable invalid value message and provides a default invalid value message if a custom one is not specified.
The available bultin validators can be obtained using the Validator
interface static builder methods:
-
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
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()); (3)
}
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 | The default getLocalizedMessage() method of the ValidationException class actually returns the localized validation error message, if a message localization code was provided and the platform localization context is setted up. See the Internationalization section for information about messages localization. |
4.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)
methos 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.
4.5. StringValuePresenter
The StringValuePresenter API deals with String
representation of a generic Object.
Presentation parameters can be used to tune the String
presentation strategy.
4.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 valuetoString()
representation is used -
Boolean
: boolean values are represented using the default boolean localization rules of the currentLocalizationContext
, if available. Otherwise,String.valueOf(value)
is used.See Internationalization for further information on internationalization and
LocalizationContext
-
Localizable
: the value is localized using currentLocalizationContext
, if available.See Internationalization for further information on internationalization and
LocalizationContext
-
Enum
: The enum value name is used by default. If the enumeration isLocalizable
, the value is localized using currentLocalizationContext
, if available. The@Caption
annotation annotation is supported on enumeration values for localization. -
Temporal
andDate
: The currentLocalizationContext
is used to format the value, if available. Otherwise, a default short date format is used with the default Locale. -
Number
: The currentLocalizationContext
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 ; . The holon.value-presenter.values-separator parameter can be used to change the separator character to use.
|
See the Internationalization section for information about messages localization. |
Presentation parameters:
The default StringValuePresenter
supports the following parameters to setup and tune the presentation:
Name | Constant | Type | Meaning |
---|---|---|---|
holon.value-presenter. values-separator |
String |
The separator character to use for arrays/collections presentation |
|
holon.value-presenter. max-length |
Integer number |
Limit the max length of the presented String |
|
holon.value-presenter. decimal-positions |
Integer number |
Specify the decimal positions to use to present numeric type values |
|
holon.value-presenter. disable-grouping |
Boolean (true/false) |
Disable the use of grouping symbol for numeric type values |
|
holon.value-presenter. hide-zero-decimals |
Boolean (true/false) |
Hide number decimals when all decimal positions (if any) are equal to zero |
|
holon.value-presenter. percent-style |
Boolean (true/false) |
Use a percent-style format for numeric decimal values |
|
holon.value-presenter. temporal-type |
TemporalType enumeration |
Set the temporal time format (Date, time or date and time) to use to present |
For Property value presentation, the presentation parameters are read from the property configuration attributes. |
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 |
4.6. Internationalization
The internationalization architecture of the Holon platform relies upon the LocalizationContext interface, which is the main entry point for the localization of messages, numbers and date/time elements.
4.6.1. Localizable messages
A localizable message is represented using the following attributes:
-
A default message: The default message to use if the localized message is not available or a localization provider is not available.
-
A localization message code: The symbolic message code to use as identifier to provide message localizations.
-
Optional message arguments: A set of arguments to be used to replace conventional placeholders in the message String with the actual values at message localization time.
The Localizable interface is available to represent a localizable message.
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 |
4.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 annotation attributes are:
-
value
: The default message to use as a caption -
messageCode
: The symbolic message code to use to provide message translations
The @Caption
annotation support must be declared and documented by the classes/elements which actually support it.
For example, the default StringValuePresenter supports the @Caption annotation for enum values presentation.
|
4.6.3. Message providers
To perform actual messages localization the MessageProvider API 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 Holon platform makes available a default MessageProvider
, which uses properties files as message localization containers. It can be created using the fromProperties(String… basenames)
static method:
MessageProvider messageProvider = MessageProvider.fromProperties("messages").build(); (1)
messageProvider = MessageProvider.fromProperties("i18n/messages").encoding("UTF-8").build(); (2)
1 | Build a MessageProvider which uses properties files with messages as base name |
2 | Build a MessageProvider which uses properties files with messages as base name in the i18n folder and set UTF-8 as encoding |
Properties file names are resolved using the configured basenames as prefix. This prefix can be followed by the Locale
language, country and variant codes, separated by an underscore (_
) character. The files must have the .properties
extension.
The basenames follow the java.util.ResourceBundle
conventions: essentially, a fully-qualified classpath location. If the base name 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 folder structure.
The Locale
attributes are used to build a fallback message localization resolution chain, starting from the most qualified Locale
definition and matched against the Locale
for which the message localization is requested.
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
withen
as language,US
as country andvar
as variant -
messages_en_US.properties: This file will be used for a
Locale
withen
as language,US
as country and no variant -
messages_en.properties: This file will be used for a
Locale
withen
as language and a country different fromUS
-
messages_it.properties: This file will be used for a
Locale
withit
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 the list of the available localizations (translations),
organized by message localization code. For example the test.msg=translation
line declares translation
as the localization of the test.msg
message code.
4.6.4. LocalizationContext
The LocalizationContext interface is the main entry point for localization of messages, numbers and date/time elements.
4.6.5. Building a LocalizationContext
The LocalizationContext
interface provides a fluent builder to create LocalizationContext
instances:
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 |
4.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), it can be obtained by using the convenience getCurrent()
static method.
The require()
static method can be used to obtain the current LocalizationContext
or throwing an exception if it’s not available as context resource.
Furthermore, the requireLocalized()
static method acts the same as the require()
method, but additionally requires that the current LocalizationContext
is localized.
4.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
, the localize(…)
method con be used, providing the Locale
instance.
To fine tune the context localization, a Localization object can be used instead of a simple Locale
.
A Localization
is bound to a Locale
and allows to setup:
-
A parent
Localization
, i.e. the Localization to use as fallback when a localization operation cannot be successfully performed using the current localization, for example because a message translation is not available. This allows the creation of aLocalization
hierarchy; -
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 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 |
4.6.8. Using the LocalizationContext
The LocalizationContext
API provides several methods to perform localizations of messages, temporal values 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
The Java 8 java.time.*
API is fully supported by the LocalizationContext
API.
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 |
4.6.9. MissingMessageLocalizationListener
One or more MissingMessageLocalizationListener can be registered to a LocalizationContext
to be notified when a message localization is missing from any of the available MessageProvider
.
The MissingMessageLocalizationListener
provides the localization message code for which the localization was requested, in addition to the Locale
and the optionally provided default message.
The withMissingMessageLocalizationListener(…)
method of the LocalizationContext
builder can be used to register a MissingMessageLocalizationListener
.
LocalizationContext ctx = LocalizationContext.builder()
.withMissingMessageLocalizationListener((locale, messageCode, defaultMessage) -> { (1)
LOGGER.warn("Missing message localization [" + messageCode + "] for locale [" + locale + "]");
}).build();
1 | Add a MissingMessageLocalizationListener to the LocalizationContext which logs missing message localizations |
4.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.
4.7.1. Property naming and identity
Each Property
provides a name through the getName()
method. The property name can have a different meaning for each implementation category. For example, if the Property
is actually bound to a concrete data model, the name can represent the data model attribute identifier.
Since the property name semantics is highly dependent from the concrete property implementation and use, it is not used by default to identify the property in the Holon platform architecture. From a standard Java point of view, a Property
is a Java class and it is identified by its address in memory. For this reason, it makes sense to declare the Property
instances as static class (or, better, interface) members.
But if a finest identity logic is required (and more control on Property
equality semantics is needed), the base Property
builder allows to provide a specific Property
identity and equality strategy.
This is achieved through the HashCodeProvider and EqualsHandler interfaces. This functional interfaces can be used to provide a custom hash code and equals logic, to override the default Java Objects hashCode
and equals
implementations. The combination of these two methods leads to a consistent Property
identity definition within the Java objects model and the Holon Platform architecture.
For example, to use the Property
name as unique property identifier, the Property
equals and hashCode strategy can be defined as follows:
Property.Builder<String, Property<String>, ?> builder = getPropertyBuilder();
builder.hashCodeProvider(property -> Optional.of(property.getName().hashCode())) (1)
.equalsHandler((property, other) -> { (2)
if (other instanceof Property)
return property.getName().equals(((Property) other).getName());
return false;
});
1 | Set the Property hash code strategy using the property name |
2 | Set the Property equals strategy using the property name |
The example above is not production ready, since the null values checking and management are completely absent.
|
4.7.2. Configuration
The Property
interface provides a generic container to store and manage property configuration attributes, and it is mainly intended as a custom configuration attributes handler for extension purposes and to better integrate the Property
representation in specific application architectures.
The property configuration is represented by the PropertyConfiguration interface and can be obtained through the Property.getConfiguration()
method.
The PropertyConfiguration
interface extends the Holon platform ParameterSet API, providing a set of methods to inspect and obtain the configuration attributes. Since the ParameterSet
API can be compared to a name-value map, it is highly flexible and versatile, allowing to store and retrieve anything that can be represented as a Java object.
Besides the generic configuration attributes, the PropertyConfiguration
interface explicitly declares a 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 is used by default by a number of platform services to perform consistent operations on the 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 is no need to use the TemporalType property configuration attribute to ensure consistency in property value manipulation and presentation.
|
The property configuration is considered as immutable during the Property
lifetime, and can be setted up at Property
build time, using the property builder’s configuration(String parameterName, Object value)
methods. Since the PropertyConfiguration
interface extends the ParameterSet
API, the Configuration properties type is fully supported.
final ConfigProperty<Long> EXAMPLE_CFG = ConfigProperty.create("exampleConfig", Long.class);
Property.Builder<String, Property<String>, ?> builder = getPropertyBuilder();
builder.temporalType(TemporalType.DATE_TIME) (1)
.configuration("myAttribute", "myValue") (2)
.configuration(EXAMPLE_CFG, 7L); (3)
PropertyConfiguration cfg = aProperty().getConfiguration(); (4)
Optional<String> value1 = cfg.getParameter("myAttribute", String.class); (5)
Long value2 = cfg.getParameter(EXAMPLE_CFG, 0L); (6)
1 | Set the property TemporalType |
2 | Set a custom configuration attribute |
3 | Set a configuration attribute using a ConfigProperty |
4 | Get the property configuration |
5 | Get the myAttribute configuration attribute |
6 | Get a property configuration value using a ConfigProperty , providing a default fallback value |
4.7.3. Converters
Each Property
supports a PropertyValueConverter, which can be used to perform property value conversions from a the actual Property
type to a different value type and vice-versa.
Tipically, the PropertyValueConverter
API is used to map the data model attribute type to which the Property
is bound to the actual Property
presentation type. 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.
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 |
A PropertyValueConverter
can be provided at Property
build time, using the appropriate property builder methods. The base Property
builder provides also a method to declare the property value conversion logic using standard Java functions.
Futhermore, the Property
interface makes available a convenience getConvertedValue
method to obtain the Property
model value using the configured value converter, if available, or returning the actual property value if not.
Property.Builder<Integer, Property<Integer>, ?> builder = getPropertyBuilder();
builder.converter(String.class, (1)
v -> (v != null) ? Integer.parseInt(v) : null, (2)
v -> (v != null) ? String.valueOf(v) : null); (3)
1 | Set the Property value converter providing the model data type and the value conversion functions |
2 | Set the value conversion function to convert a String model value into the Integer property type |
3 | Set the value conversion function to convert the Integer property value type to a String type model value |
The Holon platform provides some useful builtin converters out-of-the-box:
-
Numeric boolean converter: perform conversions from/to a numeric data model type to a
boolean
property type using the following convention:null
or0
numeric values will be converted asfalse
boolean values, any other value will be converted as thetrue
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 8LocalDate
temporal type property types; -
LocalDateTime value converter: perform conversions from/to
Date
type data model values and Java 8LocalDateTime
temporal type property types.
The PropertyValueConverter
interface provides static methods to obtain the listed builtin converters.
PropertyValueConverter.numericBoolean(Integer.class); (1)
PropertyValueConverter.localDate(); (2)
PropertyValueConverter.localDateTime(); (3)
PropertyValueConverter.enumByOrdinal(); (4)
PropertyValueConverter.enumByName(); (5)
Property.Builder<Boolean, Property<Boolean>, ?> builder = getPropertyBuilder();
builder.converter(PropertyValueConverter.numericBoolean(Integer.class)); (6)
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 |
6 | Set a numeric boolean builtin converter a Property build time, to map an Integer model value to a Boolean property type. |
4.7.4. Localization
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.
Since Property
is a Localizable
, the property localization attributes can be used anywhere is meaningful and in a seamless way within an internationalization environment.
This allows to centralize the property display and localization attributes and store it tightly coupled with the property definition.
See Internationalization for additional details about the Holon platform internationalization architecture.
4.7.5. Validation
The Property
interface implements the Validatable API to support property value validation using Validators
.
A set of property value Validator can be added to the property definition at property build time and later used to validate a value against the property definition.
This allows to tightly couple the validation logic to the property representation and make it available to any actor which will use the property itself, promoting validation factorization and value consistency across service or application stack layers.
See the Data validation section for detailed information about validators definition and usage.
Property.Builder<Integer, Property<Integer>, ?> builder = getPropertyBuilder();
builder.validator(Validator.notNull()) (1)
.validator(Validator.lessThan(10)); (2)
1 | Add a property validator to ensure the value is not null |
2 | Add a property validator to ensure the value is less than 10 |
4.7.6. Read only attribute
The Property
interface declares by default a read-only attribute, which can be used by property handlers to check if the property represents a binding with a data model attribute in both directions: to read and to write values.
The actual meaning of the read-only attribute can depend from each implementation and data model architecture. The read-only attribute is immutable, and bound to each Property
API extension, so it cannot be changed at property definition time or during the propety lifecycle.
4.8. PathProperty
When a property is bound to a data model attribute, the Holon platform provides the PathProperty interface to declare a Property
and represent the binding with the data model attribute.
The binding with the data model attribute is represented by the Path interface, extended by the PathProperty
itself. A Path
is the symbolic String
representation of the data model attribute, and can assume a different meaning for each concrete data persistence context.
Generally speaking, the Path
name corresponds to the name of the data model attribute. In the PathProperty
case, the property name is the path name itself.
A Path
can be hierarchical, supporting parent path declaration. When a path hierarchy is defined, the full path name is represented by default by the concatenation of the path hierarchy, starting from the first ancestor (the root path), using the dot (.
) character as path hierarchy separator. The default fullName()
method can be used to obtain the full path name.
Just like a Property
, a Path
is typed, i.e. declares the Java type of the path segment which represents. Talking about a data model, this corresponds to the type of the data model attribute to which the path segment is bound. For the PathProperty
, the path and the property type coincide.
The Path
can also be used independently from a Property
definition. The Path
interface provides builder methods to create path instances:
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 named "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 the parent path |
5 | The path full name will be pathName.subName |
The PathProperty
API combines the Property
and Path
APIs, allowing to declare a property which is bound to a data model path.
The PathProperty
builder makes available all the Property
and Path
builder methods, allowing to declare the property path name, the property and path type and to setup the property configuration attributes, a property value converter, register property value validators and define property localization attributes.
A property builder is obtained using the create(…)
static methods:
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 (the path name) of type Long |
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 (the path name) of Boolean type but which is bound to a Integer data model attribute type |
7 | Set the converter to perform conversion between the Integer data model values and the Boolean property type |
As a Path
, the PathProperty
builder supports property hierarchy definition, allowing to set the parent property path:
public final static PathProperty<String> PARENT_PROPERTY = PathProperty.create("parent", String.class);
public final static PathProperty<String> A_PROPERTY = PathProperty.create("child", String.class)
.parent(PARENT_PROPERTY); (1)
public final static StringProperty STRING_PROPERTY = StringProperty.create("name"); (1)
public final static NumericProperty<Integer> INTEGER_PROPERTY = NumericProperty.create("name", Integer.class); (2)
public final static NumericProperty<Long> LONG_PROPERTY = NumericProperty.longType("name"); (3)
public final static TemporalProperty<LocalDate> LDATE_PROPERTY = TemporalProperty.localDate("name"); (4)
public final static TemporalProperty<Date> DATE_PROPERTY = TemporalProperty.date("name")
.temporalType(TemporalType.DATE); (5)
public final static BooleanProperty BOOLEAN_PROPERTY = BooleanProperty.create("name"); (6)
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)
public void propertySet() {
final StringProperty NAME = StringProperty.create("name");
final StringProperty SURNAME = StringProperty.create("surname");
final NumericProperty<Integer> SEQUENCE = NumericProperty.integerType("surname");
PropertySet<Property<?>> set = PropertySet.of(NAME, SURNAME); (1)
set = PropertySet.builder().add(NAME).add(SURNAME).build(); (2)
PropertySet<Property<?>> set2 = PropertySet.builder().add(set).add(SEQUENCE).build(); (3)
}
public void propertySet2() {
final PathProperty<String> NAME = PathProperty.create("name", String.class);
final PathProperty<String> SURNAME = PathProperty.create("surname", String.class);
final PropertySet<Property<?>> SET = PropertySet.of(NAME, SURNAME); (1)
boolean contains = SET.contains(NAME); (2)
SET.forEach(p -> p.toString()); (3)
String captions = SET.stream().map(p -> p.getMessage()).collect(Collectors.joining()); (4)
List<Property<?>> list = SET.asList(); (5)
}
public void propertySet3() {
final NumericProperty<Long> ID = NumericProperty.longType("id");
final StringProperty NAME = StringProperty.create("name");
PropertySet<Property<?>> SET = PropertySet.builder().add(ID).add(NAME).identifier(ID).build(); (1)
SET = PropertySet.builderOf(ID, NAME).identifier(ID).build(); (2)
Set<Property<?>> ids = SET.getIdentifiers(); (3)
Optional<Property<?>> id = SET.getFirstIdentifier(); (4)
SET.identifiers().forEach(p -> p.toString()); (5)
}
@SuppressWarnings("unchecked")
public void propertyBox() {
final PathProperty<Long> ID = PathProperty.create("id", Long.class);
final StringProperty NAME = StringProperty.create("name");
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).set(ID, 1L).set(NAME, "testName").build(); (5)
Long id = propertyBox.getValue(ID); (6)
String name = propertyBox.getValueIfPresent(NAME).orElse("default"); (7)
boolean containsNotNullId = propertyBox.containsValue(ID); (8)
PropertyBox ids = propertyBox.cloneBox(ID); (9)
}
public void propertyBox2() {
final PathProperty<Long> ID = PathProperty.create("id", Long.class).validator(Validator.notNull()); (1)
final StringProperty NAME = StringProperty.create("name").validator(Validator.notBlank()); (2)
final PropertySet<?> PROPERTIES = PropertySet.of(ID, NAME);
PropertyBox propertyBox = PropertyBox.create(PROPERTIES);
propertyBox.setValue(ID, null); (3)
propertyBox = PropertyBox.builder(PROPERTIES).invalidAllowed(true).build(); (4)
propertyBox.validate(); (5)
}
public void propertyBox3() {
final StringProperty NAME = StringProperty.create("name");
final StringProperty SURNAME = StringProperty.create("surname");
final VirtualProperty<String> FULL_NAME = VirtualProperty.create(String.class, propertyBox -> { (1)
return propertyBox.getValue(NAME) + " " + propertyBox.getValue(SURNAME);
});
PropertyBox propertyBox = PropertyBox.create(NAME, SURNAME, FULL_NAME); (2)
propertyBox.setValue(NAME, "John");
propertyBox.setValue(SURNAME, "Doe");
String fullName = propertyBox.getValue(FULL_NAME); (3)
}
public void propertyBox4() {
final NumericProperty<Long> ID = NumericProperty.longType("id");
final StringProperty NAME = StringProperty.create("name");
final PropertySet<?> PROPERTIES = PropertySet.builderOf(ID, NAME).identifier(ID).build(); (1)
PropertyBox propertyBox1 = PropertyBox.builder(PROPERTIES).set(ID, 1L).set(NAME, "name1").build();
PropertyBox propertyBox2 = PropertyBox.builder(PROPERTIES).set(ID, 1L).set(NAME, "name2").build();
boolean isTrue = propertyBox1.equals(propertyBox2); (2)
}
public void propertyBox5() {
final NumericProperty<Integer> ID = NumericProperty.integerType("id");
final StringProperty NAME = StringProperty.create("name");
PropertyBox propertyBox = PropertyBox.builder(ID, NAME)
.hashCodeProvider(pb -> Optional.ofNullable(pb.getValue(ID))) (1)
.equalsHandler((pb, other) -> (other instanceof PropertyBox) (2)
&& ((PropertyBox) other).getValue(ID).equals(pb.getValue(ID)))
.build();
}
public void presenter0() {
final NumericProperty<Integer> ID = NumericProperty.integerType("id");
PropertyValuePresenter<Integer> presenter = getPropertyValuePresenter();
presenter.present(ID, 123); (1)
}
private static PropertyValuePresenter<Integer> getPropertyValuePresenter() {
return null;
}
public void presenter() {
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)
}
public void presenterRegistration() {
PropertyValuePresenter<LocalTime> myPresenter = (p, v) -> v.getHour() + "." + v.getMinute(); (1)
PropertyValuePresenterRegistry.get().register(p -> LocalTime.class.isAssignableFrom(p.getType()), myPresenter); (2)
}
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)
}
public void subTypes() {
final StringProperty STR = StringProperty.create("name"); (1)
QueryFilter filter = STR.contains("value"); (2)
}
}
1 | Create a PathProperty named child and set the PARENT_PROPERTY property definition as parent path |
4.8.1. PathProperty sub types
When a PathProperty
is used as a data model query expression, the property value type can be a discriminant for the expression usage within a query definition. For example, a query restriction expression can be consistent only for specific data type, or a query function could only be applicable for a certain data type.
Since the PathProperty
interface provides useful convenience methods to create expressions from the property itself, a set of PathProperty
sub types are provided to deal with the most common data types.
Each sub type provides one or more specific builder method too, to easily and quickly create the property definition.
Four PathProperty
sub types are provided out-of-the-box by the Holon platform:
-
StringProperty: Represents a
String
typePathProperty
and extends StringQueryExpression, providing String related expression builder methods. -
NumericProperty: Represents a
Number
typePathProperty
and extends NumericQueryExpression, providing numbers related expression builder methods. -
TemporalProperty: Represents a temporal type
PathProperty
and extends TemporalQueryExpression, providing temporal related expression builder methods. -
BooleanProperty: Represents a boolean type
PathProperty
. -
PropertyBoxProperty: Represents a
PropertyBox
typePathProperty
, i.e. a property which value is a set of properties with its values. The property set definition must be provided at construction time. See PropertySet and PropertyBox for details.
Each PathProperty
sub type interface provide static builder methods to create property instances using each specific type.
public final static StringProperty STRING_PROPERTY = StringProperty.create("name"); (1)
public final static NumericProperty<Integer> INTEGER_PROPERTY = NumericProperty.create("name", Integer.class); (2)
public final static NumericProperty<Long> LONG_PROPERTY = NumericProperty.longType("name"); (3)
public final static TemporalProperty<LocalDate> LDATE_PROPERTY = TemporalProperty.localDate("name"); (4)
public final static TemporalProperty<Date> DATE_PROPERTY = TemporalProperty.date("name")
.temporalType(TemporalType.DATE); (5)
public final static BooleanProperty BOOLEAN_PROPERTY = BooleanProperty.create("name"); (6)
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)
public void propertySet() {
final StringProperty NAME = StringProperty.create("name");
final StringProperty SURNAME = StringProperty.create("surname");
final NumericProperty<Integer> SEQUENCE = NumericProperty.integerType("surname");
PropertySet<Property<?>> set = PropertySet.of(NAME, SURNAME); (1)
set = PropertySet.builder().add(NAME).add(SURNAME).build(); (2)
PropertySet<Property<?>> set2 = PropertySet.builder().add(set).add(SEQUENCE).build(); (3)
}
public void propertySet2() {
final PathProperty<String> NAME = PathProperty.create("name", String.class);
final PathProperty<String> SURNAME = PathProperty.create("surname", String.class);
final PropertySet<Property<?>> SET = PropertySet.of(NAME, SURNAME); (1)
boolean contains = SET.contains(NAME); (2)
SET.forEach(p -> p.toString()); (3)
String captions = SET.stream().map(p -> p.getMessage()).collect(Collectors.joining()); (4)
List<Property<?>> list = SET.asList(); (5)
}
public void propertySet3() {
final NumericProperty<Long> ID = NumericProperty.longType("id");
final StringProperty NAME = StringProperty.create("name");
PropertySet<Property<?>> SET = PropertySet.builder().add(ID).add(NAME).identifier(ID).build(); (1)
SET = PropertySet.builderOf(ID, NAME).identifier(ID).build(); (2)
Set<Property<?>> ids = SET.getIdentifiers(); (3)
Optional<Property<?>> id = SET.getFirstIdentifier(); (4)
SET.identifiers().forEach(p -> p.toString()); (5)
}
@SuppressWarnings("unchecked")
public void propertyBox() {
final PathProperty<Long> ID = PathProperty.create("id", Long.class);
final StringProperty NAME = StringProperty.create("name");
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).set(ID, 1L).set(NAME, "testName").build(); (5)
Long id = propertyBox.getValue(ID); (6)
String name = propertyBox.getValueIfPresent(NAME).orElse("default"); (7)
boolean containsNotNullId = propertyBox.containsValue(ID); (8)
PropertyBox ids = propertyBox.cloneBox(ID); (9)
}
public void propertyBox2() {
final PathProperty<Long> ID = PathProperty.create("id", Long.class).validator(Validator.notNull()); (1)
final StringProperty NAME = StringProperty.create("name").validator(Validator.notBlank()); (2)
final PropertySet<?> PROPERTIES = PropertySet.of(ID, NAME);
PropertyBox propertyBox = PropertyBox.create(PROPERTIES);
propertyBox.setValue(ID, null); (3)
propertyBox = PropertyBox.builder(PROPERTIES).invalidAllowed(true).build(); (4)
propertyBox.validate(); (5)
}
public void propertyBox3() {
final StringProperty NAME = StringProperty.create("name");
final StringProperty SURNAME = StringProperty.create("surname");
final VirtualProperty<String> FULL_NAME = VirtualProperty.create(String.class, propertyBox -> { (1)
return propertyBox.getValue(NAME) + " " + propertyBox.getValue(SURNAME);
});
PropertyBox propertyBox = PropertyBox.create(NAME, SURNAME, FULL_NAME); (2)
propertyBox.setValue(NAME, "John");
propertyBox.setValue(SURNAME, "Doe");
String fullName = propertyBox.getValue(FULL_NAME); (3)
}
public void propertyBox4() {
final NumericProperty<Long> ID = NumericProperty.longType("id");
final StringProperty NAME = StringProperty.create("name");
final PropertySet<?> PROPERTIES = PropertySet.builderOf(ID, NAME).identifier(ID).build(); (1)
PropertyBox propertyBox1 = PropertyBox.builder(PROPERTIES).set(ID, 1L).set(NAME, "name1").build();
PropertyBox propertyBox2 = PropertyBox.builder(PROPERTIES).set(ID, 1L).set(NAME, "name2").build();
boolean isTrue = propertyBox1.equals(propertyBox2); (2)
}
public void propertyBox5() {
final NumericProperty<Integer> ID = NumericProperty.integerType("id");
final StringProperty NAME = StringProperty.create("name");
PropertyBox propertyBox = PropertyBox.builder(ID, NAME)
.hashCodeProvider(pb -> Optional.ofNullable(pb.getValue(ID))) (1)
.equalsHandler((pb, other) -> (other instanceof PropertyBox) (2)
&& ((PropertyBox) other).getValue(ID).equals(pb.getValue(ID)))
.build();
}
public void presenter0() {
final NumericProperty<Integer> ID = NumericProperty.integerType("id");
PropertyValuePresenter<Integer> presenter = getPropertyValuePresenter();
presenter.present(ID, 123); (1)
}
private static PropertyValuePresenter<Integer> getPropertyValuePresenter() {
return null;
}
public void presenter() {
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)
}
public void presenterRegistration() {
PropertyValuePresenter<LocalTime> myPresenter = (p, v) -> v.getHour() + "." + v.getMinute(); (1)
PropertyValuePresenterRegistry.get().register(p -> LocalTime.class.isAssignableFrom(p.getType()), myPresenter); (2)
}
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)
}
public void subTypes() {
final StringProperty STR = StringProperty.create("name"); (1)
QueryFilter filter = STR.contains("value"); (2)
}
}
1 | Create a StringProperty |
2 | Create a NumericProperty of Integer type |
3 | Create a NumericProperty of Long type |
4 | Create a TemporalProperty of LocalDate type |
5 | Create a TemporalProperty of Date type |
6 | Create a BooleanProperty |
4.9. VirtualProperty
The VirtualProperty interface represents a Property
which is not directly bound to a data model attribute, but which instead provides its value through a PropertyValueProvider
.
A VirtualProperty
is declared as read only by default.
The property value provision is delegated to the PropertyValueProvider at runtime. The provided value must be consistent with the VirtualProperty
type.
The PropertyValueProvider
is configured at VirtualProperty
definition time, and it is immutable during the property lifecycle.
When a VirtualProperty
is used within objects and structures which explicitly support it, the current data model context can be used to provide the virtual property value. The current data context is represented through a PropertyBox
and it is provided to the PropertyValueProvider
value providing method. This way, the value of other properties can be used to calculate the virtual property value.
See Managing property values using a PropertyBox for further information. |
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 |
4.10. Organizing and collecting properties using a PropertySet
The PropertySet interface represents an immutable set of Property
, providing methods to inspect and obtain the Property
elements. It is an Iterable
, in analogy with the standard Java collections structures.
A PropertySet
allows to collect and organize a set of properties and use it consistently through the application stack levels.
In reference to a data model, a PropertySet
can represent a data model entity as a collection of data model attributes. In this sense, it can be used for example as a query projection, to obtain the values of the properties which belogs to the set as a PropertyBox
instance.
See the Query section for information about query definition and execution. |
The PropertySet
API makes available appropriate builder methods to create PropertySet
definitions.
final StringProperty NAME = StringProperty.create("name");
final StringProperty SURNAME = StringProperty.create("surname");
final NumericProperty<Integer> SEQUENCE = NumericProperty.integerType("surname");
PropertySet<Property<?>> set = PropertySet.of(NAME, SURNAME); (1)
set = PropertySet.builder().add(NAME).add(SURNAME).build(); (2)
PropertySet<Property<?>> set2 = PropertySet.builder().add(set).add(SEQUENCE).build(); (3)
1 | Create a PropertySet containing NAME and SURNAME properties |
2 | Create a PropertySet containing NAME and SURNAME properties using the fluent builder |
3 | Create a PropertySet adding the SEQUENCE property to the properties of the previous property set |
The PropertySet
API can be used as a standard Iterable
. Furthermore, it provides convenience methods to inspect the property set contents.
final PathProperty<String> NAME = PathProperty.create("name", String.class);
final PathProperty<String> SURNAME = PathProperty.create("surname", String.class);
final PropertySet<Property<?>> SET = PropertySet.of(NAME, SURNAME); (1)
boolean contains = SET.contains(NAME); (2)
SET.forEach(p -> p.toString()); (3)
String captions = SET.stream().map(p -> p.getMessage()).collect(Collectors.joining()); (4)
List<Property<?>> list = SET.asList(); (5)
1 | Create a PropertySet which contains the NAME and SURNAME properties |
2 | Check if PropertySet contains the property NAME |
3 | Use the forEach operation to invoke the toString method for each property of the set |
4 | Use a stream of the properties of the set to join the property captions in a String |
5 | Obtain the PropertySet as a List of properties |
4.10.1. PropertySet configuration
The PropertySet
API provides a generic container to store and manage property configuration attributes, and it is mainly intended as a custom configuration attributes handler for extension purposes and to better integrate the PropertySet
representation in specific application architectures.
The property set configuration can be obtained through the PropertySet.getConfiguration()
method and is represented by a ParameterSet
, providing a set of methods to inspect and obtain the configuration attributes. Since the ParameterSet
API can be compared to a name-value map, it is highly flexible and versatile, allowing to store and retrieve anything that can be represented as a Java object.
4.10.2. Identifier properties
The PropertySet
API supports the declaration of the properties which act as identifiers for the property set. Such properties must belong to the property set itself.
The identifier properties are declared at PropertySet
definition time and can be later inspected using the PropertySet
API. The PropertySet
interface provides also builderOf
static method to directly provide the properties of the set and declare the identifiers immediately after.
final NumericProperty<Long> ID = NumericProperty.longType("id");
final StringProperty NAME = StringProperty.create("name");
PropertySet<Property<?>> SET = PropertySet.builder().add(ID).add(NAME).identifier(ID).build(); (1)
SET = PropertySet.builderOf(ID, NAME).identifier(ID).build(); (2)
Set<Property<?>> ids = SET.getIdentifiers(); (3)
Optional<Property<?>> id = SET.getFirstIdentifier(); (4)
SET.identifiers().forEach(p -> p.toString()); (5)
1 | Create a PropertySet with the ID and NAME properties, declaring ID as the identifier property |
2 | The same operation using the convenience builderOf method |
3 | Get the identifier properties as a Set |
4 | Get the first identifier property, if available |
5 | Get the identifier properties as a Stream |
4.11. Managing property values using a PropertyBox
A PropertyBox represents a container of Property
values and it is bound to a specific (and immutable) PropertySet. For each Property
of the property set, the PropertyBox
can be used to set or obtain the associated property value.
The PropertyBox
handles the property values ensuring property and value type consistency, fully supporting the PropertyValueConverter to perform value conversions when suitable.
The PropertyBox
API provides builders to create PropertyBox
instances and setting the property values. The property values can be obtain from the PropertyBox
and setted in the PropertyBox
itself during the object lifetime. Several methods are available to inspect the PropertyBox
definition and contents.
The PropertyBox
API directly extends the PropertySet
API, consequently providing its property set inspection methods.
final PathProperty<Long> ID = PathProperty.create("id", Long.class);
final StringProperty NAME = StringProperty.create("name");
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).set(ID, 1L).set(NAME, "testName").build(); (5)
Long id = propertyBox.getValue(ID); (6)
String name = propertyBox.getValueIfPresent(NAME).orElse("default"); (7)
boolean containsNotNullId = propertyBox.containsValue(ID); (8)
PropertyBox ids = propertyBox.cloneBox(ID); (9)
1 | Create an empty PropertyBox using ID and NAME properties as property set |
2 | Create an empty PropertyBox using the 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 type 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 type in this case) |
5 | Create a PropertyBox using the fluent builder, with the PROPERTIES property set, setting the property values |
6 | 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) |
7 | Get the Optional NAME property value, using the default String value if a value for that property is not present in the PropertyBox |
8 | Check if a value for the ID property is present in the PropertyBox , i.e. the ID property is available in the property set and its value is not null |
9 | Clone the PropertyBox , creating a new PropertyBox with a property set composed only by the ID property |
The Property
value validation is enabled by default, ensuring a valid property value management by invoking any property value Validator
when dealing with property value. The PropertyBox
API handles the property value validation any time a property value is about to be setted in the PropertyBox
.
This behaviour can be disabled at PropertyBox
definition time by using the invalidAllowed(true)
builder method. An explicit validate()
is available to explicitly perform the validation of all the property values currently available in the PropertyBox
.
final PathProperty<Long> ID = PathProperty.create("id", Long.class).validator(Validator.notNull()); (1)
final StringProperty NAME = StringProperty.create("name").validator(Validator.notBlank()); (2)
final PropertySet<?> PROPERTIES = PropertySet.of(ID, NAME);
PropertyBox propertyBox = PropertyBox.create(PROPERTIES);
propertyBox.setValue(ID, null); (3)
propertyBox = PropertyBox.builder(PROPERTIES).invalidAllowed(true).build(); (4)
propertyBox.validate(); (5)
1 | Add a not null validator to the ID property |
2 | Add a not empty validator to the NAME property |
3 | Setting the ID property value to null will throw a ValidationException |
4 | Build a PropertyBox with disabled automatic property value validation |
5 | Trigger property value validation explicitly |
The PropertyBox
API fully supports virtual properties. When the value of a VirtualProperty
is requested from a PropertyBox
, the current PropertyBox
instance is provided to the PropertyValueProvider
which is used to supply the virtual property value.
final StringProperty NAME = StringProperty.create("name");
final StringProperty SURNAME = StringProperty.create("surname");
final VirtualProperty<String> FULL_NAME = VirtualProperty.create(String.class, propertyBox -> { (1)
return propertyBox.getValue(NAME) + " " + propertyBox.getValue(SURNAME);
});
PropertyBox propertyBox = PropertyBox.create(NAME, SURNAME, FULL_NAME); (2)
propertyBox.setValue(NAME, "John");
propertyBox.setValue(SURNAME, "Doe");
String fullName = propertyBox.getValue(FULL_NAME); (3)
1 | Define a VirtualProperty to provide a full name, chaining the values of the NAME and SURNAME properties |
2 | Add the FULL_NAME property to the PropertyBox property set |
3 | Get the FULL_NAME virtual property value: it will be John Doe |
The PropertyBox
abstraction is the base structure to transport and provide property values in the Holon Platform. It is used by the Holon platform every time a set of property values comes into play.
In a data exchange scenario (for example the interaction with a persistent data model or the interchange of data between two independent services), the PropertyBox
API allows to preserve a strong independence from the underlying concrete data structure.
In a typical data model mapping scenario, a PropertyBox
is used to represent a persistent data entity value, i.e. the values of all the data model entity attributes (the property set).
4.11.1. PropertyBox instances identification
As a standard Java object, a PropertyBox
instance is identified by its memory address. This makes each PropertyBox
instance "different" form one another, in the Java objects sense, regardless of the property set and the property values contained in the PropertyBox
.
This may not always be the desired behavior. In many situations is it appropriate to implement a PropertyBox
identification strategy relying on its property set and the corresponding property values. According to this vision, two PropertyBox
with the same property set and the same property values should be considered equal.
When dealing with a data model, a more consistent PropertyBox
identification strategy should consider only the identifier property values, to check PropertyBox
equality within the same data model entity.
For this reason, the PropertySet
identifier properties are used by default to implement the PropertyBox
identification strategy: this means that the standard equals and hashCode methods of the PropertyBox
instance are implemented accordingly to the identifier property values, if available from the property set.
final NumericProperty<Long> ID = NumericProperty.longType("id");
final StringProperty NAME = StringProperty.create("name");
final PropertySet<?> PROPERTIES = PropertySet.builderOf(ID, NAME).identifier(ID).build(); (1)
PropertyBox propertyBox1 = PropertyBox.builder(PROPERTIES).set(ID, 1L).set(NAME, "name1").build();
PropertyBox propertyBox2 = PropertyBox.builder(PROPERTIES).set(ID, 1L).set(NAME, "name2").build();
boolean isTrue = propertyBox1.equals(propertyBox2); (2)
1 | Declare the ID property as the PropertySet identifier property |
2 | Since the two PropertyBox instances contains the same ID property value (1 ), they will be considered equal by default |
4.11.2. Custom PropertyBox instances identification
When the default PropertyBox
identification strategy is not suitable or consistent for your needs, it can be customized just like it can be done with a Property definition.
This is achieved through the HashCodeProvider and EqualsHandler interfaces. This functional interfaces can be used to provide a custom hash code and equals logic for the PropertyBox
instances, to override the default Java Objects hashCode
and equals
implementations.
The custom HashCodeProvider
and EqualsHandler
implementation can be setted at PropertyBox
definition time using the appropriate builder methods.
final NumericProperty<Integer> ID = NumericProperty.integerType("id");
final StringProperty NAME = StringProperty.create("name");
PropertyBox propertyBox = PropertyBox.builder(ID, NAME)
.hashCodeProvider(pb -> Optional.ofNullable(pb.getValue(ID))) (1)
.equalsHandler((pb, other) -> (other instanceof PropertyBox) (2)
&& ((PropertyBox) other).getValue(ID).equals(pb.getValue(ID)))
.build();
1 | Custom HashCodeProvider |
2 | Custom EqualsHandler |
4.12. 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
.
final NumericProperty<Integer> ID = NumericProperty.integerType("id");
PropertyValuePresenter<Integer> presenter = getPropertyValuePresenter();
presenter.present(ID, 123); (1)
1 | Present the value 123 for the ID property using a PropertyValuePresenter |
The property value presenters are organized in a registry (the PropertyValuePresenterRegistry), which collects all the available presenters and provides the most suitable presenter for a given property, using the conditions, expressed as Predicate
, the presenters were registered with.
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.
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.
4.12.1. PropertyValuePresenter registration
The registration of a new PropertyValuePresenter
can be made in two ways:
1. Using the PropertyValuePresenterRegistry:
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.
|
4.12.2. Default PropertyValuePresenter
A default property value presenter is provided by the platform and automatically registered in the registry. The default presenter is used when no other suitable presenter is available form 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.
|
4.12.3. Using the Property presenters
The Property
interface provides a convenience present(T value)
method to present the 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.
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 |
4.13. Property rendering
A further property handling concept is made available by the Holon platform: the property renderers.
The PropertyRenderer interface 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 a Predicate
, 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.
4.13.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.
|
4.13.2. Using the Property renderers
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 aNoSuitableRendererAvailableException
if noPropertyRenderer
is available for this property and given rendering type -
renderIfAvailable(Class renderType)
: Renders the property as given renderType object type if a suitablePropertyRenderer
for the required renderType is available from thePropertyRendererRegistry
obtained from currentContext
or from the default one for the current ClassLoader. If a suitable renderer is not available, an empty Optional is returned.
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 |
4.14. Java Beans and the Property
model
The Holon platform offers a wide support to handle standard Java Beans and seamlessy integrate them with the platform Property model.
A Java Bean can be seen as a collection of typed properties, with getter and setter methods to read and write the property values. From this perspective, we can introduce the following analogies:
-
A Bean property can be represented by a Property.
-
The Bean definition (the collection of the declared properties) can be represented by a PropertySet.
-
A Bean instance, which holds the property values, can be represented by a PropertyBox.
The Holon platform provides a complete API to manage Java Beans using the property model, making available all the essential services to switch from one model to another, i.e. to handle a Bean as a Property
set and to manage the Bean property values through a PropertyBox
, both to read the Bean property values and to write them.
4.14.1. Bean properties
A Java Bean property is represented by a PathProperty, where the property path name 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.
Internally, a Bean property is actually represented by a PathProperty
extension, which is defined through the BeanProperty interface. A BeanProperty
instance holds the references to the Bean property getter and setter methods, besides other property configuration attributes, to ensure consistency for the property value read and write operations.
4.14.2. Bean property set
The BeanPropertySet interface represents the collection of the Bean definition properties as a PropertySet
.
The BeanPropertySet
is obtained from a Bean class and provides the available Bean properties as PathProperty
references.
Fully supports nested bean classes, allowing to access the nested bean class properties by name using the conventional dot notation, for example parentProperty.nestedProperty
.
The BeanPropertySet
API extends the default PropertySet
API and additionally provides operations to:
-
Obtain a Bean property by name as a
PathProperty
. -
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 a Bean instance and obtain such values as a
PropertyBox
. -
Write the property values contained in a
PropertyBox
to a Bean instance.
A BeanPropertySet
can be simply obtained from a Bean class using the create(Class beanClass)
method.
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 = BeanPropertySet.create(MyBean.class); (1)
public void propertySet() {
Optional<PathProperty<Long>> idProperty = PROPERTIES.<Long>getProperty("id"); (2)
PathProperty<Long> id = PROPERTIES.property("id", Long.class); (3)
PathProperty<String> nestedName = PROPERTIES.property("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.property("id")); (7)
// write
instance = new MyBean();
PROPERTIES.write("nested.nestedName", "test", instance); (8)
MyBean written = PROPERTIES.write(PropertyBox.builder(PROPERTIES).set(PROPERTIES.property("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 |
In addition to the standard PathProperty
representation, a Bean property can be also obtained as a specific sub type, when type consistency is ensured. The supported sub types are the builtin PathProperty sub types made available by the Holon platform.
Each Bean property can be obtained as a specific PathProperty
type using the appropriate methods of the BeanPropertySet
API. When type consistency is not respected, an exception is thrown.
final BeanPropertySet<MyBean> PROPERTIES = BeanPropertySet.create(MyBean.class);
StringProperty stringProperty = PROPERTIES.propertyString("aStringTypeBeanPropertyName"); (1)
NumericProperty<Integer> numericProperty = PROPERTIES.propertyNumeric("aIntegerTypeBeanPropertyName"); (2)
TemporalProperty<LocalDate> temporalProperty = PROPERTIES.propertyTemporal("aLocalDateTypeBeanPropertyName"); (3)
BooleanProperty booleanProperty = PROPERTIES.propertyBoolean("aBooleanTypeBeanPropertyName"); (4)
1 | Get the Bean property named aStringTypeBeanPropertyName as a StringProperty |
2 | Get the Bean property named aIntegerTypeBeanPropertyName as a NumericProperty |
3 | Get the Bean property named aLocalDateTypeBeanPropertyName as a TemporalProperty |
4 | Get the Bean property named aBooleanTypeBeanPropertyName as a BooleanProperty |
4.14.3. BeanIntrospector
A BeanPropertySet
is built using the BeanIntrospector API. It provides methods to actually obtain a BeanPropertySet
from a Bean class, introspecting it to resolve the Bean properties and their configuration.
The BeanIntrospector
interface provides static methods to obtain a BeanIntrospector
as a Context resource, if available, or retrieve the default implementation, which is always available.
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 |
Furthermore, the BeanIntrospector
API makes available convenience methods to directly obtain a Bean instance as a PropertyBox
and vice-versa.
MyBean instance = new MyBean();
instance.setId(7L);
PropertyBox value = BeanIntrospector.get().read(instance); (1)
final NumericProperty<Long> ID = NumericProperty.longType("id");
BeanIntrospector.get().write(PropertyBox.builder(ID).set(ID, 8L).build(), instance); (2)
1 | Read the given MyBean instance as a PropertyBox . The PropertyBox property set will be the MyBean class bean property set |
2 | Write a PropertyBox into a MyBean instance. The PropertyBox property values, matched by name, will be written into the corresponding Bean instance properties |
The BeanIntrospector
API is easily extensible regarding the introspection strategy, especially for the Bean properties configuration. The main extension points are represented by the BeanPropertyPostProcessor
and the BeanPropertySetPostProcessor
interfaces, as described below.
4.14.4. BeanPropertyPostProcessor
A BeanPropertyPostProcessor can be used to extend the Bean introspection strategy at Bean properties level, before they will be returned as part of the final BeanPropertySet
.
A BeanPropertyPostProcessor
can be used for example to set property configuration attributes, manage property validators, configure property value converters and so on.
A BeanPropertyPostProcessor
must be registered in the BeanIntrospector
and it is called for every detected and valid Bean property at Bean introspection time. The processBeanProperty
method accepts the current BeanProperty
builder, which can be used to modify the configuration of the Property that will be part of the final BeanPropertySet
.
The registration of a BeanPropertyPostProcessor
can be performed in two ways:
1. Registration using the 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.
BeanIntrospector.get()
.addBeanPropertyPostProcessor((property, cls) -> property.configuration("test", "testValue")); (1)
1 | Register a BeanPropertyPostProcessor which adds a test property configuration attribute to all the 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.
|
4.14.5. BeanPropertySetPostProcessor
A BeanPropertySetPostProcessor can be used to extend the Bean introspection strategy at Bean property set level, before the final BeanPropertySet
is returned.
A BeanPropertySetPostProcessor
can be used for example to modify the BeanPropertySet
configuration.
A BeanPropertySetPostProcessor
must be registered in the BeanIntrospector
and it is called for each Bean class at Bean introspection time. The processBeanPropertySet
method accepts the current BeanPropertySet
builder, which can be used to modify the configuration of Bean property set.
The registration of a BeanPropertySetPostProcessor
can be performed in two ways:
1. Registration using the BeanIntrospector
: The addBeanPropertySetPostProcessor
method can be used to register a BeanPropertySetPostProcessor
.
2. Registration using the standard Java service extensions: BeanPropertySetPostProcessor
registration can be performed also using default Java extension services, providing a com.holonplatform.core.beans.BeanPropertySetPostProcessor file under the META-INF/services
folder containing the fully qualified BeanPropertySetPostProcessor
concrete class names to register.
BeanIntrospector.get()
.addBeanPropertySetPostProcessor((propertySet, cls) -> propertySet.configuration("test", "testValue")); (1)
1 | Register a BeanPropertySetPostProcessor which adds a test property configuration attribute to the Bean property set |
The javax.annotation.Priority annotation can be used on a BeanPropertySetPostProcessor implementation class to assign a priority order within the registered processors list, where lower values corresponds to higher priority.
|
4.14.6. Builtin Bean post processors
The Holon platform makes available a set of builtin Bean post processors, automatically registered in the default BeanIntrospector
implementation.
Most of them supports annotations on Bean property field which can be used to tune the Bean introspection strategy and to manage Bean properties configuration.
See below for a list of all the avaiable annotations. All the listed annotations are located in the com.holonplatform.core.beans
package.
@Ignore
The @Ignore
annotation can be used on Bean property fields to skip the Bean property during the introspection process. The ignored property will not be part of the final Bean property set.
The annotation provides a includeNested()
attribute which can be used to set whether to ignore any nested Bean property (if the Bean property type is itself a Bean class) or not. Defaults to true
, which means that if the ignored property is a Bean class type, also the properties of the nested bean class will be ignored.
class Bean1 {
public static final BeanPropertySet<Bean1> PROPERTIES = BeanPropertySet.create(Bean1.class); (2)
private Long id;
@Ignore (1)
private String name;
// getters and setters omitted
}
1 | Set the name property as ignored |
2 | The Bean property set will not contain the name property |
@Caption
The @Caption
annotation can be used on Bean property fields to provide the property localization attributes, such as the property caption and the property caption localization message code.
See the Property localization section for further details.
class Bean2 {
public static final BeanPropertySet<Bean2> PROPERTIES = BeanPropertySet.create(Bean2.class);
@Caption("Code") (1)
private Long id;
@Caption(value = "Name", messageCode = "name.localization.code") (2)
private String name;
// getters and setters omitted
}
1 | Set the id property default caption message to Code . The property caption can later be read using the getMessage() method of the Localizable interface, a super interface of Property . |
2 | Set the name property caption default message and localization message code |
@Sequence
The @Sequence
annotation can be used on Bean property fields to order the bean properties within the Bean property set. When used as an Iterable
, the Bean property set will return the Bean properties ordered according to the sequence value declared through this annotation.
class Bean3 {
public static final BeanPropertySet<Bean3> PROPERTIES = BeanPropertySet.create(Bean3.class);
@Sequence(10) (1)
private Long id;
@Sequence(20) (2)
private String name;
// getters and setters omitted
}
1 | Set the id property sequence number to 10 |
2 | Set the name property sequence number to 20 : this property will be always returned after the id property when the BeanPropertySet is used as an Iterable |
@Config
The @Config
annotation can be used on Bean property fields to add a property configuration attribute to the annotated property, specifying the configuration key and its value.
Only String
type configuration values are supported by this annotation, use your own BeanPropertyPostProcessor to perform more advanced property configuration setup operations.
The @Config
annotation is a repeteable annotation, so it can be repeated on a Bean property to provide more than one configuration attribute.
class Bean4 {
public static final BeanPropertySet<Bean4> PROPERTIES = BeanPropertySet.create(Bean4.class);
private Long id;
@Config(key = "test1", value = "myValue1") (1)
@Config(key = "test2", value = "myValue2")
private String name;
// getters and setters omitted
}
1 | Set two property configuration attributes using the @Config annotation on the name bean property |
@Converter
The @Converter
annotation can be used on Bean property fields to configure a property value converter for a bean property.
Besides the PropertyValueConverter
class to use, the @Converter
annotation supports the configuration of a builtin property value converter through the builtin()
annotation attribute. The builtin property value converter can be selected among the ones provided by default by the Holon platform.
class Bean5 {
public static final BeanPropertySet<Bean5> PROPERTIES = BeanPropertySet.create(Bean5.class);
@Converter(MyConverter.class) (1)
private Long id;
@Converter(builtin = BUILTIN.NUMERIC_BOOLEAN) (2)
private Boolean value;
// getters and setters omitted
}
1 | Set a custom MyConverter property value converter for the id bean property |
2 | Set a builtin numeric booelan converter for the value bean property |
Validators
The Bean property validation can be configured in one of the following ways:
-
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
. A Bean validation API implementation must be available in classpath. -
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.
|
class Bean6 {
public static final BeanPropertySet<Bean6> PROPERTIES = BeanPropertySet.create(Bean6.class);
@Min(1) (1)
@Max(value = 100, message = "{my.localizable.message}") (2)
private Long id;
@NotBlank (3)
@ValidationMessage(message = "Name must be not blank", messageCode = "my.message.localization.code")
private String name;
@Validator(MyFirstValidator.class) (4)
@Validator(MySecondValidator.class)
private String value;
// getters and setters omitted
}
1 | Set the mininum value for the id bean property using the standard javax.validation.constraints annotation |
2 | Set the maximum value for the id bean property using the standard javax.validation.constraints annotation and providing the invalid value message localization code |
3 | Set the name bean property as not blank using the additional platform validation annotation. The @ValidationMessage annotation is used to specify the invalid value default message and localization message code |
4 | Set two custom Validator implementations for the value bean property using the @Validator repeteable annotation |
@DataPath
The @DataPath
annotation can be used on Bean class and on Bean property fields to declare a data path mapping as Bean property or property set configuration attributes, using the default PATH
configuration property of the DataMappable interface.
The data path mapping can be used to declare the data model attribute path when it is not the same as the Bean class name or Bean property name, and the Bean property set is used for persistence related operations.
The data path mapping must be explicitly supported by the data model handler API which will be used, that could be for example a Datastore.
See the specific Datastore implementation documentation to check the data path mapping support and the actual meaning it assumes.
@DataPath("myPath") (2)
class Bean7 {
public static final BeanPropertySet<Bean7> PROPERTIES = BeanPropertySet.create(Bean7.class);
@DataPath("code") (1)
private Long id;
private String name;
// getters and setters omitted
}
1 | Set the data path mapping to code for the id bean property |
2 | Set the Bean class data path mapping to myPath |
4.14.7. BeanIntrospector cache
The default BeanIntrospector
implementation uses an internal cache of processed bean class and property sets, to boost introspecton operations and obtain better performaces.
If memory consuption issues are detected, the internal cache can be disabled setting the 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.
4.15. Datastore
The Datastore API is the main entry point to manage data access and persistence in a technology/platform/vendor independent way.
The Datastore
data management strategy relies on the Holon platform property model architecture to represent and manage data model attributes in a generic and implementation-independent way, using the Managing property values using a PropertyBox structure as data interchange carrier between the Datastore
API and the concrete data model.
A concrete Datastore
implementation could provide a more specialized API, with functionalities expressly related to the specific persistence technology/model.
We’ll refer to an entity in this documentation as a generic persistence model data container. An entity may be for example a table in a RDBMS context, a JPA entity class in a JPA environment or a document in a document based data store.
The Datastore
API provides the following operations:
-
Refresh: Refresh the data of a data model entity, retrieving the most updated version.
-
Insert: Insert a new data model entity into the persistence store.
-
Update: Update a data model entity already present in the persistence store.
-
Save: Insert or update a data model entity, depending on the existence of the entity itself in the persistence store.
-
Bulk operations definition and execution (
bulkInsert
,bulkUpdate
andbulkDelete
): to execute batch operations in the persistence store. -
Query definition and execution: to configure and execute queries against the persistence store, allowing to declare query results restrictions, aggregations and sorting and to obtain the query results using different projections.
Each operation which involves a possible persistence store data modification returns an OperationResult
type object, which provides information about the operation outcome, such as 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.
4.15.1. Expressions and resolvers
The Datastore
API architecture is designed on top of the Holon platform Expression based architecture.
An Expression
is a very abstract and generic representation of an element of a language. The Datastore
API uses expressions to "translate" a meta-language into the actual language which can be understood by the concrete persistense context engine to which the Datastore
is bound.
The ExpressionResolver interface is the key element to perform the language manipulation and translation, since it is used to resolve 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 expression: this way, the resultion process must proceed to the next available resolver for given expression and resolution type.
A generic ResolutionContext
object is provided to the ExpressionResolver
resolution method, to provide information about the current resolution context.
A set of ExpressionResolver
can be handled using an ExpressionResolverRegistry. When an Expression
must be resolved, all the available resolvers which declare to resolve the given expression type and provide a consistent resolution type will be taken into account. These resolvers are invoked sequentially,
returning the first valid resolved expression, if any.
To order the expression resolvers with the same expression types, the javax.annotation.Priority
annotation can be used on the ExpressionResolver
implementation class to assign a priority order, where lower values corresponds to higher priority.
The Datastore
API , extending the ExpressionResolverSupport
interface, supports the registration of new ExpressionResolver
instances. This is the recommended way to implement Datastore
extensions and persistence operations customization.
Since a Datastore
implementation can be bound to very different data models and persistence context, each expression resolution strategy is highly dependent from the concrete Datastore
implementation, including a possibly specialized version of the ResolutionContext
. See each concrete Datastore
implementation documentation to learn about the ExpressionResolver
support and the available expression types which can be used for extensibility purposes.
Anyway, the core meta-language expressions set is common to any Datastore
implementation. For this reason, it is possible to provide Datastore
API extensions in a general and implementation independent way using the ExpressionResolver
based strategy when the expression resolution is bound to the standard meta-language expressions.
Most of the standard Datastore
operations (for example the Query operation) support ExpressionResolver
registration, to provide expression resolution manipulation only for a specific operation execution.
See the Datastore API extensions section for details.
4.15.2. Property data types
The Datastore
API fully supports property values conversions using the standard property value converter API. 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, to use custom types or to face common value conversion needs, such as enumeration property types mapped to integer or text data model types.
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
, and LocalDateTime
.
When a java.util.Date
or java.util.Calendar
property type is used with Datastore
API operations, it is recommended to configure the actual temporal type which is expected for the property, to ensure data consistency. This can be done through the property configuration.
4.15.3. DataTarget
The DataTarget interface is used by the Datastore
API and the Query definition API 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, an 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 specialized DataTarget
object types to identify a data model entity which is specific of the persistence model to which the Datastore
is bound.
The DataTarget
interface provides static methods to create data targets using the default name representation, optionally providing the DataTarget
(Path
) type:
DataTarget<String> target1 = DataTarget.named("test1"); (1)
DataTarget<MyType> target2 = DataTarget.of("test2", MyType.class); (2)
1 | Create a default String type DataTarget named test1 |
2 | Create a MyType type DataTarget named test2 |
All the Datastore
operations definition APIs involving persistent entity structures use a DataTarget
to identify the data model entity to which the operation refers.
4.15.4. Data manipulation operations
The Datastore API provides the most common data manipulation operations, listed in the table below.
Each operation throws a DataAccessException if an error occurs during the operation execution.
Each operation (except for refresh) support 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 each specific Datastore
documentation for further information.
The DefaultWriteOption enumeration provides write options which can be available for any Datastore
API. 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 PropertyBox
property set.
Check specific Datastore implementations documentation to learn if this option is actually supported.
|
Operation | Purpose | Return |
---|---|---|
|
Refresh the values of the properties of given |
The refreshed |
|
Insert a new data model entity, identified by given data target and represented by given |
The |
|
Update an existing data model entity, identified by given data target and represented by given |
The |
|
Insert a new data model entity (identified by given data target and represented by given |
The |
|
Remove a data model entity, identified by given data target and represented by given |
The |
|
Configure and perform a bulk insert of data model entities identified by given data target and represented by |
The |
|
Configure and perform a bulk update of data model entities identified by given data target, to change a set of property values according to a set of restriction predicates to identify the set of data model entities to update. |
The |
|
Configure and perform a bulk delete of data model entities identified by given data target, providing a set of restriction predicates to identify the set of data model entities to remove. |
The |
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 specified DataTarget (insert a new entity if not present in the persistence store or update it if exists) |
2 | Insert the given PropertyBox data into the persistence store using the specified DataTarget |
3 | Update the given PropertyBox data into the persistence store using the specified DataTarget |
4 | Refresh the PropertyBox property values using the specified DataTarget |
5 | Remove the entity which corresponds to given PropertyBox |
6 | Execute a bulk insert operation using the specified DataTarget , inserting given PropertyBox elements |
7 | Execute a bulk update operation using the specified DataTarget , setting the property value to updated when the property value is null |
8 | Execute a bulk delete operation using the specified DataTarget , removing entities for which the given property value is null |
4.16. Query
The Query API can be used to configure and execute queries against the persistence data store.
Just like any other Datastore
API operation, the Query
API relies on the Holon platform property model to represent the data model attributes and to obtain the query results, using a PropertyBox to provide a set of property values.
This allows the query to be declared and executed in an abstract and implementation-independent way.
The Query
API supports the following clauses and configuration attributes:
-
The DataTarget on which the query has to be performed.
-
A set of query restrictions, expressed as QueryFilter clauses.
-
The query results sorting declarations, expressed as QuerySort clauses.
-
The query results aggregation, expressed as QueryAggregation clauses.
-
The query results paging, to configure the query result set limit and offset.
-
A set of generic query configuration parameters.
Below are described the standard query Expressions made available by the Holon platform for the defintion of a query.
4.16.1. QueryFilter
The QueryFilter interface represents a query results restriction Expression
.
A QueryFilter
acts on other expressions which represent the restriction subject and conditions. Such expressions are typically a TypedExpression, i.e an Expression
with explicitly declared type.
The most common restriction predicates representations are provided by the core platform classes. The following predicates are available:
-
Is null / is not null: An expression is null / not null.
-
equal / not equal: An expression is equal / not equal to another expression.
-
less than / less than or equal: An expression is less than / less than or equal to another expression.
-
greater than / greater than or equal: An expression is greater than / greater than or equal to another expression.
-
between: The value of an expression is included between a minimum and a maximum value.
-
in / not in: The value of an expression is included / not included in a set of values.
For String
type expressions:
-
contains: The value of a
String
type expression contains a specified text (ignoring case or not). -
startsWirth: The value of a
String
type expression contains starts with a specified text (ignoring case or not). -
endsWith: The value of a
String
type expression contains ends with a specified text (ignoring case or not).
Furthermore, the QueryFilter
predicates can be composed using logical operations:
-
not: Negation of a
QueryFilter
predicate. -
and: Conjunction of
QueryFilter
predicates. -
or: Disjunction of
QueryFilter
predicates.
The QueryFilter
predicates can be obtained in two ways:
1. Using the static builder methods provided by the QueryFilter interface.
final PathProperty<Integer> PROPERTY1 = PathProperty.create("test1", Integer.class);
final PathProperty<Integer> PROPERTY2 = PathProperty.create("test2", Integer.class);
final StringProperty STRING_PROPERTY = StringProperty.create("test3");
(1)
QueryFilter restriction = QueryFilter.isNotNull(PROPERTY1); // is not null
restriction = QueryFilter.isNull(PROPERTY1); // is null
restriction = QueryFilter.eq(PROPERTY1, 7); // equal to a value
restriction = QueryFilter.eq(PROPERTY1, PROPERTY2); // equal to another property expression
restriction = QueryFilter.neq(PROPERTY1, 7); // not equal
restriction = QueryFilter.lt(PROPERTY1, 7); // less than
restriction = QueryFilter.loe(PROPERTY1, 7); // less than or equal
restriction = QueryFilter.gt(PROPERTY1, 7); // greater than
restriction = QueryFilter.goe(PROPERTY1, 7); // greater than or equal
restriction = QueryFilter.between(PROPERTY1, 1, 7); // between
restriction = QueryFilter.in(PROPERTY1, 1, 2, 3); // in
restriction = QueryFilter.nin(PROPERTY1, 1, 2, 3); // not in
(2)
restriction = QueryFilter.startsWith(STRING_PROPERTY, "V", false); // starts with 'v'
restriction = QueryFilter.startsWith(STRING_PROPERTY, "v", true); // starts with 'v', ignoring case
restriction = QueryFilter.endsWith(STRING_PROPERTY, "v", false); // ends with 'v'
restriction = QueryFilter.contains(STRING_PROPERTY, "v", false); // contains 'v'
QueryFilter restriction2 = QueryFilter.contains(STRING_PROPERTY, "v", true); // contains 'v', ignoring case
// negation (3)
QueryFilter negation = restriction.not();
negation = QueryFilter.not(restriction);
// conjuction (4)
QueryFilter conjuction = restriction.and(restriction2);
conjuction = QueryFilter.allOf(restriction, restriction2).orElse(null);
// disjunction (5)
QueryFilter disjunction = restriction.or(restriction2);
disjunction = QueryFilter.anyOf(restriction, restriction2).orElse(null);
1 | Common restriction predicates using a PathProperty as expression |
2 | String type expression restriction predicates |
3 | Negation using either the not() QueryFilter method or the not(QueryFilter filter) builder method |
4 | Conjunction (AND) using either the and() QueryFilter method or the allOf(QueryFilter… filters) builder method |
5 | Disjunction (OR) using either the or() QueryFilter method or the anyOf(QueryFilter filter) builder method |
2. Using the convenience methods provided by the QueryExpression interface. The QueryExpression
API is implemented, for example, by the PathProperty
interface.
The QueryExpression
API makes available a set of methods to create a QueryFilter
using the expression itself as the subject of the restriction predicate.
For restrictions which refers to a specific data type, specialized QueryExpression
extensions should be used. For example, StringQueryExpression interface makes available methods to obtain String type restrictions, such a contains, startsWith, endsWith.
The type specific query expression APIs are implemented by each PathProperty sub type: the StringQueryExpression
API is implemented by the StringProperty
type and so on.
final PathProperty<Integer> PROPERTY1 = PathProperty.create("test1", Integer.class);
final PathProperty<Integer> PROPERTY2 = PathProperty.create("test2", Integer.class);
final StringProperty STRING_PROPERTY = StringProperty.create("test3");
(1)
QueryFilter restriction = PROPERTY1.isNotNull(); // is not null
restriction = PROPERTY1.isNull(); // is null
restriction = PROPERTY1.eq(7); // equal to a value
restriction = PROPERTY1.eq(PROPERTY2); // equal to another property
restriction = PROPERTY1.neq(7); // not equal
restriction = PROPERTY1.lt(7); // less than
restriction = PROPERTY1.loe(7); // less than or equal
restriction = PROPERTY1.gt(7); // greater than
restriction = PROPERTY1.goe(7); // greater than or equal
restriction = PROPERTY1.between(1, 7); // between
restriction = PROPERTY1.in(1, 2, 3); // in
restriction = PROPERTY1.nin(1, 2, 3); // not in
(2)
restriction = STRING_PROPERTY.startsWith("v"); // starts with
restriction = STRING_PROPERTY.startsWithIgnoreCase("v"); // starts with ignoring case
restriction = STRING_PROPERTY.endsWith("v"); // ends with
restriction = STRING_PROPERTY.endsWithIgnoreCase("v"); // ends with ignoring case
restriction = STRING_PROPERTY.contains("v"); // contains
QueryFilter restriction2 = STRING_PROPERTY.containsIgnoreCase("v"); // contains ignoring case
(3)
QueryFilter negation = PROPERTY1.eq(7).not(); // negation
QueryFilter conjuction = PROPERTY1.isNotNull().and(PROPERTY2.eq(3)); // conjuction
QueryFilter disjunction = PROPERTY1.isNull().or(PROPERTY2.eq(3)); // disjunction
1 | Common restriction predicates using the QueryExpression API implemented by PathProperty |
2 | String type restriction predicates using the StringQueryExpression API implemented by StringProperty |
3 | Logical operations |
4.16.2. QuerySort
The QuerySort interface represents a query results sorting directive.
A QuerySort
acts on a generic Path
expression to declare the sorting subject and uses the SortDirection
enumeration to declare the sort direction (ascending or descending). Query sorts can be composed to declare an ordered list of sort declarations.
A QuerySort
declaration can be obtained in two ways:
1. Using the static builder methods provided by the QuerySort interface.
The PathExpression
API makes available convenience methods to create a QuerySort
using the expression itself (i.e. the Path
represented by the expression) as the subject of the sort declaration.
final PathProperty<String> PROPERTY = PathProperty.create("test", String.class);
final PathProperty<String> ANOTHER_PROPERTY = PathProperty.create("another", String.class);
(1)
QuerySort sort = QuerySort.of(PROPERTY, SortDirection.ASCENDING); // sort ASCENDING on given property path
sort = QuerySort.of(PROPERTY, true); // sort ASCENDING on given property path
sort = QuerySort.asc(PROPERTY); // sort ASCENDING on given property path
QuerySort sort2 = QuerySort.desc(ANOTHER_PROPERTY); // sort DESCENDING on given property path
(2)
QuerySort.of(sort, sort2); // sort using 'sort' and 'sort2' declarations, in the given order
1 | Sort declarations specifying the sort direction |
2 | Query sorts composition |
2. Using the convenience methods provided by the PathExpression interface. The PathExpression
API is implemented, for example, by the PathProperty
interface.
The PathExpression
API makes available convenience methods to create a QuerySort
using the expression itself (i.e. the Path
represented by the expression) as the subject of the sort declaration.
final PathProperty<String> PROPERTY = PathProperty.create("test", String.class);
final PathProperty<String> ANOTHER_PROPERTY = PathProperty.create("another", String.class);
(1)
QuerySort sortAsc = PROPERTY.asc(); // sort ASCENDING on given property
QuerySort sortDesc = PROPERTY.desc(); // sort DESCENDING on given property
(2)
PROPERTY.asc().and(ANOTHER_PROPERTY.desc()); // sort ASCENDING on PROPERTY, than sort DESCENDING on
// ANOTHER_PROPERTY
1 | Use the asc() and desc() PathExpression methods to create ascending and descending sorts |
2 | The and(QuerySort sort) method of the QuerySort interface can be used to compose a list of sorts |
4.16.3. QueryFunction
The QueryFunction interface represents a function expression.
A QueryFunction
may accept a list of arguments, expressed as expressions themself. The TypedExpression
type is used as query function arguments type, since a function could only be applicable to a specific argument type.
A set of common query functions is provided by the Holon platform, each represented by a specific QueryFunction
sub type.
Common query functions can be obtained either using the QueryFunction
static builder methods or using the convenience methods provided by APIs like QueryExpression. In this second case, the expression itself is used as the query function argument.
Aggregation functions
-
Count: Aggregation function to count the number of available elements (for example the number of query results). Represented by the Count interface. Always returns a
Long
type result. -
Min: Aggregation function to obtain the smallest value within a set of available elements. Represented by the Min interface.
-
Max: Aggregation function to obtain the largest value within a set of available elements. Represented by the Max interface.
-
Avg: Aggregation function to obtain the average value within a set of numeric elements. Represented by the Avg interface. Always returns a
Double
type result. -
Sum: Aggregation function to sum the values of a set of available elements. Represented by the Sum interface.
For agrregation functions which refers to a specific data type, specialized QueryExpression
extensions should be used. For example, the NumericQueryExpression interface makes available methods to obtain numeric type restrictions, such a avg and sum.
The type specific query expression APIs are implemented by each PathProperty sub type: the NumericQueryExpression
API is implemented by the NumericProperty
type and so on.
final NumericProperty<Integer> PROPERTY = NumericProperty.integerType("test");
(1)
Count count = QueryFunction.count(PROPERTY);
Min<Integer> min = QueryFunction.min(PROPERTY);
Max<Integer> max = QueryFunction.max(PROPERTY);
Avg avg = QueryFunction.avg(PROPERTY);
Sum<Integer> sum = QueryFunction.sum(PROPERTY);
(2)
count = Count.create(PROPERTY);
min = Min.create(PROPERTY);
max = Max.create(PROPERTY);
avg = Avg.create(PROPERTY);
sum = Sum.create(PROPERTY);
(3)
count = PROPERTY.count();
min = PROPERTY.min();
max = PROPERTY.max();
avg = PROPERTY.avg();
sum = PROPERTY.sum();
1 | Aggregation functions created using the QueryFunction builder methods |
2 | Aggregation functions created using each function interface creation method |
3 | Aggregation functions obtained using the QueryExpression convenience methods |
String related functions
For String
type expressions, two standard query functions are made available by the Holon platform:
final StringProperty PROPERTY = StringProperty.create("test");
(1)
Lower lower = QueryFunction.lower(PROPERTY);
Upper upper = QueryFunction.upper(PROPERTY);
(2)
lower = Lower.create(PROPERTY);
upper = Upper.create(PROPERTY);
(3)
lower = PROPERTY.lower();
upper = PROPERTY.upper();
1 | Lower/upper functions created using the QueryFunction builder methods |
2 | Aggregation functions created using each function interface creation method |
3 | Lower/upper functions obtained using the StringQueryExpression convenience methods |
Temporal functions
For temporal data types, a set of common functions are made available by the Holon platform.
Functions to obtain the current date or timestamp:
-
CurrentDate: function to obtain the current date. Represented by the CurrentDate interface.
-
CurrentLocalDate: function to obtain the current date as a
LocalDate
. Represented by the CurrentLocalDate interface. -
CurrentTimestamp: function to obtain the current timestamp. Represented by the CurrentTimestamp interface.
-
CurrentLocalDateTime: function to obtain the current timestamp as a
LocalDateTime
. Represented by the CurrentLocalDateTime interface.
(1)
CurrentDate currentDate = QueryFunction.currentDate();
CurrentLocalDate currentLocalDate = QueryFunction.currentLocalDate();
CurrentTimestamp currentTimestamp = QueryFunction.currentTimestamp();
CurrentLocalDateTime currentLocalDateTime = QueryFunction.currentLocalDateTime();
(2)
currentDate = CurrentDate.create();
currentLocalDate = CurrentLocalDate.create();
currentTimestamp = CurrentTimestamp.create();
currentLocalDateTime = CurrentLocalDateTime.create();
1 | Current date/time functions created using the QueryFunction builder methods |
2 | Current date/time functions created using each function interface creation method |
Functions to extract a temporal part. All the listed functions returns an Integer
type result:
-
Year: function extract the year part of a temporal data type. Represented by the Year interface.
-
Month: function extract the month part of a temporal data type. Represented by the Month interface. The month range index is between 1 and 12.
-
Day: function extract the day part of a temporal data type. Represented by the Day interface. The day is intended as the day of month and the day range index is between 1 and 31.
-
Hour: function extract the hour part of a temporal data type. Represented by the Hour interface. The 24-hour clock is used and the hour range index is between 0 and 23.
final TemporalProperty<LocalDateTime> PROPERTY = TemporalProperty.localDateTime("test");
(1)
Year year = QueryFunction.year(PROPERTY);
Month month = QueryFunction.month(PROPERTY);
Day day = QueryFunction.day(PROPERTY);
Hour hour = QueryFunction.hour(PROPERTY);
(2)
year = Year.create(PROPERTY);
month = Month.create(PROPERTY);
day = Day.create(PROPERTY);
hour = Hour.create(PROPERTY);
(3)
year = PROPERTY.year();
month = PROPERTY.month();
day = PROPERTY.day();
hour = PROPERTY.hour();
1 | Temporal part extraction functions created using the QueryFunction builder methods |
2 | Temporal part extraction functions functions created using each function interface creation method |
3 | Temporal part extraction functions obtained using the TemporalQueryExpression convenience methods |
4.16.4. QueryAggregation
The QueryAggregation interface represents a query results aggregation expression.
The QueryAggregation
API allows to specify:
-
The paths (using the
Path
type) to be used to aggregate the query results, i.e. to group the results by the values of the specified paths. -
The optional restrictions to apply on the aggregation path values, expressed by using 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.
|
The QueryAggregation
API provides a buider to create and configure a query aggregation expression.
final PathProperty<Integer> PROPERTY = PathProperty.create("test", Integer.class);
final PathProperty<String> ANOTHER_PROPERTY = PathProperty.create("another", String.class);
QueryAggregation aggregation = QueryAggregation.builder() (1)
.path(PROPERTY) (2)
.path(ANOTHER_PROPERTY) (3)
.filter(PROPERTY.isNotNull()) (4)
.build();
1 | Obtain a QueryAggregation builder |
2 | Declare an aggregation path using the PROPERTY PathProperty |
3 | Add another aggregation path using the ANOTHER_PROPERTY PathProperty |
4 | Configure an aggregation restrinction filter |
4.16.5. Query definition
A Query
can be defined and configured using the QueryBuilder interface.
The QueryBuilder
provides methods to configure the query using the query expressions listed above. Furthermore, it provides methods to declare:
-
The query target, which represents the data model entity to be queried and is expressed through a DataTarget.
-
The optional query results pagination declaration, which can be declared using:
-
limit:: the query results limit, i.e. the max number of results to obtain.
-
offset:: the 0-based offset from which to fetch the query results within the total results set.
-
-
Optional query parameters, mainly used to for extension purposes.
The Query API extends QueryBuilder
, and can be obtained from a Datastore
using the query()
method.
final PathProperty<Integer> PROPERTY = PathProperty.create("test", Integer.class);
Datastore datastore = getDatastore(); // build or obtain a Datastore
Query query = datastore.query() (1)
.target(DataTarget.named("testTarget")) (2)
.filter(PROPERTY.gt(10)) (3)
.sort(PROPERTY.asc()) (4)
.aggregate(PROPERTY) (5)
.limit(100) (6)
.offset(200); (7)
query = datastore.query(DataTarget.named("testTarget")) (8)
.aggregate(QueryAggregation.builder().path(PROPERTY).filter(PROPERTY.gt(10)).build()) (9)
.restrict(100, 200); (10)
1 | Obtain a query builder |
2 | Set the query target using DataTarget |
3 | Add a query restriction filter using QueryFilter |
4 | Add a query sort declaration using QuerySort |
5 | Declare a query result aggregation path |
6 | Set the query results limit to 100 |
7 | Set the query results offset to 200 |
8 | Obtain a query builder and simultaneously set the query target |
9 | Declare a query results aggregation clause using the QueryAggregation builder |
10 | Set the the query pagination using the convenience restrict method, which accepts the query results limit and offset |
4.16.6. Query projection and execution
To obtain the Query
results, a query results projection must be declared. A projection is represented by the QueryProjection interface and is used to declare which data model attribute values are to be returned and which type has to used to represent the query results.
The query results projection is provided at query execution time, using the QueryResults API, which is implemented by the Query
interface.
The main QueryResults
API method to obtain the query results is stream(QueryProjection<R> projection)
, which can be used to provide a QueryProjection
and get the query execution result, which will be of the same type of the query projection type.
In addition to the default stream method, a set of other convenience methods are provided by the QueryResults
API for query execution and results retrieval:
-
list(QueryProjection<R> projection)
: To obtain the query results stream as aList
. -
findOne(QueryProjection<R> projection)
: To obtain a result which is expected to be unique, if it is available. The query result is provided as anOptional
and if more than one result is obtained from query execution, aQueryNonUniqueResultException
is thrown. -
count()
: To count all the query results, returing the number of results as along
.
Furthermore, a set of convenience methods are provided to use the Holon platform properties abstration as query projection and to obtain the query results using the PropertyBox type:
-
stream(Iterable<P> properties)
andstream(Property… properties)
: Allows to provide one or moreProperty
as query projection and obtain the query results as a stream ofPropertyBox
. The list version is also available to obtain the query results as aList
instead of aStream
. -
findOne(Iterable<P> properties)
andfindOne(Property… properties)
: Allows to provide one or moreProperty
as query projection and obtain a query result which is expected to be unique as aPropertyBox
. The query result is provided as anOptional
and if more than one result is obtained from query execution, aQueryNonUniqueResultException
is thrown.
final NumericProperty<Integer> PROPERTY1 = NumericProperty.integerType("test1");
final StringProperty PROPERTY2 = StringProperty.create("test2");
final PropertySet<?> PROPERTIES = PropertySet.of(PROPERTY1, PROPERTY2);
final DataTarget<?> TARGET = DataTarget.named("testTarget");
Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation
long count = datastore.query().target(TARGET).count(); (1)
Stream<Integer> values = datastore.query(TARGET).stream(PROPERTY1); (2)
Optional<Integer> value = datastore.query(TARGET).findOne(PROPERTY1); (3)
Stream<PropertyBox> results = datastore.query(TARGET).stream(PROPERTY1, PROPERTY2); (4)
results = datastore.query(TARGET).stream(PROPERTIES); (5)
List<PropertyBox> list = datastore.query(TARGET).list(PROPERTY1, PROPERTY2); (6)
Optional<PropertyBox> result = datastore.query(TARGET).findOne(PROPERTY1, PROPERTY2); (7)
1 | Count the query results |
2 | Use the PROPERTY1 as query projection and obtain the query results as a Stream of values of the property value type (Integer ) |
3 | Use the PROPERTY1 as query projection expecting a unique result, obtaining the query result as an Optional value of the property value type (Integer ) |
4 | When more than one Property is provided as query projection, the query results are obtained as a Stream of PropertyBox instances |
5 | A PropertySet can be used to provide a multiple Property query projection |
6 | The same operation can be performed with the list method, obtaining the query results as a List |
7 | PROPERTY1 and PROPERTY2 are provided as query projection and a unique result is expected: the query result is obtained as an Optional PropertyBox instance |
Builtin query projections
The Holon platform core module provides some builtin QueryProjection
types which can be used for query execution.
As seen in the previous section, a PathProperty
is a QueryProjection
itself, and can be directly used as query projection. When the query projection must include more than one property, the PropertySetProjection type can be used. Anyway, is easier to use the appropriate QueryResults
API methods to directly provide a set of properties or a PropertySet
as query projection.
The other builtin query projections are:
1. QueryFunction:
A QueryFunction can be directly used as query projection. The projection result type will be the same as the QueryFunction
result type.
final NumericProperty<Integer> PROPERTY1 = NumericProperty.integerType("test");
final StringProperty PROPERTY2 = StringProperty.create("test2");
final DataTarget<?> TARGET = DataTarget.named("testTarget");
Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation
Optional<Integer> sum = datastore.query(TARGET).findOne(PROPERTY1.sum()); (1)
Stream<String> results = datastore.query(TARGET).stream(PROPERTY2.upper()); (2)
1 | Use the Sum function on PROPERTY1 as query projection |
2 | Use the Upper function on PROPERTY2 to obtain a stream of String values applying the uppercase transformation |
2. Constant expression:
The ConstantExpressionProjection type can be used to declare a costant expression value as query projection.
final NumericProperty<Integer> PROPERTY1 = NumericProperty.integerType("test");
final StringProperty PROPERTY2 = StringProperty.create("test2");
final DataTarget<?> TARGET = DataTarget.named("testTarget");
Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation
Optional<Integer> sum = datastore.query(TARGET).findOne(PROPERTY1.sum()); (1)
Stream<String> results = datastore.query(TARGET).stream(PROPERTY2.upper()); (2)
1 | Use the constant TEST value as query projection |
3. Bean projection:
The BeanProjection interface can be used to obtain the query results as Java Bean class instances, providing the bean class to be used.
Optionally, the projection Path
names can be specified to control the bean property set which as to be obtained as query result. If not specified, all the bean definition class properties will be used as query projection paths.
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() {
final DataTarget<?> TARGET = DataTarget.named("testTarget");
Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation
Stream<MyBean> results = datastore.query(TARGET).stream(BeanProjection.of(MyBean.class)); (1)
Optional<MyBean> result = datastore.query(TARGET).findOne(BeanProjection.of(MyBean.class)); (2)
final BeanPropertySet<MyBean> PROPERTIES = BeanPropertySet.create(MyBean.class);
results = datastore.query(TARGET).stream(BeanProjection.of(MyBean.class, PROPERTIES.property("code"))); (3)
}
1 | Use MyBean class as query projection and obtain a Stream of MyBean instances as query results |
2 | The same operation but expecting a unique result, which will be returned as an Optional MyBean instance |
3 | Use MyBean class as query projection and specify the query projection paths. In this example, only the code bean property is declared as projection paths, so only the code property values will be retrieved from query execution and setted in the MyBean result instances |
4.16.7. Configuration
The DatastoreConfigProperties interface represents and provides the available configuration properties which can be used to configure a generic Datastore
instance.
The interface extends the default ConfigPropertySet
API, bound to the property name prefix holon.datastore.
The available configuration properties are listed below:
Name | Type | Meaning |
---|---|---|
holon.datastore. trace |
Boolean ( |
Enable/disable |
holon.datastore. dialect |
String |
The fully qualified class name of the dialect to be used by the |
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 Datastores 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 from 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();
4.16.8. Relational Datastores
When a Datastore
implementation refers to a relational persistence data model, some additional expressions are provided to use 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 methods of the SubQuery interface can be used.
Since SubQuery
extends QueryBuilder, a sub query cna be configured (setting the query target, restrictions, sorting and so on) the same way as a standard Query
.
The SubQuery
projection is provided to the create(…)
builder methods at sub query definition time.
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().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, since the 1
literal value is used by default as projection.
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().target(TARGET2).filter(PROPERTY2.eq(PROPERTY1)).exists()).stream(PROPERTY2); (1)
results = datastore.query().target(TARGET1)
.filter(SubQuery.create().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 declare and alias and to configure 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 joinedDataTarget
; -
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 joinedDataTarget
); -
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 sourceDataTarget
);
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 from 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 |
4.16.9. Transactional Datastores
If a Datastore
implementation supports transactions, the Transactional API can be used to manage the transactions at a higher level, in an abstract and implementation-independent way.
The Transactional
API makes available method to execute a Datastore
operation within a transaction, taking care of the transaction lifecycle.
The TransactionalOperation functional interface has to used to perform actual operation execution and to handle the current transaction, represented by a Transaction reference, for example to perform transaction commit or rollback.
The Datastore
API provides a isTransactional()
method which can be used to check if the concrete Datastore
implementation supports transaction and to obtain it as a Transactional
API reference. The requireTransactional()
method has the same meaning, but throws an exception if the concrete Datastore
implementation does not support transactions.
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
datastore.isTransactional().ifPresent(transactional -> { (1)
OperationResult result = transactional.withTransaction(tx -> { (2)
OperationResult r = datastore.insert(TARGET,
PropertyBox.builder(A_PROPERTY).set(A_PROPERTY, "test").build()); (3)
tx.commit(); (4)
return r;
});
});
1 | Check if Datastore is transactional: if so, obtain the Transactional API reference |
2 | Execute on operation within a transaction and return a OperationResult type result |
3 | Execute the actual operation |
4 | Commit the transaction |
When a return value is not needed, the TransactionalInvocation
interface can be used instead of the standard TransactionalOperation
one.
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
datastore.requireTransactional() (1)
.withTransaction(tx -> { (2)
datastore.insert(TARGET, PropertyBox.builder(A_PROPERTY).set(A_PROPERTY, "test").build()); (3)
tx.commit(); (4)
});
1 | Require the Datastore to be transactional and obtain the Transactional API reference |
2 | Execute on operation within a transaction which do not returns any result |
3 | Execute the actual operation |
4 | Commit the transaction |
The Transaction interface allows also to set the transaction as rollback only, so that the only possible outcome of the transaction is for the transaction to be rolled back.
getDatastore().requireTransactional().withTransaction(tx -> {
tx.setRollbackOnly(); (1)
});
1 | Set the transaction as rollback only |
The TransactionConfiguration API can be used to configure the transaction.
The transaction configuration options are:
-
Set the auto-commit mode: whether the transaction must be committed when a transactional operation ends and no error occurred. Default is
false
. -
Set the rollback on error mode: whether the transaction must be rolled back when an exception is thrown during a transactional operation execution. Default is
true
. -
Configure the transaction isolation level, if supported by the concrete
Datastore
implementation
getDatastore().requireTransactional().withTransaction(tx -> {
// ...
}, TransactionConfiguration.withAutoCommit()); (1)
1 | Configure the transaction enabling the auto-commit mode |
getDatastore().requireTransactional().withTransaction(tx -> {
// ...
}, TransactionConfiguration.create(false, false)); (1)
1 | Configure the transaction disabling the rollback on error behaviour and the auto-commit mode |
4.16.10. Datastore API extensions
Datastores provides two main entry points for extension purposes:
-
Use the
Expression
based architecture, through the ExpressionResolver interface, to provide custom expressions and the expression resolution logic which is required to resolve such expressions in a form that theDatastore
is able to understand. -
Provide additional
Datastore
operations and functionalities relying on the DatastoreCommodity concept, through the registration of a DatastoreCommodityFactory.
Extend the Datastore API using ExpressionResolver
The Datastore
API supports ExpressionResolver
registration, to add new expression resolution strategies and to handle new Expression
types.
In a general sense, any new expression type should be resolved in an expression type that the Datastore
is able to understand.
Each concrete Datastore
implementation could provide additional expression types and specific expression resolution capabilities. See each Datastore
implementation documentation to learn about any additional extension capability which could be provide by a specific Datastore
implementation.
final static PathProperty<String> SOME_PROPERTY = PathProperty.create("test", String.class);
class MyExpression implements QuerySort { (1)
@Override
public void validate() throws InvalidExpressionException {
}
}
public void resolver() {
ExpressionResolver<MyExpression, QuerySort> resolver = ExpressionResolver.create(MyExpression.class, (2)
QuerySort.class, (expression, context) -> {
return Optional.of(QuerySort.asc(SOME_PROPERTY));
});
Datastore datastore = getDatastore(); // build or obtain a concrete Datastore implementation
datastore.addExpressionResolver(resolver); (3)
datastore.query().target(DataTarget.named("test")).sort(new MyExpression()).stream(SOME_PROPERTY); (4)
}
1 | Create a custom QuerySort expression class |
2 | Create an ExpressionResolver to resolve the MyExpression type into a standard QuerySort type |
3 | Register the ExpressionResolver in the Datastore |
4 | Use the MyExpression type as any other QuerySort expression |
For the most common query expressions, convenience ExpressionResolver
types are provided to easily create expression resolution extensions. See below for the available core ExpressionResolver
extensions.
DataTargetResolver
The DataTargetResolver is a convenience ExpressionResolver
extension to resolve DataTarget type expressions.
Tipically, a DataTargetResolver
can be defined to resolve a DataTarget
with a symbolic name into a specific Datastore data target.
ExpressionResolver resolver = DataTargetResolver.create(DataTarget.class,
(target, context) -> "test".equals(target.getName())
? Optional.of(DataTarget.named("wellKnownTargetName"))
: Optional.empty()); (1)
1 | Create a resolver which translates the symbolic test data target name into another named target with the wellKnownTargetName name |
QueryFilterResolver
The QueryFilterResolver interface is a convenience ExpressionResolver
extension to resolve QueryFilter type expressions.
A typical custom QueryFilter
expression definition process takes place with the following steps:
-
First af all, you have to define your custom filter representation, providing a class which implements the
QueryFilter
interface (and, optionally, an interface which extendsQueryFilter
and represents your custom filter API); -
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 aQueryFilter
that can be handled by the concreteDatastore
. -
Finally, register the
QueryFilterResolver
in theDatastore
instance, using theaddExpressionResolver(…)
method.
When the resolver is registered, the custom filter can be used as any another QueryFilter
implementation.
class MyFilter implements QueryFilter { (1)
final StringProperty property;
final String value;
public MyFilter(StringProperty 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 StringProperty PROPERTY = StringProperty.create("testProperty");
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 |
QuerySortResolver
The QuerySortResolver interface is a convenience ExpressionResolver
extension to resolve QuerySort type expressions.
A typical custom QuerySort
expression definition process takes place with the following steps:
-
First af all, you have to define your custom sort representation, providing a class wich implements the
QuerySort
interface (and, optionally, an interface which extendsQuerySort
and represents your custom filter API); -
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 aQuerySort
declaration which the concreteDatastore
can handle. -
Finally, register the
QuerySortResolver
in theDatastore
instance, using theaddExpressionResolver(…)
method.
When the resolver is registered, the custom sort can be used as any another QuerySort
implementation.
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.
Concrete Datastore implementations may provide other methods to register a commodity. See each specific Datastore implementation documentation for details.
|
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.
Concrete Datastore implementations may offer specific DatastoreCommodityContext extensions to provide specific Datastore context references and configuration attributes.
|
A DatastoreCommodity
can be obtained from a 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.
See each concrete Datastore
implementations documentation for further details and examples.
4.16.11. 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
4.17. DataMappable
The DataMappable interface can be used to define and provide a data mapping for a data model related object, declaring the actual data attribute path to to which it refers.
The data mapping declaration can also be used when the path represented by a data model related object does not match the actual data model path name, to provide the real data model path name itself.
The DataMappable
provides the data path mapping, if available, through the method:
Optional<String> getDataPath();
4.17.1. Data mapping declaration
Some Holom Platform APIs directly extends the DataMappable
API and provides builder methods to set the data path mapping. Two of these are Path and PathProperty.
PathProperty<Integer> property = PathProperty.create("name", Integer.class) //
.dataPath("mapping-name"); (1)
Optional<String> mapping = property.getDataPath(); (2)
1 | Set the actual data path for the property |
2 | Obtain the data path, if available |
As a general convention, the data path mapping is configured in the objects which support a configuration using the DataMappable.PATH
configuration property, and this property can be used as an alternative for the DataMappable
API method when the object does not directly implement that interface.
For example, to set the data path for a PropertySet
type object the DataMappable.PATH
configuration property can be used in this way:
PropertySet<?> PROPERTIES = PropertySet.builderOf(P1, P2) //
.configuration(DataMappable.PATH, "mapping-name") (1)
.build();
Optional<String> mapping = PROPERTIES.getConfiguration().getParameter(DataMappable.PATH); (2)
1 | Set the property set data path using the DataMappable.PATH configuration property |
2 | Get the data path, if available, through the DataMappable.PATH configuration property |
4.17.2. Data mapping usage
The meaning and usage strategy of the data path value is completely dependent from each concrete API or implementation.
See, for example, the JDBC Datastore documentation for an use case of the data path representation.
4.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.
4.19. Utilities
The core Holon platform module provides some utility interfaces/classes which can be used in applications development.
4.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 |
4.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.
5. HTTP messages and RESTful Java client
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.1.1</version>
5.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 core 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.
The HttpMessage API is the base HTTP message representation.
5.1.1. Headers
For a HTTP message, the header values are represented as a List
of Strings, since HTTP supports multiple values for each header.
The HttpMessage
API, through the HttpHeaders interface, provides a set of methods to inspect the HTTP message headers and obtain header values in a more useful and convenient way.
For example, frequently used header values can be obtained using a suitable Java type, if the value is available in the HTTP message headers.
HttpMessage<String> message = getMessage();
Optional<String> value = message.getHeaderValue("HEADER_NAME"); (1)
Optional<Date> date = message.getDate(); (2)
Optional<URI> location = message.getLocation(); (3)
Optional<Long> length = message.getContentLength(); (4)
Optional<Locale> locale = message.getLocale(); (5)
List<Locale> locales = message.getLocales(); (6)
Optional<String[]> basicAuth = message.getAuthorizationBasicCredentials(); (7)
Optional<String> bearerAuth = message.getAuthorizationBearer(); (8)
1 | Get the value of given header name, if present. If the HTTP header is present more than once then the values are joined together and separated by a , character. |
2 | Get the value of the HTTP Date header, if available, as a Java Date instance |
3 | Get the value of the HTTP Location header, if available, as a Java URI instance |
4 | Get the value of the HTTP Content-Length header, if available, as a Java Long |
5 | Get the first (most qualified) Locale using the Accept-Language header, if present. |
6 | Get a list of Locale languages using the Accept-Language header, if present. If more than one language is specified in the Accept-Language header, returned Locales will be ordered relying on quality parameter. |
7 | Get the basic authorization credentials from a Basic type HTTP Authorization header, if available. The credentials are decoded from Base64 and returned as a username/password array. |
8 | Get the bearer authorization token from a Bearer type HTTP Authorization header, if available. |
5.1.2. HttpRequest
The HttpRequest API represents a HTTP request message.
Besides the operations made available from the HttpMessage API, it 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.
-
The request message body (payload) as an
InputStream
.
The HttpRequest
message type is bound to a String
type message payload.
HttpRequest message = getRequestMessage();
HttpMethod method = message.getMethod(); (1)
String path = message.getRequestPath(); (2)
Optional<String> value = message.getRequestParameter("param1"); (3)
Optional<List<String>> values = message.getMultiValueRequestParameter("param2"); (4)
Optional<Cookie> cookie = message.getRequestCookie("cookie1"); (5)
Optional<String> body = message.getPayload(); (6)
InputStream bodyAsStream = message.getBody(); (7)
1 | Get the HTTP request method as HttpMethod enumeration value |
2 | Get the path of the HTTP request, relative to the base URI |
3 | Get a request URI parameter value, if available. If the parameter is multi-value, the values are joined together and separated by a , character |
4 | Get the values of a multi-value request URI parameter, if available |
5 | Get a request Cookie value, if available |
6 | Get the message body as a String |
7 | Get the message body as an InputStream |
5.1.3. HttpResponse
The HttpResponse interface represents a HTTP response message.
Besides the operations made available from the HttpMessage API, it 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.
HttpResponse<String> message = getResponseMessage();
int statusCode = message.getStatusCode(); (1)
HttpStatus status = message.getStatus(); (2)
1 | Get the HTTP response status code |
2 | Get the HTTP response status using the HttpStatus enumeration |
5.1.4. Servlet API integration
The ServletHttpRequest API represents a HttpRequest
backed by a javax.servlet.http.HttpServletRequest
instance, and can be used as an adapter to deal with servlet request messages using the Holon Platform HttpRequest
API.
The ServletHttpRequest
API provides additional methods to obtain servlet related message information:
-
The request context path.
-
The request URI.
-
The HTTP session id.
To obtain a HttpRequest
API from a servlet HttpServletRequest
instance, the ServletHttpRequest
create(HttpServletRequest request)
method can be used.
HttpServletRequest servletRequest = getServletRequest();
HttpRequest request = ServletHttpRequest.create(servletRequest); (1)
1 | Create a HttpRequest API from a HttpServletRequest instance |
This way, a servlet request can be used for example with a Holon MessageAuthenticator to perform authentication operations directly using the request message. See the MessageAuthenticator section for further information.
|
5.2. RESTful client API
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 API 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
-
Cache-Control
header configuration
-
-
Convenience methods to setup authorization headers(
Basic
andBearer
types). -
Convenience methods to perform most common invocations using one of the
GET
,POST
,PUT
,PATCH
,DELETE
,OPTIONS
,TRACE
orHEAD
methods.
5.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 client = RestClient.create(); (1)
client = RestClient.create("com.holonplatform.jaxrs.client.JaxrsRestClient"); (2)
client = RestClient.forTarget("https://host/api"); (3)
1 | Create a RestClient API using the default available implementation for current ClassLoader |
2 | Create a RestClient API using a specific implementation class name |
3 | Create a RestClient API using the default available implementation and setting the default base URI |
Available implementations
The RestClient
implementations provided by the Holon Platform are are:
-
A JAX-RS based implementation, using a standard JAX-RS
Client
to perform invocations, available from the holon-jaxrs.html#JaxrsRestClient[Holon platform JAX-RS module]; -
A Spring based implementation, using the Spring
RestTemplate
API to perform invocations;
5.2.2. Configure defaults
The RestClient
API supports some default configuration attributes, which will be used for each request performed using a RestClient
instance:
-
A default target, i.e. the default base URI which will be used for all the requests performed with the
RestClient
API, unless overridden using the specific request configurationtarget
method. -
A set of default headers to be included in all the requests performed with the
RestClient
API.
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 |
5.2.3. Build and configure a request
To build a client request, the RequestDefinition API is used, which represents both a fluent builder to configure the request message and an Invocation API to perform the actual invocation and obtain a response.
The request can be configured using the RequestDefinition
API 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
API 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
API supports URI query parameters specification, with single or multiple values, through the queryParameter(…)
methods.
client.request().queryParameter("parameter", "value") (1)
.queryParameter("multiValueParameter", 1, 2, 3); (2)
1 | Set a single value query parameter |
2 | Set a multiple values query parameter |
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
types 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 values using the accept(MediaType… mediaTypes) builder method.
|
The CacheControl API provides a fluent builder to build and set a Cache-Control header value for the request, using the cacheControl(CacheControl cacheControl) builder method.
|
client.request().header("Accept", "text/plain"); (1)
client.request().header(HttpHeaders.ACCEPT, "text/plain"); (2)
client.request().accept("text/plain", "text/xml"); (3)
client.request().accept(MediaType.APPLICATION_JSON); (4)
client.request().acceptEncoding("gzip"); (5)
client.request().acceptCharset("utf-8"); (6)
client.request().acceptCharset(Charset.forName("utf-8")); (7)
client.request().acceptLanguage("en-CA"); (8)
client.request().acceptLanguage(Locale.US, Locale.GERMANY); (9)
client.request().cacheControl(CacheControl.builder().noCache(true).noStore(true).build()); (10)
1 | Set a request header, providing its name and its value |
2 | Set a request header, providing its name through the HttpHeaders enumeration and its value |
3 | Set the request Accept header values |
4 | Set the request Accept header value using the MediaType enumeration |
5 | Set the request Accept-Encoding header value |
6 | Set the request Accept-Charset header value |
7 | Set the request Accept-Charset header value using the Java Charset class |
8 | Set the request Accept-Language header value |
9 | Set the request Accept-Language header values using the Java Locale class |
10 | Build a CacheControl definition and set it as Cache-Control request header value |
Authorization headers
The RestClient
API provides two convenience request builder methods to setup a request Authorization
header using:
-
The
Basic
authorization scheme, providing a username and a password, through theauthorizationBasic(String username, String password)
builder method. -
The
Bearer
authorization scheme, providing a token, through theauthorizationBearer(String bearerToken)
builder method.
client.request().authorizationBasic("username", "password"); (1)
client.request().authorizationBearer("An389fz56xsr7"); (2)
1 | Set the Authorization request header value using the Basic scheme and providing the credentials. Username and password will be encoded according to the HTTP specifications |
2 | Set the Authorization request header value using the Bearer scheme and providing the bearer token value. See RFC6750 |
5.2.4. Invoke the request and obtain a response
The RequestDefinition
API extends the Invocation API, which can be used to perform the actual invocation and obtain a response.
The Invocation
API provides a generic invocation method:
<T, R> ResponseEntity<T> invoke(HttpMethod method, RequestEntity<R> requestEntity, ResponseType<T> responseType)
This method requires the following parameters:
-
The HTTP method to use to perform the request (
GET
,POST
, and so on), specified using theHttpMethod
enumeration. -
An optional request entity, i.e. the request message payload (body), represented through the
RequestEntity
API. -
The expected response entity type using the
ResponseType
class, to declare the Java type of the response payload and apply a suitable converter, if available, to obtain the HTTP response body as the expected Java type.
The method returns a ResponseEntity
type object, a HttpResponse extension which can be used to:
-
Inspect the response message, for example to obtain the HTTP response status code, as a number or represented through the HttpStatus enumeration.
-
Obtain the HTTP response raw payload or get it as a Java object, unmarshalled by a suitable converter which must be available from the concrete
RestClient
API implementation.
For non textual request or response payload types, any marshalling/unmarshalling strategy and implementation must be provided by the concrete RestClient API. See the specific RestClient Available implementations documentation for additional information.
|
See the next sections for details about the invocation parameters and return types.
5.2.5. Request entity
The RequestEntity interface can be used to provide a request entity to the RestClient
API invocation methods, i.e. the request message payload.
The request entity is represented by a Java object and its serialization format is specified using a media type declaration (i.e. a MIME type definition) through the Content-Type
request header value.
Depending on the RestClient API implementation used, you must ensure the request media type is supported and a suitable request message body converter is 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 instance using the most common media types, such a text/plain
, application/json
, application/xml
and application/x-www-form-urlencoded
(the latter 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, using test as request entity value |
2 | Build a application/json type request entity, using a TestData class instance as request entity value |
3 | Build a application/x-www-form-urlencoded type request entity, using the formBuilder method to build the form data map |
The RequestEntity.EMPTY
constant value can be used to provide an empty request entity.
RequestEntity<?> emptyRequest = RequestEntity.EMPTY; (1)
1 | Build an empty request empty, to provide a request message without a payload |
5.2.6. Response type
The ResponseType interface can be used to provide the expected response entity type to the RestClient
API invocation methods.
In addition to a simple Java class type, a parametrized type can be declared, allowing to use Java generic types as response types.
ResponseType<TestData> responseType1 = ResponseType.of(TestData.class); (1)
ResponseType<List<TestData>> responseType2 = ResponseType.of(TestData.class, List.class); (2)
1 | Declares a response type as TestData type |
2 | Declares a response type as a List of TestData types |
5.2.7. Response entity
The ResponseEntity interface is used by RestClient
API to represent the invocation response and to deal with the optional response entity obtained as invocation result.
Since it is a HttpResponse instance, the ResponseEntity
API can be used to inspect the response message, for example the HTTP message headers, including the HTTP status code.
ResponseEntity<TestData> response = RestClient.forTarget("https://rest.api.example/testget").request()
.accept(MediaType.APPLICATION_JSON).get(TestData.class); (1)
HttpStatus status = response.getStatus(); (2)
int statusCode = response.getStatusCode(); (3)
long contentLength = response.getContentLength().orElse(-1L); (4)
Optional<String> value = response.getHeaderValue("HEADER_NAME"); (5)
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 as HttpStatus enumeration value |
3 | Get the response status code |
4 | Get the Content-Length header value |
5 | Get a generic header value |
To obtain the response entity value as the expected Java type, the getPayload()
method can be used. The return ResponseEntity
object generic type is provided according to the specified Response type, so the payload value will be and instance of the expected response Java type.
Furthermore, the ResponseEntity
API makes available the as(Class entityType)
type as a different type from the one specified with the Response type invocation parameter, if the media type is supported by the concrete RestClient
API implementation and a suitable converter is available.
When a response has not a payload, i.e. the response entity is not available, the Optional
result of the getPayload()
and as(Class entityType)
methods will be empty.
ResponseEntity<TestData> response = RestClient.forTarget("https://rest.api.example/testget").request()
.accept(MediaType.APPLICATION_JSON).get(TestData.class); (1)
boolean hasEntity = response.getPayload().isPresent(); (2)
Optional<TestData> entity = response.getPayload(); (3)
Optional<String> asString = response.as(String.class); (4)
1 | Perform a GET request, setting the Accept header as application/json and declaring the TestData class as expected response entity Java type |
2 | Checks whether a response entity payload is available |
3 | Get the response entity value, as a TestData class instance |
4 | Get the response entity value as a String |
Depending on the concrete RestClient API implementation, you must ensure the response media type is supported and a suitable message body converter is available to deal with the Java object type and the media type of the response entity.
|
5.2.8. Specific request invocation methods
In most cases, it is easier and faster to use HTTP method-specific invocation methods, made available by the RestClient
invocation API.
Each invocation method is relative to a specific HTTP request method and it is named accordingly. More than one method version is provided for each HTTP 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
request method), the RestClient
API makes available a set of invocation methods organized as follows:
1. A set of methods to optionally provide a Request entity and to obtain a Response entity. If the response is expected to contain a payload which has to be deserialized into a Java object, the Response type can be specified, either as a simple or parametrized Java class.
final RestClient client = RestClient.forTarget("https://rest.api.example/test");
ResponseEntity<TestData> response = client.request().get(TestData.class); (1)
response = client.request().get(ResponseType.of(TestData.class)); (2)
response = client.request().put(RequestEntity.json(new TestData()), TestData.class); (3)
1 | Perform an invocation using the GET method and obtain a ResponseEntity expecting the TestData class as response entity type |
2 | The same invocation using the ResponseType API to specify the expected response entity type |
3 | Perform an invocation using the PUT method and providing an application/json type request entity, expecting a TestData response entity type |
When a response entity is not expected, this category of invocation methods return a Void
type ResponseEntity
.
ResponseEntity<Void> response2 = client.request().post(RequestEntity.json(new TestData())); (1)
HttpStatus status = response2.getStatus(); (2)
1 | Perform an invocation using the POST method and providing an application/json type request entity, but no response entity is expected |
2 | Get the response HTTP status |
2. A set of method to directly obtain the deserialized response entity value, named with the ForEntity
suffix. This methods expects a successful response (i.e. a response with a 2xx
HTTP status code), otherwise an UnsuccessfulResponseException
is thrown. The exception which can be inspected to obtain the response status code and the response itself. This kind of methods returns an Optional
value, which will be empty for empty responses.
Optional<TestData> value = client.request().getForEntity(TestData.class); (1)
Optional<List<TestData>> values = client.request().getForEntity(ResponseType.of(TestData.class, List.class)); (2)
1 | Perform an invocation using the GET method and directly obtain the TestData type response entity value, if available |
2 | Perform an invocation using the GET method and directly obtain a List of TestData type response entity values, if available |
The UnsuccessfulResponseException type, which is thrown by the xxxForEntity
invocation methods when the response status code do not belongs to the 2xx
family, provides some information about the invocation failure:
-
The actual response status code.
-
A reference to the actual
ResponseEntity
instance.
try {
client.request().getForEntity(TestData.class);
} catch (UnsuccessfulResponseException e) {
// got a response with a status code different from 2xx
int httpStatusCode = e.getStatusCode(); (1)
Optional<HttpStatus> sts = e.getStatus(); (2)
ResponseEntity<?> theResponse = e.getResponse(); (3)
}
1 | Get the actual response status code |
2 | Get the response status code as a HttpStatus |
3 | Get the ResponseEntity instance |
3. A set of convenience methods are provided for frequent needs and situations, for example:
-
A
getForStream
method to perform a request using the HTTPGET
method and obtain the response entity as anInputStream
. This can be useful, for example, for API invocations which result is a stream of byte or characters.
InputStream responseEntityStream = client.request().getForStream();
-
A
getAsList
method, to perform a request using the HTTPGET
method and obtain the response entity contents as aList
of deserialized Java objects in a specified expected response type. For empty response entities, an empty List is returned.
List<TestData> collectionOfValues = client.request().getAsList(TestData.class);
-
A
postForLocation
to perform a request using the HTTPPOST
and directly obtain theLocation
response header value as a JavaURI
instance, if available.
Optional<URI> locationHeaderURI = client.request().postForLocation(RequestEntity.json(new TestData()));
5.2.9. RestClient
API invocation methods reference
Below a reference list of the RestClient
Invocation
API, available from the request definition API:
RestClient restClient = RestClient.forTarget("http://api.example"); // Obtain a RestClient
restClient.request(); // Request definition
Generic invocations:
Operation | Description | Parameters | Returns | Response status handling |
---|---|---|---|---|
|
Invoke the request and receive a response back. |
|
A |
None |
|
Invoke the request and receive a response back only if the response has a success ( |
|
The |
If the response status code is not |
|
Invoke the request and receive back the response content entity, already deserialized in the expected response type. |
|
Optional response entity value, already deserialized in the expected response entity type |
If the response status code is not |
By method invocations:
1. GET
:
Operation | Parameters | Returns | Response status handling |
---|---|---|---|
|
Expected response entity type, either using a |
A |
None |
|
Expected response entity type, either using a |
Optional response entity value ( |
If the response status code is not |
|
None |
The response payload stream as an |
If the response status code is not |
|
Expected response entity type ( |
A |
If the response status code is not |
2. POST
:
Operation | First parameter | Second parameter | Returns | Response status handling |
---|---|---|---|---|
|
The request entity represented as |
Optional expected response entity type, either using a |
A |
None |
|
The request entity represented as |
Expected response entity type, either using a |
Optional response entity value ( |
If the response status code is not |
|
The request entity represented as |
None |
Optional |
If the response status code is not |
3. PUT
:
Operation | First parameter | Second parameter | Returns | Response status handling |
---|---|---|---|---|
|
The request entity represented as |
Optional expected response entity type, either using a |
A |
None |
|
The request entity represented as |
Expected response entity type, either using a |
Optional response entity value ( |
If the response status code is not |
4. PATCH
:
Operation | First parameter | Second parameter | Returns | Response status handling |
---|---|---|---|---|
|
The request entity represented as |
Optional expected response entity type, either using a |
A |
None |
|
The request entity represented as |
Expected response entity type, either using a |
Optional response entity value ( |
If the response status code is not |
5. DELETE
:
Operation | Parameter | Returns | Response status handling |
---|---|---|---|
|
Optional expected response entity type, either using a |
A |
None |
|
None |
Nothing |
If the response status code is not |
|
Expected response entity type, either using a |
Optional response entity value ( |
If the response status code is not |
6. OPTIONS
:
Operation | Parameter | Returns | Response status handling |
---|---|---|---|
|
Optional expected response entity type, either using a |
A |
None |
|
Expected response entity type, either using a |
Optional response entity value ( |
If the response status code is not |
7. TRACE
:
Operation | Parameter | Returns | Response status handling |
---|---|---|---|
|
Optional expected response entity type, either using a |
A |
None |
|
Expected response entity type, either using a |
Optional response entity value ( |
If the response status code is not |
8. HEAD
:
Operation | Returns | Response status handling |
---|---|---|
|
A |
None |
5.2.10. Property
and PropertyBox
support
The RestClient
API fully supports the Holon Platform Property model when used along with the PropertyBox data type as a request/response entity in RESTful API calls.
Regarding the JSON media type, the PropertyBox
type marshalling and unmarshalling support is provided by the Holon Platform JSON module. For the builtin RestClient
API implementations, the PropertyBox
type JSON support is automatically setted up when the suitable Holon platform JSON module artifacts are available in classpath.
When a response entity value has to be deserialized into a PropertyBox
object type, the property set to be used must be specified along with the reponse entity type, in order to instruct the JSON module unmarshallers about the property set with which to build the response PropertyBox
instances.
For this purpose, the RestClient
invocation API propertySet(…)
methods can be used to specify the property set with which to obtain a PropertyBox
type response entity value.
final PathProperty<Integer> CODE = create("code", int.class);
final PathProperty<String> VALUE = create("value", String.class);
final PropertySet<?> PROPERTIES = PropertySet.of(CODE, VALUE);
RestClient client = RestClient.create();
PropertyBox box = client.request().target("https://rest.api.example").path("/apimethod").propertySet(PROPERTIES)
.getForEntity(PropertyBox.class).orElse(null); (1)
Optional<PropertyBox> box2 = client.request().target("https://rest.api.example").path("/apimethod")
.propertySet(CODE, VALUE).getForEntity(PropertyBox.class); (2)
List<PropertyBox> boxes = client.request().target("https://rest.api.example").path("/apimethod")
.propertySet(PROPERTIES).getAsList(PropertyBox.class); (3)
1 | GET request for a PropertyBox type response, using PROPERTIES as property set |
2 | Response PropertyBox property set specification using directly an array of properties |
3 | GET request for a list of PropertyBox type response, using PROPERTIES as property set |
6. Authentication and Authorization
The holon-auth
artifact provides a complete and highly configurable authentication and authorization architecture, integrated with all the platform modules.
Maven coordinates:
<groupId>com.holon-platform.core</groupId>
<artifactId>holon-auth</artifactId>
<version>5.1.1</version>
6.1. Realm
The Realm API represents a security abstraction providing operations for principals authentication and authorization.
The Realm
API is the main entry point to deal with the Holon Platform authentication and authorization architecture: it holds the configuration of the authentication and authorization context and provides operations to perform principals authentication and authorization controls.
The Realm
authentication strategy is defined using a set of authenticators, represented by the Authenticator interface, each bound to a specific AuthenticationToken, which represents the principal’s credentials.
In a mirrored way, the Realm
authorization strategy is defined using a set of authorizers, represented by the Authorizer interface, each bound to a specific Permission type, and used by the Realm
API to perform authorization controls against the principal’s granted permissions.
The authenticators and authorizers bound to a specific Realm
instance define the authentication and authorization strategy of such Realm
, so they are registered at Realm
configuration time.
The Realm
API provides a fluent builder to build and configure a Realm
instance, with authenticators and authorizers registration methods.
Realm realm = Realm.builder() (1)
.authenticator(AUTHENTICATOR1) (2)
.authenticator(AUTHENTICATOR2) (3)
.authorizer(AUTHORIZER1) (4)
.authorizer(AUTHORIZER2) (5)
.build();
1 | Obtain a Realm builder |
2 | Register an Authenticator |
3 | Register another Authenticator |
4 | Register an Authorizer |
5 | Register another Authorizer |
The Authenticator and Authorizer sections describe these API definitions in detail.
6.1.1. Realm name
A Realm
instance can be identified by a name, which can be used to identify a specific Realm
instance when more than one is available.
Realm realm = Realm.builder().name("nyname").build(); (1)
Optional<String> name = realm.getName(); (2)
1 | Set the Realm name using the default builder |
2 | Get the Realm name, if available |
6.1.2. Realm authentication
Authentication requests are made available through the Authenticator API, which is implemented by the Realm
API.
The Authenticator
API provides the following method to perform authentication requests:
Authentication authenticate(AuthenticationToken authenticationToken) throws AuthenticationException;
The authentication request is represented by an AuthenticationToken and the authenticated principal, if the authentication request is successful, is returned as an Authentication representation.
The Realm
API itself does not implement any authentication model or strategy, but delegates the specific authentication strategy to one or more concrete Authenticator, relying on the AuthenticationToken
type in order to discern which Authenticator
has to be used to handle the authentication process.
The authentication flow is structured as follows:
-
A concrete
AuthenticationToken
, which represents the authentication request (for example, the principal's credentials), is provided to theauthenticate
method; -
The Realm checks if a suitable
Authenticator
is registered, i.e. anAuthenticator
which can handle the givenAuthenticationToken
type. If not, anUnsupportedTokenException
is thrown; -
The
authenticate(AuthenticationToken authenticationToken)
method is called on the specificAuthenticator
API, performing the concrete authentication operation. -
If the authentication operation is successful, the authenticated principal is returned using
Authentication
representation. -
Otherwise, an
AuthenticationException
type is thrown. The concrete type of the exception gives more detailed informations on what went wrong.
Each Authenticator
declares the AuthenticationToken type to which is bound through the getTokenType()
method. When a new Authenticator
is registered, the Realm
instance will support the AuthenticationToken
type which is bound to the registered Authenticator
, and such Authenticator
will be used to perform the authentication operation when a matching AuthenticationToken
type is provided.
A concrete Authenticator
can be registered in a Realm
instance in two ways:
-
Using the
Realm
API builder. -
Using the
addAuthenticator
method of theRealm
API.
Realm realm = Realm.builder().authenticator(AUTHENTICATOR1).build(); (1)
realm.addAuthenticator(AUTHENTICATOR2); (2)
1 | Add a Realm Authenticator using the builder API |
2 | Add a Realm Authenticator using the Realm API method |
To check if a Realm
instance supports a specific AuthenticationToken
type, the supportsToken
API method can be used.
Realm realm = getRealm();
boolean supported = realm.supportsToken(MyAuthenticationToken.class); (1)
1 | Checks whether given Realm supports the MyAuthenticationToken authentication token type |
So the Realm
API is itself an Authenticator
, bound to a generic AuthenticationToken
type. The authenticate
method is the entry point to perform any authentication request, providing a suitable AuthenticationToken
type implementation.
Realm realm = getRealm();
try {
Authentication authc = realm.authenticate(new MyAuthenticationToken("test")); (1)
} catch (AuthenticationException e) {
// handle failed authentication
}
1 | Perform an authentication request using the MyAuthenticationToken authentication token type |
See the next sections for details about authenticators, authentication tokens and the authenticated principal representation.
6.1.3. 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 genericObject
, 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. The return type is a genericObject
, since each authentication model could represent the principal credentials in a different way.
Each AuthenticationToken
sub-type is bound to an Authenticator, which is able to interpret the principal and credentials representations and to perform the actual authentication process using the information provided through the AuthenticationToken
instance.
Some builtin AuthenticationToken
representations are provided by the core Holon Platform module:
Account credentials authentication 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"); (1)
1 | Create an account credentials authentication token type using username as account id (the principal’s name) and password as account secret (the account credentials) |
Bearer authentication 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"); (1)
1 | Create a bearer authentication token type, providing the token value |
6.1.4. Authenticator
As stated in the Realm authentication flow description, the Realm
API relies on the registered Authenticator
instances to perform the actual authentication process, according to the provided AuthenticationToken
type.
The Authenticator API represents a concrete authentication strategy, using a specific AuthenticationToken
type to represent the authentication request. The AuthenticationToken
type to which an Authenticator
is bound is provided by the getTokenType()
method.
The authenticate(AuthenticationToken authenticationToken)
method of the Authenticator API is used to perform the actual authentication operation, checking the principal’s credentials provided through the AuthenticationToken
instance and returning an Authentication representation of the authenticated principal if the process was successful.
When an authentication request is not successful, an AuthenticationException
type is thrown. The concrete type of the exception gives more detailed informations on what went wrong. See Authentication exceptions for a list of the authentication exceptions available by default.
Authenticator<MyAuthenticationToken> authenticator = Authenticator.create(MyAuthenticationToken.class, (1)
token -> {
// check authentication token information
token.getPrincipal();
token.getCredentials();
boolean valid = true; // ...
// if not valid, throw an exception
if (!valid) {
throw new InvalidCredentialsException();
}
// otherwise, return the authenticated principal representation
return Authentication.builder("thePrincipalName").build();
});
try {
Authentication authc = authenticator.authenticate(new MyAuthenticationToken("test")); (2)
} catch (AuthenticationException e) {
(3)
}
1 | Create an Authenticator bound to the MyAuthenticationToken authentication token type |
2 | Perform an authentication request on the Authenticator , obtaining the authenticated principal representation if successful. |
3 | If the authentication request is not successful, an AuthenticationException type is thrown |
See the Authentication section for information about the authenticated principal representation. |
Builtin authenticators
-
See the Account section to learn about the builtin account credentials type authenticator.
-
See the JWT support section learn about the builtin JSON Web Token type authenticator.
Authenticator example
Below is provided a simple example on how to create a custom Authenticator
, bound to a specific AuthenticationToken
, register it into a Realm
and use it to handle authentication requests.
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;
}
@Override
public Authentication authenticate(MyAuthenticationToken authenticationToken) throws AuthenticationException {
if (!"test".equals(authenticationToken.getPrincipal())) { (3)
throw new UnknownAccountException();
}
return Authentication.builder(authenticationToken.principalName).build();
}
}
public void authenticate() {
Realm realm = Realm.builder().authenticator(new MyAuthenticator()).build(); (4)
try {
Authentication authc = realm.authenticate(new MyAuthenticationToken("test")); (5)
} catch (AuthenticationException e) {
// handle failed authentication
}
}
1 | Create an AuthenticationToken implementation, which returns the principal name as a String from the getPrincipal() method |
2 | Create a custom Authenticator bound to the MyAuthenticationToken type |
3 | This authenticator only accepts test named principals, building a simple Authentication instance with the provided principal name. Otherwise, an UnknownAccountException is thrown. |
4 | Create a Realm and register the custom authenticator |
5 | Perform an authentication request using a MyAuthenticationToken istance |
6.1.5. Authentication exceptions
Below a list of the default authentication exceptions.
Class | Meaning |
---|---|
|
Provided credentials are not valid or do not match the stored credentials |
|
Provided credentials are expired |
|
An unexpected internal error occurred during credentials match |
|
Account is disabled |
|
Account is locked |
|
Unknown account |
|
The authentication token is not valid |
|
Unsupported authentication token type |
|
Unsupported authentication message |
|
Generic authentication process failure |
6.1.6. Authentication
The result of an Authenticator
successful authentication request is represented by the Authentication API.
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:
-
An optional set of Permission granted to the authenticated principal.
-
A
isRoot()
flag, to mark the authenticated principal as a 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, to identify the authentication scheme with which the principal was authenticated. See MessageAuthenticator for details about authentication schemes.
-
It extends the Holon Platform ParameterSet API, which represents a set of custom name-value attributes and 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.
The Authentication
API provides a builder to create new Authentication
instances.
Authentication authc = Authentication.builder("userId") (1)
.permission("VIEW") (2)
.permission(new MyPermission()) (3)
.parameter("name", "John") (4)
.parameter("surname", "Doe") (5)
.scheme("myscheme") (6)
.build();
1 | Obtain an Authentication builder and set userId as principal name |
2 | Add a VIEW String type granted permission (using the role name convention) |
3 | Add a custom MyPermission type granted permission |
4 | Add name named parameter |
5 | Add surname named parameter |
6 | Set myscheme as authentication scheme |
6.1.7. Authentication listeners
The AuthenticationListener interface can be used to be notified when a successfull authentication is performed. The authenticated principal, represented as an Authentication instance, is provided to the listener method.
The AuthenticationNotifier API allows to add and remove an AuthenticationListener
. The AuthenticationNotifier
is implemented by the Realm
API, so an AuthenticationListener
can be registered in a Realm
instance to be notified when a successfull authentication request is performed.
Realm realm = getRealm();
realm.addAuthenticationListener(authentication -> { (1)
// do something ...
authentication.getName();
});
1 | Add an AuthenticationListener to given Realm instance |
6.1.8. MessageAuthenticator
The MessageAuthenticator interface represents an intermediate authenticator API, specialized for Message
based authentication.
The MessageAuthenticator
API relies on the Holon Platform Message API as a generic message representation.
See the HTTP messages section to lean about the HTTP implementations of the Message API.
|
The MessageAuthenticator
API allows to perform authentication requests directly using a message to provide the authentication request information.
The aim of a MessageAuthenticator
is to translate a message representation into a standard AuthenticationToken representation, through the AuthenticationTokenResolver interface, and then use the token in order to perform a conventional authentication flow.
An AuthenticationTokenResolver
is bound to a specific Message
type and provides an additional message partitioning level, the authentication scheme. This way, for the same type of Message
, different resolvers can be provided for different authentication schemes.
For example, taken two resolvers bound to the same HttpMessage
type, one could deal with basic authentication scheme and the other with bearer authentication scheme.
For example, supposing to use:
-
A custom Message implementation class, that we’ll call
MyMessage
. -
A custom
AuthenticationToken
type, calledMyMessageAuthenticationToken
.
We want to create an AuthenticationTokenResolver
that processes a MyMessage
instance, looks for a MY_HEADER
message header value and, if available, resolves the message in a MyMessageAuthenticationToken
instance setting the MY_HEADER
header value as token principal id:
AuthenticationTokenResolver<MyMessage> myResolver = AuthenticationTokenResolver.create(MyMessage.class, (1)
msg -> msg.getHeader("MY_HEADER").map(value -> new MyMessageAuthenticationToken(value)) (2)
);
1 | Create an AuthenticationTokenResolver bound to the MyMessage message type |
2 | If the MY_HEADER message header value is present, return a MyMessageAuthenticationToken token type, setting the header value as principal |
The MessageAuthenticator
API supports more than one message type, through a set of registered AuthenticationTokenResolver
. The supportsMessage
and getResolversForMessageType
methods of the MessageAuthenticator
API allow to check if a message type is supported (i.e. one or more AuthenticationTokenResolver
is available for given message type) and to obtain the resolvers for a specific message type, respectively.
The MessageAuthenticator
API makes available a specialized method which accepts a Message
as authentication request representation:
Authentication authenticate(Message<?, ?> message, String... schemes)
This method can be used to perform an authentication request directly using a Message representation.
Use Realm
as a MessageAuthenticator
The Realm API extends the MessageAuthenticator API, so a Realm
instance can be used to process authentication requests using messages.
The default Realm
builder provides methods to register one ore more AuthenticationTokenResolver
.
The message based authentication flow is structured as follows:
-
Lookup for suitable
AuthenticationTokenResolver
to obtain anAuthenticationToken
from the request message, i.e. a resolver bound to given message type; -
If one or more authentication scheme is specified, only the
AuthenticationTokenResolver
bound to the provided scheme names will be taken into account. -
If a not empty set of
AuthenticationTokenResolver
is obtained using the strategy described above, all the suitable resolvers are invoked to obtain anAuthenticationToken
: theAuthenticationToken
to be used will be the one obtained from the firstAuthenticationTokenResolver
which provides a not null token (or better, the first resolver which returns a not empty Optional, according to theAuthenticationTokenResolver
API). The resolvers are invoked in the order they were registered and according to provided authentication schemes order, if any. -
If an
AuthenticationToken
was successfully obtained, it will be used to trigger a conventional authentication request, as described in the Realm authentication section. -
Otherwise, an
UnsupportedMessageException
is thrown.
Realm realm = Realm.builder().resolver(myResolver) (1)
.authenticator(new MyAuthenticator()) (2)
.build();
MyMessage message = new MyMessage();
Authentication authc = realm.authenticate(message); (3)
1 | Build an configure a Realm registering an AuthenticationTokenResolver |
2 | Add an Authenticator to handle actual AuthenticationToken based authentication requests |
3 | Perform the authentication request using a MyMessage message instance |
Builtin HTTP message resolvers
For the HttpRequest message type, two builtin AuthenticationTokenResolver
are provided: one to deal with the HTTP Basic
scheme and the other to handle the Bearer
scheme.
Basic HTTP message resolver:
This AuthenticationTokenResolver
can be otained using the httpBasicResolver()
method of the AuthenticationToken interface:
AuthenticationTokenResolver<HttpRequest> basicResolver = AuthenticationToken.httpBasicResolver();
The resolver inspect the message Authorization
header, if available, checking if it is declared using the Basic
authorization scheme. If so, extracts and decodes the basic credentials (username and password) and uses them to create an Account credentials authentication token authentication token type.
This resolver can be used, for example, to implement a message based account authentication strategy. See Account section to learn how to use the Holon Platform APIs for this purpose.
AccountProvider accountProvider = getAccountProvider();
Realm realm = Realm.builder().resolver(AuthenticationToken.httpBasicResolver()) (1)
.authenticator(Account.authenticator(accountProvider)) (2)
.build();
// obtain the HttpRequest message, for example using a servlet request
HttpRequest request = getHttpRequest();
try {
Authentication authc = realm.authenticate(request); (3)
} catch (AuthenticationException e) {
// handle authentication failures
}
1 | Register a Basic authorization HTTP message resolver |
2 | Register an Authenticator to handle account credentials based authentication |
3 | Perform the authentication request using the HTTP request message |
Bearer HTTP message resolver:
This AuthenticationTokenResolver
can be otained using the httpBearerResolver()
method of the AuthenticationToken interface:
AuthenticationTokenResolver<HttpRequest> bearerResolver = AuthenticationToken.httpBearerResolver();
The resolver inspect the message Authorization
header, if available, checking if it is declared using the Bearer
authorization scheme. If so, extracts the bearer token value and uses it to create an Bearer authentication token authentication token type.
This resolver can be used, for example, to implement a JWT (JSON Web Token) message based authentication. See JWT support section to learn how to use the Holon Platform APIs for this purpose.
6.1.9. Realm authorization
Authorization controls operations are provided by the Authorizer API, which is implemented by the Realm
API itself.
The Authorizer
API provides a set of isPermitted(…)
methods to perform authorization controls, using the Permission representation to validate a set of permissions against an Authentication (which represents an authenticated principal) granted authorities.
A Permission
represents a granted authority and its actual meaning and validation strategy depends on the concrete Authorizer
implementation, which can use specific Permission
sub types to represent any kind of granted authority.
The Realm
API itself does not implement any concrete authorization control strategy, but delegates the specific authorization control strategy to one or more concrete Authorizer, relying on the Permission
type in order to discern which Authorizer
has to be used to handle a specific permission type.
A concrete Authorizer
can be registered in a Realm
instance in two ways:
-
Using the
Realm
API builder. -
Using the
addAuthorizer
method of theRealm
API.
Realm realm = Realm.builder().authorizer(AUTHORIZER1).build(); (1)
realm.addAuthorizer(AUTHORIZER2); (2)
1 | Add a Realm Authorizer using the builder API |
2 | Add a Realm Authorizer using the Realm API method |
Each Authorizer
declares the Permission type to which is bound through the getPermissionType()
method.
To check if a Realm
instance supports a specific Permission
type, the supportsPermission
API method can be used.
Realm realm = getRealm();
boolean supported = realm.supportsPermission(MyPermission.class); (1)
1 | Checks whether given Realm supports the MyPermission permission type |
So the Realm
API is itself an Authorizer
, bound to a generic Permission
type. The Authorizer
API provides a set of isPermitted(…)
methods, which accept an Authentication instance and a set of permissions to be validated according to an authenticated principal’s granted authorities, obtained through the getPermissions()
method of the Authentication
API.
The permissions check methods provided by the Authorizer
API can be categorized as follows:
Operation | Parameters type | Returns | Description |
---|---|---|---|
|
|
|
Check if the given authentication has all the specified permissions. |
|
|
|
Check if the given authentication has any of the specified permission. |
|
|
|
Check if the given authentication has all the specified permissions, using the |
|
|
|
Check if the given authentication has any of the specified permission, using the |
For standard role based authorization controls, the Authorizer
API provides a set of methods which accept the String representation of a permission, which can be obtained using the Permission.getPermission()
method.
Realm realm = getRealm();
Authentication principal = getAuthentication();
boolean permitted = realm.isPermitted(principal, new MyPermission()); (1)
permitted = realm.isPermittedAny(principal, new MyPermission(), new AnotherPermission()); (2)
permitted = realm.isPermitted(principal, "role1"); (3)
permitted = realm.isPermittedAny(principal, "role1", "role2"); (4)
1 | Checks if the MyPermission instance is granted to given principal |
2 | Checks if any of MyPermission or AnotherPermission is granted to given principal |
3 | Checks if the role1 role name (i.e. the String permission representation) is granted to given principal |
4 | Checks if the role1 or the role2 role name (i.e. the String permission representation) are granted to given principal |
See the next sections for details about authorizers and permission representations.
6.1.10. Authorizer
As stated in the Realm authorization strategy description, the Realm
API relies on the registered Authorizer
instances to perform the actual authorization controls, according to a specific Permission
type.
The Authorizer API represents a concrete authorizations control strategy, using a specific Permission
type to represent a granted authority. The Permission
type to which an Authorizer
is bound is provided by the getPermissionType()
method.
Default Authorizer
A default Authorizer
implementation is provided and can be obtained using the Authorizer.create()
static method. Alternatively, the default Authorizer
can be created and registered in a Realm
instance at the same time using the withDefaultAuthorizer()
Realm builder method.
Authorizer<Permission> defaultAuthorizer = Authorizer.create(); (1)
Realm realm = Realm.builder().withDefaultAuthorizer() (2)
.build();
1 | Create the default Authorizer implementation |
2 | Create a default Authorizer an register it in the Realm instance |
The default Authorizer
, since it is bound to the generic Permission
type, is able to handle any kind of permission and uses the following authorization control strategy:
-
Checks if the
Authentication.isRoot()
method returnstrue
: if so, the authorization control is skipped andtrue
is always returned from theisPermitted(…)
methods. -
Get the
Authentication
granted permissions using thegetPermissions()
method. -
Compares the
Authentication
granted permissions (if any) with the provided permissions to check, using the standardequals(…)
method to compare a single permission to another.
To use a custom Permission type consistently with the default Authorizer , the equals() /hashCode() comparison logic of the concrete permission type must be provided accordingly.
|
See Authentication for more inormation about the authenticated principal’s granted permissions. |
6.1.11. Permission
The Permission API is the representation of a granted authority.
The actual meaning of a specific Permission
type is implementation dependent and it is bound to the concrete Authorizer
which implements the authorization control strategy.
The Permission
API makes available a getPermission()
method, which returns the Optional String representation of the permission, if the permission type can be represented as a String retaining a sufficient precision to be relied upon for authorization control decisions.
Default Permission
A default Permission
implementation is available and can be obtained through the Permission.create(String permission)
static method.
The default Permission
implementation can be used as a role name representation. The role name is returned from the getPermission()
method and it is used as permission identity in the equals()
/hashCode()
based comparison logic.
Permission permission = Permission.create("myrole"); (1)
Optional<String> roleName = permission.getPermission(); (2)
1 | Create a default Permission using the myrole role name |
2 | The getPermission() will return the myrole String |
Authorization control example
Below is provide a simple example on how to use the default Authorizer
and the default Permission
implementation to perform authorization controls:
final Permission p1 = Permission.create("role1"); (1)
final Permission p2 = Permission.create("role2"); (2)
Authentication authc = Authentication.builder("test").permission(p1).permission(p2).build(); (3)
// Realm with default authorizer
Realm realm = Realm.builder().withDefaultAuthorizer().build(); (4)
// permission checking
boolean permitted = realm.isPermitted(authc, p1); (5)
permitted = realm.isPermitted(authc, "role1"); (6)
permitted = realm.isPermittedAny(authc, p1, p2); (7)
permitted = realm.isPermittedAny(authc, "role1", "role2"); (8)
boolean notPermitted = realm.isPermitted(authc, "other_role"); (9)
1 | Create a default Permission using the role1 role name |
2 | Create a default Permission using the role2 role name |
3 | Build an Authentication and grant the two previously defined permission to it |
4 | Create a Realm and register the default Authorizer |
5 | Verify the p1 permission is permitted |
6 | Verify the role1 role name is permitted |
7 | Verify the p1 or the p2 permission is permitted |
8 | Verify the role1 or role2 role name is permitted |
9 | The other_role role name is not permitted |
6.1.12. Realm
as a Context resource
When a Realm
is managed using the Holon Platform Context architecture, and the default name, i.e. the Realm
class name, is used as context resource key, the getCurrent()
and require()
methods can be used to obtain the current Realm
instance from context.
final Realm realm = Realm.builder().build();
Context.get().classLoaderScope().map(scope -> scope.put(Realm.CONTEXT_KEY, realm)); (1)
Optional<Realm> currentRealm = Realm.getCurrent(); (2)
Realm requiredRealm = Realm.require(); (3)
1 | Register a Realm instance in context using the classLoader default scope |
2 | Obtain the Realm context resource |
3 | Require the Realm context resource, throwing an exception if not available |
6.2. Authentication credentials
The Holon Platform provides a support for authentication credentials management, relying on the following structures:
-
The Credentials API to represent credentials data, for example a secret access token and the encoding informations related to it.
-
The CredentialsContainer API to be used as a credentials data holder (for example, the stored account informations related to a principal and the credentials provided by a principal with an authentication request).
-
The CredentialsMatcher API to deal with credential validation, to check if two credentials representations match.
6.2.1. Credentials creation
The Credentials interface provides a builder to create and encode a Credentials
representation, basing on the String
representation of the secret access token (for example, a password).
The Credentials
builder provides method to encode a the secret representation by using a hashing algorithm (specifying also a salt and the hash iterations to be performed), specifing an optional expiry date and applying encoding methods, for example Base64
.
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 |
6.2.2. Credentials encoding
To encode credentials data, for example for storing purposes, the Credentials
interface provides an Encoder
interface, which can be obtained through the encoder()
static method.
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 |
6.2.3. Credentials container
The CredentialsContainer API represents a credentials holder, providing the credentials data through the getCredentials()
.
The credentials data is provided using a generic Object
type, since the specific credentials representation is higly dependent from the concrete implementations.
The AuthenticationToken API is an example of CredentialsContainer
.
6.2.4. Credentials matching
Credentials matching can be performed using the CredentialsMatcher API.
The CredentialsMatcher
API can be used to compare two credentials representations, providing the providing the respective CredentialsContainer
instances.
The Holon Platform provides a default CredentialsMatcher
implementation which can be obtained through the defaultMatcher()
method from the CredentialsContainer
interface.
The default credentials matcher tries to employ best-practices and common behaviours to perform credentials validation and matching:
-
Obtain the credentials representations using the
getCredentials()
method of the Credentials container representation. -
Try to convert generic Object credentials data into a byte array:
-
Supports
char[]
,String
,File
andInputStream
for direct bytes conversion. -
Supports the
Credentials
type, using thegetSecret()
method to obtain the credentials secret representation.
-
-
If the provided credentials data are of
Credentials
type and an expiry date is provided, it checks the credentials are not expired. -
It checks 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.
6.3. Account
The Holon Platform provides an abstraction of an Account structure, which represents information about a principal.
The Account API 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.
-
An optional map of generic account details, using a
Map
ofString
type detail key and genericObject
type value. -
An optional set of permissions granted to the account, using the Permission representation.
-
Whether the account is enabled.
-
Whether the account is locked.
-
Whether the account is expired.
A builder is available to create an Account
instances:
Account.builder("accountId") (1)
.enabled(true) (2)
.locked(false) (3)
.expired(false) (4)
.credentials(Credentials.builder().secret("pwd").hashAlgorithm(Credentials.Encoder.HASH_SHA_256)
.base64Encoded().build()) (5)
.root(false) (6)
.permission(new MyPermission()) (7)
.permission("role1") (8)
.detail("name", "TheName").detail("surname", "TheSurname") (9)
.build();
1 | Create an Account with accountId as account id |
2 | Set the account as enabled |
3 | Set the account as not locked |
4 | Set the account as not expired |
5 | Set the account credentials using the Credentials API builder: set pwd as secret, hashed with SHA-256 and encoded using Base64 |
6 | The account is not a root account |
7 | Add a permission using a custom MyPermission type |
8 | Add a role type default permission, using role1 as role name |
9 | Set two account details |
6.3.1. AccountProvider
The AccountProvider API can be used to provide Account instances using the account id, for example from a data store.
The AccountProvider
method to obtain an Account
by id is:
Optional<Account> loadAccountById(String id);
The method returns an Optional
value: when empty means that an Account
with given id is not available from the account provider.
AccountProvider accountProvider = accountId -> { (1)
if ("test".equals(accountId)) {
return Optional.of(Account.builder(accountId)
// configure account
// ...
.build());
}
return Optional.empty();
};
1 | An AccountProvider which provides only the Account bound to the test account id |
6.3.2. Account Authenticator
A default Authenticator is provided to perform Account
based authentication, using an AccountProvider to access accounts data and the AccountCredentialsToken
type to represent the authentication request.
The account authenticator strategy is defined as follows:
-
Obtain the authentication request credentials using the Account credentials authentication token type.
-
Check if an
Account
with the account id obtained from the tokengetPrincipal()
method is available, using the AccountProvider provided at authenticator creation time. -
If so, checks if the account credentials obtained from the token
getCredentials()
method matches with the credentials provided by the loadedAccount
instance.
The account authenticator can be obtained by using the authenticator(AccountProvider accountProvider)
static method of the Account
interface.
AccountProvider accountProvider = getAccountProvider(); // build or obtain the AccountProvider to use
Authenticator<AccountCredentialsToken> authenticator = Account.authenticator(accountProvider); (1)
1 | Obtain an account authenticator using given AccountProvider |
The authenticator builder method shown above uses the default Credentials matching to perform account credentials checks.
To provide a custom CredentialsMatcher
, the following creation method can be used:
Authenticator<AccountCredentialsToken> authenticator = Account.authenticator(getAccountProvider(),
new MyCredentialsMatcher() (1)
);
1 | Set a custom MyCredentialsMatcher as authenticator CredentialsMatcher |
AccountCredentialsToken
The default account authenticator uses the AccountCredentialsToken type to represent account authentication requests.
This token type returns:
-
The account id (i.e. the principal name) as a
String
from thegetPrincipal()
method. -
The account secret as
byte[]
from thegetCredentials()
method.
The Account
interface provides AccountCredentialsToken
creation methods:
AuthenticationToken token = Account.accountCredentialsToken("accountId", "secret"); (1)
token = Account.accountCredentialsToken("accountId", new byte[] { 1, 2, 3 }); (2)
1 | Create an account AuthenticationToken providing account id and secret |
2 | Create an account AuthenticationToken providing account id and secret as an array of bytes |
Account authenticator example
Below an example on how to use an account authenticator with a Realm
.
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)) (2)
.withDefaultAuthorizer().build();
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: this provider always provide an Account instance, setting pwd as credentials secret |
2 | Register an account Authenticator which uses the previously defined AccountProvider in the Realm instance |
3 | Perform authentication using an account credentials authentication token type |
6.4. AuthContext
The AuthContext API can be used to represent the current authentication and authorization context.
The AuthContext
API:
-
Acts as a holder of the current
Authentication
, providing methods to check if anAuthentication
is available and obtain it. -
Provides a method to remove the current context
Authentication
. -
Provides methods to perform authentication operations.
-
Provides methods for authorization controls using the current
Authentication
. -
As an
AuthenticationNotifier
, supports AuthenticationListener registration to be notified when a successfull authentication is performed or when anAuthentication
is not available anymore.
The default AuthContext
implementation relies on a Realm
instance to perform concrete authentication and authorization operations, which must be provided at AuthContext
creation time.
See the Realm section for detailed information about Realm operations and configuration.
|
6.4.1. Authentication
The AuthContext API provides two methods to perform authentication requests:
-
authenticate(AuthenticationToken authenticationToken)
to perform an authentication using anAuthenticationToken
as authentication request representation. -
authenticate(Message<?, ?> message, String… schemes)
to perform an authentication aMessage
and optional authentication scheme names.
The authentication process is completely delegated to the backing Realm instance: See the Realm authentication section and the MessageAuthenticator section for details about the two authentication strategies, respectively.
6.4.2. Current Authentication
When an authentication request made using the AuthContext
API is successful, the current Authentication
reference is made available from the AuthContext
instance and can be inspected and obtained using the methods of the AuthenticationInspector API, extended by the AuthContext
API.
Operation | Returns | Description |
---|---|---|
|
|
Checks whether an |
|
|
Get the current |
|
|
Get the current |
To remove the current Authentication
, the unauthenticate()
method can be used. After this method is called, a new successful authentication request has to be made to make available a new context Authentication
.
Below
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)
boolean notAlreadyAuthenticated = context.isAuthenticated(); (4)
context.authenticate(AuthenticationToken.accountCredentials("test", "pwd")); (5)
Authentication authc = context.requireAuthentication(); (6)
context.unauthenticate(); (7)
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 Realm instance |
4 | An Authentication is not available from the AuthContext since no authentication operation was performed yet |
5 | Trigger an authentication request by using an account credentials authentication token |
6 | If the authentication request is successful, the current Authentication is available from the AuthContext |
7 | Unauthenticate the context, i.e. remove the current Authentication |
6.4.3. Custom Authentication holder
A custom authentication holder can be used at AuthContext
creation time, to customize the current Authentication
handling.
To provide a custom current Authentication
handling logic, the AuthenticationHolder interface can be implemented and provided at AuthContext
creation time.
class ThreadLocalAuthenticationHolder implements AuthenticationHolder { (1)
static final ThreadLocal<Authentication> CURRENT_AUTHENTICATION = new ThreadLocal<>();
@Override
public Optional<Authentication> getAuthentication() {
return Optional.ofNullable(CURRENT_AUTHENTICATION.get());
}
@Override
public void setAuthentication(Authentication authentication) {
CURRENT_AUTHENTICATION.set(authentication);
}
}
public void customAuthenticationHolder() {
AuthContext.create(getRealm(), new ThreadLocalAuthenticationHolder()); (2)
}
1 | Create an AuthenticationHolder that uses a ThreadLocal variable to handle the current Authentication reference |
2 | Set the ThreadLocalAuthenticationHolder as AuthContext authentication holder |
6.4.4. Authentication listeners
The AuthContext
API, through the AuthenticationNotifier
API, supports AuthenticationListener registration to be notified when a successfull authentication is performed or when an Authentication
is not available anymore.
When the AuthContext
is unauthenticated using the unauthenticate()
method, i.e. when the current Authentication
is removed from the AuthContext
, a null
Authentication
value is provided to the registered authentication listeners.
The Realm and AuthContext authentication listeners are considered as separate sets, but since the AuthContext uses its configured Realm to perform authentications, when the authentication is performed from the AuthContext , also the Realm authentication listeners will be triggered. Vice-versa, when the authentication is performed from the Realm , the AuthContext authentication listeners will not be triggered.
|
6.4.5. AuthContext
as a Context resource
When an AuthContext
is managed using the Holon Platform Context architecture, and the default name, i.e. the AuthContext
class name, is used as context resource key, the getCurrent()
and require()
methods can be used to obtain the current AuthContext
instance from context.
final AuthContext authContext = AuthContext.create(getRealm());
Context.get().classLoaderScope().map(scope -> scope.put(AuthContext.CONTEXT_KEY, authContext)); (1)
Optional<AuthContext> currentAuthContext = AuthContext.getCurrent(); (2)
AuthContext requiredAuthContext = AuthContext.require(); (3)
1 | Register an AuthContext instance in context using the classLoader default scope |
2 | Obtain the AuthContext context resource |
3 | Require the AuthContext context resource, throwing an exception if not available |
6.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).
See for example the Holon Platform JAX-RS module or the Holon Platform Vaadin module documentation to learn about some @Authenticate
annotation use cases.
6.6. JWT support
Maven coordinates:
<groupId>com.holon-platform.core</groupId>
<artifactId>holon-auth-jwt</artifactId>
<version>5.1.1</version>
The holon-auth-jwt
artifact provides the support for the JSON Web Token standard, providing a full integration with 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.
6.6.1. Configuration
The JwtConfiguration interface represents the default JWT configuration provider for the Holon Platform JWT support APIs.
It makes available a set of methods to obtain the JWT configuration attributes to be used to generate and parse a JSON Web Token.
A JwtConfiguration
instance can be obtained in two ways:
1. Using the provided builder:
JwtConfiguration cfg = JwtConfiguration.builder() (1)
.issuer("MyIssuer") (2)
.expireTime(10000) (3)
.includeDetails(true) (4)
.includePermissions(true) (5)
.signatureAlgorithm(JwtSignatureAlgorithm.HS256) (6)
.sharedKey(new byte[] { 1, 2, 3 }) (7)
.build();
1 | Obtain a JwtConfiguration builder |
2 | Set the JWT token issuer |
3 | Set the token expire time in milliseconds |
4 | Include the Authentication details in JWT token generation |
5 | Include the Authentication permissions in JWT token generation |
6 | Sign the JWT using HS256 (HMAC using SHA-256) as signature algorithm |
7 | Set the shared key to use with the symmetric signing algorithm |
2. Using a configuration property set:
The JwtConfigProperties property set can be used to provide the JWT configuration attributes using the standard Configuration property set API.
The JWT configuration property set is bound to the property name prefix holon.jwt.
The JWT configuration properties are currently used by the JwtTokenBuilder API to create a JWT from an Authentication
and by the JwtAuthenticator to parse a JWT for authentication purposes.
The available configuration properties are listed here below, also indicating which API use a specific property value:
Name | Type | Default | Used by | Meaning |
---|---|---|---|---|
holon.jwt. issuer |
String |
None |
|
The JWT token issuer. Corresponds to the JWT |
holon.jwt. signature-algorithm |
String |
None |
|
JWT signature algorithm name: must be one of the algorithm names listed in the JwtSignatureAlgorithm enumeration |
holon.jwt. sharedkey-base64 |
String |
None |
|
JWT sign shared key, |
holon.jwt. privatekey.source |
String |
None |
|
JWT sign private key source for asymmetric signing algorithms. The |
holon.jwt. privatekey.format |
String |
PKCS#8 |
|
JWT sign private key format: must be one of the one of the formats listed in the KeyFormat enumeration. For private keys, the supported format values are |
holon.jwt. privatekey.encoding |
String |
Base64 |
|
JWT sign private key encoding: must be one of the one of the encodings listed in the KeyEncoding enumeration. The supported encodings are |
holon.jwt. publickey.source |
String |
None |
|
JWT sign public key source for asymmetric signing algorithms. The |
holon.jwt. publickey.format |
String |
X.509 |
|
JWT sign public key format: must be one of the one of the formats listed in the KeyFormat enumeration. For public keys, the supported format values are |
holon.jwt. publickey.encoding |
String |
Base64 |
|
JWT sign public key encoding: must be one of the one of the encodings listed in the KeyEncoding enumeration. The supported encodings are |
holon.jwt. not-before-now |
Booelan (true/false) |
|
|
Whether to automatically set the |
holon.jwt. expire-ms |
Integer number |
None |
|
JWT token expire time in milliseconds. |
holon.jwt. expire-seconds |
Integer number |
None |
|
JWT token expire time in seconds. |
holon.jwt. expire-minutes |
Integer number |
None |
|
JWT token expire time in minutes. |
holon.jwt. expire-hours |
Integer number |
None |
|
JWT token expire time in hours. |
holon.jwt. expire-days |
Integer number |
None |
|
JWT token expire time in days. |
holon.jwt. include-details |
Booelan (true/false) |
|
|
Whether to include |
holon.jwt. include-permissions |
Booelan (true/false) |
|
|
Whether to include |
When the private and/or public key for an asymmetric signing algorithm is provided using a keystore, for example when the PKCS#12
format is used, a set of additional configuration properties are available to configure the key store passwords and alias:
Name | Type | Meaning |
---|---|---|
holon.jwt. privatekey.store.password |
String |
The key store password to use when the JWT sign private key is provided using a key store format such as |
holon.jwt. privatekey.store.alias |
String |
The key store key alias to use when the JWT sign private key is provided using a key store format such as |
holon.jwt. privatekey.store.alias-password |
String |
The key store key recovering password to use when the JWT sign private key is provided using a key store format such as |
holon.jwt. publickey.store.password |
String |
The key store password to use when the JWT sign public key is provided using a key store format such as |
holon.jwt. publickey.store.alias |
String |
The key store key alias to use when the JWT sign public key is provided using a key store format such as |
holon.jwt. publickey.store.alias-password |
String |
The key store key recovering password to use when the JWT sign public key is provided using a key store format such as |
This configuration property set API can be used to build a JwtConfiguration instance using the build(JwtConfigProperties properties)
method.
JwtConfiguration cfg = JwtConfiguration
.build(JwtConfigProperties.builder().withPropertySource("jwt.properties").build()); (1)
1 | Load the JWT configuration property set from a properties file named jwt.properties |
See JwtConfiguration auto-configuration to learn how to rely on the Spring Boot auto-configuration features to automatically create a JwtConfiguration bean using JWT configuration properties.
|
6.6.2. Supported JWT signing algorithms
The JwtSignatureAlgorithm enumeration provides a list of the support JWT signing algorithms.
When a signing shared key is provided in JWT configuration and a signature algorithm is not specified, the HMAC using SHA-256 default signature algorithm is used.
|
For asymmetric signing algorithms such RSA
, the key pair to use can be loaded using the JWT configuration properties from different sources and using different key formats (X.509
, PKCS#8
, PKCS#11
, PKCS#12
) and encodings (Base64
, PEM
).
See the previous section for the list of the available JWT configuration properties.
6.6.3. Building a JWT from an Authentication
The JwtTokenBuilder API can be used to create JSON Web Tokens using an Authentication as source and a JwtConfiguration
instance to provide the token configuration attributes.
The Authentication
instance will be used to:
-
Set the JWT subject (
sub
) value using theAuthentication
principal name. -
If configured, include the
Authentication
details as JWT claims. -
If configured, include the
Authentication
permissions as JWT claims.
The JWT id (jit
) can be specified at token built time.
JwtConfiguration configuration = JwtConfiguration
.build(JwtConfigProperties.builder().withPropertySource("jwt.properties").build()); (1)
Authentication authc = Authentication.builder("test").build(); (2)
String jwt = JwtTokenBuilder.get().buildJwt(configuration, authc); (3)
jwt = JwtTokenBuilder.get().buildJwt(configuration, authc, UUID.randomUUID().toString()); (4)
1 | Build a JwtConfiguration instance using the jwt.properties file |
2 | Build an Authentication |
3 | Build a JWT using given configuration and authentication |
4 | Build a JWT using given configuration and authentication and set a random id as token id |
Authentication
permissions claim
When the JwtConfiguration
method isIncludePermissions()
returns true
, the Authentication
permissions will be included in JWT using the AuthenticationClaims CLAIM_NAME_PERMISSIONS
claim name.
See Authentication for details about the Authentication permissions representation.
|
Only the Permission
which provide a String
representation through the Permission.getPermission()
method will be taken into account, using the permission String
representation as claim value.
The actual JWT claim value will by a String
array of the authentication permissions String
representation.
See Permission for details about the permission String representation.
|
JwtConfiguration configuration = JwtConfiguration.builder().includePermissions(true).build(); (1)
Authentication authc = Authentication.builder("test").permission("role1").permission("role2").build(); (2)
String jwt = JwtTokenBuilder.get().buildJwt(configuration, authc); (3)
1 | Build a JwtConfiguration instance and set to true the include permissions switch |
2 | Build an Authentication with a default permission named role1 (permission String representation) and a default permission named role2 |
3 | Build a JWT using given configuration and authentication: the token will include a ATH$prms claim name with the value ['role1','role2'] |
Authentication
details claims
When the JwtConfiguration
method isIncludeDetails()
returns true
, the Authentication
details will be included in JWT as claims.
The Authentication
details are obtained through the ParameterSet API, extended by the Authentication
interface, as a map of String
value detail keys and generic Object
detail values.
Each not null Authentication
detail will be writter in JWT using the detail key as claim name and the detail value as claim value.
JwtConfiguration configuration = JwtConfiguration.builder().includeDetails(true).build(); (1)
Authentication authc = Authentication.builder("test").parameter("name", "John").build(); (2)
String jwt = JwtTokenBuilder.get().buildJwt(configuration, authc); (3)
1 | Build a JwtConfiguration instance and set to true the include details switch |
2 | Build an Authentication with a parameter named name with value John |
3 | Build a JWT using given configuration and authentication: the token will include a name claim name with the value John |
6.6.4. Building an Authentication
from a JWT
The JwtTokenParser API can be used to create an Authentication instance from a JSON Web Token value, using a JwtConfiguration
instance to provide the token configuration attributes.
The JWT is validated before building the Authentication
instance, and an error is thrown if the validation fails, for example if the token is malformed, expired or the signatire is not valid.
The Authentication
instance is created with the following strategy:
-
The JWT subject (
sub
) value is used asAuthentication
principal name. -
If configured, any JWT claim is included as an
Authentication
detail parameter, using the claim name as parameter key. -
If configured, the default
ATH$prms
JWT claim is parsed to obtain the correspondingAuthentication
permissions.
The JwtTokenParser
API returns an Authentication.Builder
instance, allowing to perform additional Authentication
configuration before obtaining the actual Authentication
instance.
JwtConfiguration configuration = JwtConfiguration.builder().includeDetails(true).includePermissions(true)
.build(); (1)
String jwt = getJwt();
Authentication authc = JwtTokenParser.get().parseJwt(configuration, jwt).build(); (2)
1 | Build a JwtConfiguration instance and set to include the JWT claims as authentication details and to parse the default ATH$prms JWT claim to obtain the Authentication permissions |
2 | Build an Authentication from given JWT value |
6.6.5. Performing authentication using JWT
The JwtAuthenticator interface represents an Authenticator to handle JWT based authentication and can be registered in a Realm to enable JWT authentication capabilities.
A JWT authentication request is represented through a Bearer authentication token type, where the Bearer value must represent the JWT serialization and it is used by the JwtAuthenticator
to validate the JWT and provide an Authentication
obtained from the token.
The JwtAuthenticator
API relies on a JWTConfiguration definition as JWT configuration properties source to consistently parse and validate the JWT.
Additionally, a JwtAuthenticator
can support:
-
An optional set of allowed JWT issuers: If one ore more allowed issuer is setted, the JWT issuer claim (
iss
) will be required and checked during token authentication: if the token issuer doesn’t match one of the allowed issuers, the authentication will fail. -
An optional set of required claims: If one or more required claim is configured, the specified claim must exist in the JWT, otherwise the authentication will fail.
To obtain a JwtAuthenticator
, the provided builder method can be used.
JwtConfiguration configuration = JwtConfiguration.builder() (1)
.issuer("MyIssuer") // JWT token issuer
.expireTime(10000) // 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(JwtSignatureAlgorithm.HS256) // use HS256 as signature algorithm
.sharedKey(new byte[] { 1, 2, 3 }) // shared key to use with the symmetric signing algorithm
.build();
// JWT authenticator
JwtAuthenticator jwtAuthenticator = JwtAuthenticator.builder().configuration(configuration) (2)
.issuer("allowedIssuer") (3)
.requiredClaim("myClaim") (4)
.build();
// Realm
Realm realm = Realm.builder().authenticator(jwtAuthenticator) (5)
.withDefaultAuthorizer().build();
Authentication authc = realm.authenticate(AuthenticationToken.bearer("TheJWTtokenHere...")); (6)
realm = Realm.builder().authenticator(jwtAuthenticator) //
.resolver(AuthenticationToken.httpBearerResolver()) (7)
.withDefaultAuthorizer().build();
HttpRequest request = obtainHttpRequest();
authc = realm.authenticate(request); (8)
1 | Build a JwtConfiguration |
2 | Build a JwtAuthenticator using the configuration |
3 | Set allowedIssuer as allowed JWT issuer |
4 | Set the myClaim JWT claim as required |
5 | Build a Realm and register the JwtAuthenticator |
6 | Perform an authentication request using a BearerAuthenticationToken with the JWT value |
7 | Build a Realm with a JwtAuthenticator and a Bearer HTTP message resolver |
8 | Perform an authentication request using a HttpRequest message: the message must provide a Bearer Authorization type message header with the JWT value |
JWT to Authentication
The JwtAuthenticator
API parses the JWT to obtain Authentication
instance from it, if the token is valid and well formed.
The Authentication
obtained from the JSON Web Token is created with the following rules:
-
The principal name is obtained from the JWT subject (
sub
) claim and it is required: if not available, anUnknownAccountException
is thrown. -
The authentication scheme is set to
Bearer
. -
If the default permissions claim is found (i.e. a claim named
ATH$prms
, see theCLAIM_NAME_PERMISSIONS
constant of the ink:../api/holon-core/com/holonplatform/auth/jwt/AuthenticationClaims.html[AuthenticationClaims^] interface), it is expected to be a String array of role names. For each role name, a default Permission is created with given name and granted to theAuthentication
. -
Any other JWT claim is setted as an
Authentication
parameter.
See Authentication for more information about the Authentication
API.
7. Spring ecosystem integration
The holon-spring
artifact provides integration with the Spring framework and auto-configuration features using Spring Boot.
Maven coordinates:
<groupId>com.holon-platform.core</groupId>
<artifactId>holon-spring</artifactId>
<version>5.1.1</version>
7.1. Spring beans as Context resources
The EnableBeanContext annotation can be used on Spring Configuration
classes to configure a Context scope which uses the Spring ApplicationContext
to provide Spring bean definitions as context resources.
See Context for information about the Holon Platform Context architecture. |
The scope name is spring-context
and the scope registration priority order is an intermediate value between the default thread scope (highest priority) and classloader scope (lowest priority).
This scope is a read-only scope, i.e. it does not allow direct resource registration or removal using the put
and remove
context scope methods.
7.1.1. Context resources lookup strategy
The Spring context scope, when a context resource is requested through the Holon Platform Context API, checks if a Spring bean definition 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 requested type is available, this is returned;
-
Otherwise, if the
lookupByType()
attribute value of the@EnableBeanContext
annotation istrue
and a Spring bean of the required type, ignoring its name, is present and only one candidate is available, this instance is returned.
The lookupByType() attribute of the @EnableBeanContext annotation is true by default.
|
The lookup strategy can be also configured using the Environment
configuration property: holon.context.bean-lookup-by-type
. Set it to true
to enable the lookup by type strategy or false
to disable it.
The Spring scopes conventions are respected, in the sense that when a resource is requested and a matching bean is found, the bean instance lookup is performed using the standard Spring BeanFactory
API, involving any registered and active Spring scope.
@Configuration
@EnableBeanContext (1)
class SpringConfig {
@Bean(name = "testResource") (2)
public TestResource testResource() {
return new TestResource();
}
}
public void getContextResource() {
// lookup by name and type
Optional<TestResource> resource = Context.get().resource("testResource", TestResource.class); (3)
// lookup by type
resource = Context.get().resource(TestResource.class); (4)
}
1 | The @EnableBeanContext is used on a Spring Configuration class to enable the Spring context scope. The lookupByType() attribute is true by default. |
2 | TestResource type bean definition with the testResource name |
3 | Get a TestResource type using the testResource key: the TestResource Spring bean will be returned |
4 | Get a TestResource type without specifying the resource key: since the lookup by type strategy is active, the same TestResource Spring bean will be returned |
7.2. EnvironmentConfigPropertyProvider
The EnvironmentConfigPropertyProvider API can be used to build a ConfigPropertyProvider
which uses the Spring Environment
structure as property source.
Any property available from the Spring Environment
will be available, respecting names and value types, from the ConfigPropertyProvider
instance.
See Configuration properties for information about configuration property providers. |
org.springframework.core.env.Environment environment = obtainSpringEnvironment(); (1)
// build a ConfigPropertyProvider using Spring Environment as property source
ConfigPropertyProvider provider = EnvironmentConfigPropertyProvider.create(environment); (2)
String value = provider.getProperty("test.property.name", String.class); (3)
1 | Obtain the Spring Environment , for example using dependency injection |
2 | Create a EnvironmentConfigPropertyProvider using the Spring environment |
3 | Any property available from the Spring environment will be available from the property provider too, with the same name and value type |
7.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 API to obtain the current tenant id.
In order for the scope to be active and available, a bean of TenantResolver
type must be configured and available in the current BeanFactory
(i.e. in the current Spring ApplicationContext
).
That TenantResolver type bean will be used to obtain the current tenant id, if available, using the getTenantId()
method.
To enable the tenant scope, the EnableTenantScope annotation can be used on Spring configuration classes.
The scope name is tenant
, and Spring beans can be registered with this scope using either:
-
The default Spring
@Scope("tenant")
annotation. -
Or the convenience ScopeTenant annotation.
@Configuration
@EnableTenantScope (1)
class TenantScopeConfig {
@Bean
public TenantResolver tenantResolver() { (2)
// provide a meaningful current tenant id resolution strategy...
return () -> Optional.of("test");
}
@Bean
@ScopeTenant (3)
public TestResource testResource() {
// a different instance of the bean will be provided for each tenant id
return new TestResource();
}
}
1 | Use the @EnableTenantScope annotation on a Spring Configuration class to enable the tenant scope |
2 | A TenantResolver type bean must be provided for current tenant id resolution |
3 | The convenience @ScopeTenant annotation can be used to declare a Spring bean as tenant scoped |
7.3.1. TenantResolver
lookup strategy
By default, the tenant scope tries to detect a TenantResolver bean type in current application context, to be used as current tenant id provider.
If a TenantResolver
bean type is not available, or more than one TenantResolver
type bean definition is present, the tenant scope setup will fail, throwing an ApplicationContextException
at Spring application context startup time.
When more than one TenantResolver
bean type is present, the TenantResolver
bean definition to be used with the tenant scope can be configured providing the TenantResolver
bean name. This can be done in two ways:
-
Using the
tenantResolver
attribute of the@EnableTenantScope
annotation. -
Using a Spring
Environment
configuration property namedholon.tenant-scope.tenant-resolver
. This configuration method has precedence on the annotation attribute value.
7.3.2. Tenant scope ScopedProxyMode
The default ScopedProxyMode
of the @ScopeTenant
annotation is INTERFACES
.
This proxy mode allows to configure a proxy for the tenant-scoped Spring component, in order to inject (autowire) it in other Spring components with a different scope.
The INTERFACES
mode create a JDK dynamic proxy implementing all interfaces exposed by the class of the target object.
The proxyMode()
attribute of the ScopeTenant annotation can be used to change the scoped proxy mode.
7.3.3. Tenant scoped beans lifecycle
A tenant scoped bean instance is created the first time the bean is requested with a specific tenant id.
From now on, the bean instance will survive for the whole Spring application context lifecycle, likewise a singleton scoped bean. This because the tenant scope handler it has no way of knowing if a tenant id is no more available and when this will happen, since it is highly dependent from the concrete application architecture and tenant resolution strategy.
To avoid memory wastage and to ensure Spring context cleanliness, the TenantScopeManager API can be used to manage tenant scoped beans lifecycle.
When a tenant id is not valid or available anymore, the discardTenantBeanStore(String tenantId)
API method can be invoked to destroy the bean store bound to given tenant id, i.e. to remove all the tenant scoped bean instances which refer to the tenant id, triggering any associated bean destruction callback.
If the enableTenantScopeManager
attribute of the @EnableTenantScope
annotation is set to true
(the default value), a TenantScopeManager
bean type is automatically created an registered in the SPring application context. This way, it can be simply obtained, for example, using dependency injection.
@Autowired
TenantScopeManager tenantScopeManager;
void discardTenantScopedBeans() {
tenantScopeManager.discardTenantBeanStore("a_tenant_id"); (1)
}
1 | Discard the tenant scoped bean instances for the a_tenant_id tenant id using the TenantScopeManager API |
7.4. Datastore configuration
The Spring integration module provides a number of methods to extend and configure a Datastore when a Datastore
instance is registered as a bean in the Spring context.
See the Datastore section for information about the Datastore API.
|
7.4.1. DatastoreResolver
The DatastoreResolver annotation can be used to annotate ExpressionResolver type beans to automatically register them into a Datastore
implementation.
The datastoreBeanName()
annotation attribute can be used to uniquely identify the Datastore
bean into which register the
resolver, if more than one Datastore
type bean is present in the Spring application context.
7.4.2. DatastoreCommodityFactory
The DatastoreCommodityFactory annotation can be used to annotate DatastoreCommodityFactory type beans to automatically register them into a Datastore
implementation.
The datastoreBeanName()
annotation attribute can be used to uniquely identify the Datastore
bean into which register the
factory, if more than one Datastore
type bean is present in the Spring application context.
Each concrete Datastore implementation could provide a specific DatastoreCommodityFactory base type to be used to register commodity factories. See specific Datastore implementations documentation for further information.
|
7.4.3. DatastorePostProcessor
The DatastorePostProcessor interface can be used to configure a Datastore
bean, right after it is initialized in the Spring application context.
A Spring bean class implementing this interface is automatically detected and the method
postProcessDatastore(Datastore, String)
is called at Datastore
bean initialization time.
The Datastore
bean instance and the Datastore
bean name are provided as method parameters. When more than one Datastore
type bean is present in the Spring application context, the postProcessDatastore
will be called one time for each available Datastore
bean.
For example, the post processor can be used to register additional ExpressionResolver
or DatastoreCommodityFactory
.
The DatastorePostProcessor type beans must be registered using the singleton scope.
|
7.5. RestClient
implementation using Spring RestTemplate
The Spring integration module provides a RESTful client API implementation using the Spring RestTemplate
API.
See the RESTful client API documentation for information about the RestClient API.
|
The Spring RestClient
implementation is represented by the SpringRestClient interface, which provides a create(RestTemplate restTemplate)
method to create a RestClient
instance using a provided Spring RestTemplate
implementation.
RestTemplate restTemplate = getRestTemplate(); (1)
RestClient client = SpringRestClient.create(restTemplate); (2)
1 | Create or obtain a RestTemplate implementation |
2 | Create a RestClient using the RestTemplate implementation |
When a RestTemplate
instance is available as a Holon Platform Context resource, a RestClientFactory
is automatically registered to provide a SpringRestClient
implementation using that RestTemplate
implementation. This way, the default RestClient.create(…)
static methods can be used to obtain a RestClient
implementation.
If the <<Spring context scope is enabled with the default beans lookup strategy, it is sufficient that a RestTemplate bean type is registered in the Spring application context to obtain it as a context resource.
|
@Configuration
@EnableBeanContext (1)
class Config {
@Bean (2)
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
void restclient() {
RestClient client = RestClient.create(); (3)
client = RestClient.create(SpringRestClient.class.getName()); (4)
}
1 | Use the @EnableBeanContext to enable Spring beans context |
2 | Provide a RestTemplate bean definition |
3 | The RestClient.create() method can be used to obtain a RestClient implementation backed by the defined RestTemplate bean definition |
4 | If more than one RestClientFactory is available, the SpringRestClient class name can be specified to ensure that a SpringRestClient type is obtained as a RestClient implementation |
7.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 the core Spring Boot auto-configuration capabilities, the following artifact must be included in your project dependencies:
<groupId>com.holon-platform.core</groupId>
<artifactId>holon-spring-boot</artifactId>
<version>5.1.1</version>
See below for the available auto-configuration features.
7.6.1. Spring context scope auto-configuration
The Spring context scope is automatically enabled.
This has the same effect as using the @EnableBeanContext
annotation on Spring configuration classes, with the lookupByType()
attribute set as true by default.
The holon.context.bean-lookup-by-type
configuration property name can be used to configure the resource lookup strategy, enabling or not the lookup by type mode.
See the Spring beans as Context resources documentation section for details. |
To disable this auto-configuration feature, the EnableBeanContextAutoConfiguration class can be excluded from the Spring Boot auto-configuration classes:
@EnableAutoConfiguration(exclude={EnableBeanContextAutoConfiguration.class})
7.6.2. Spring tenant scope auto-configuration
If a TenantResolver type bean is available in Spring application context and a single candidate can be determined, the Spring tenant scope is automatically registered and enabled.
This has the same effect as using the @EnableTenantScope
annotation on Spring configuration classes.
See the Spring tenant scope documentation section for details about the tenant scope. |
To disable this auto-configuration feature, the TenantScopeAutoConfiguration class can be excluded from the Spring Boot auto-configuration classes:
@EnableAutoConfiguration(exclude={TenantScopeAutoConfiguration.class})
7.6.3. JwtConfiguration
auto-configuration
When at least one property of the holon.jwt.
property set is available in Spring environment, and a JwtConfiguration type bean is not already present in the Spring application context, a JwtConfiguration
bean is automatically created and setted up according to the holon.jwt.
configuration property values.
See the JWT configuration section for details about the available JWT configuration properties.
For example, to setup a JwtConfiguration
bean using a symmetric signing algorithm, the following configuration properties can be provided using the YAML format:
holon:
jwt:
issuer: example-issuer
signature-algorithm: HS512
sharedkey-base64: "eWGZLlCrUjtBZwxgzcLPnA"
expire-hours: 1
Or when an asymmetric signing algorithm is used:
holon:
jwt:
issuer: example-issuer
expire-hours: 1
signature-algorithm: RS256
privatekey:
encoding: PEM
source: >
-----BEGIN RSA PRIVATE KEY-----
MIICeAIBADANBgkqhkiG9w0BAQEFAASCAmIwggJeAgEAAoGBALv07pB1uFK4fQ3QlHcRSCofMWovYpYp
hO2Jh31h2hIivC3TbFzWn07pL14d8ec8LIoIYWZAn4L9ZUpEzCPr3nbHpdoPaEcrpqXlgpjO/Jgf8Ysa
QPWq7ArjWr/ifiORA3vRg2OVhEGD309BccYh9peh/IOpt9EfmWioYlidO+S/AgMBAAECgYB1zoY8y1w1
lObk4sg7fPyDUjvRt1OOlQV5MQtYPh3F4jmaa3rvEaKWfjevQQufCKtN9QS/Z1/TZWm4TDi7hxpOu6YZ
gVL9JYHwOvb8opX9Yle9FyLRv4pPdhUkHs7ahzmhPPAf0kSjwKAYlqBmTUzZY5HTRZy/ffpVftPwcl50
mQJBAOJAromanqe6PDpxnL4IGcPPyn0dWQ3VyTV+i1XkZ8d60nBoLUriG8Ok+ehj4eiEYeK4Ca7GPciM
EqkZc54XrjsCQQDUq0TRB3V+1mVjJtMixN4I1nb5lo2MVASDjvl/3LCv7LxCZErWLpCjPivMrii+OOAr
k1VenXV7uTLD/Si9HKdNAkEAlUbn4ZJKq4+MvWLIb/kYRsGKcBI095PeNZVQiVMxxcObpN6XQ5j7iJII
8PM10hvGGbgja1UQ3ojMpxVL2zr0kQJBANQt1Ejgsj9L1HfqQnjMBeK3Zph5ttus75v6R79kBfxfqyxq
N6gdaT0VSEm78PZodG/FXUU6v/4ith2INN8I+XkCQQCq77unFpv3OESzhNRa0hjJgAAiwvAqwrWRxLHT
DijzpQ4PNDfR32bTV/pB9i0nJAPce+9cB7ahx+vpLX2jFuLu
-----END RSA PRIVATE KEY-----
publickey:
encoding: PEM
source: >
-----BEGIN RSA PUBLIC KEY-----
MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQC79O6QdbhSuH0N0JR3EUgqHzFqL2KWKYTtiYd9YdoS
Irwt02xc1p9O6S9eHfHnPCyKCGFmQJ+C/WVKRMwj6952x6XaD2hHK6al5YKYzvyYH/GLGkD1quwK41q/
4n4jkQN70YNjlYRBg99PQXHGIfaXofyDqbfRH5loqGJYnTvkvwIDAQAB
-----END RSA PUBLIC KEY-----
To disable this auto-configuration feature, the JwtAutoConfiguration class can be excluded from the Spring Boot auto-configuration classes:
@EnableAutoConfiguration(exclude={JwtAutoConfiguration.class})
7.6.4. 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.1.1</version>
The starter provides the dependency to the holon-spring-boot
artifact, enabling the auto-configuration features listed above, in addition to the default core Spring Boot starter (spring-boot-starter
), which provides auto-configuration support, logging and YAML support.
See the Spring Boot starters documentation for details about the Spring Boot starters topic and the core Spring Boot starter features.
7.7. Spring Security integration
Maven coordinates:
<groupId>com.holon-platform.core</groupId>
<artifactId>holon-spring-security</artifactId>
<version>5.1.1</version>
The holon-spring-security
artifact provides integration between the Holon Platform authentication and authorization architecture and the Spring Security components and APIs.
See the Authentication and Authorization for information about the the Holon Platform authentication and authorization architecture. |
The main entry point to deal with Spring Security integration is the SpringSecurity API interface, which provides operations to:
-
Create an
AuthContext
backed by a Spring Security SecurityContext and using a AuthenticationManager to perform authentication operations. -
Seamlessy use a Spring Security Authentication as a Holon platform Authentication representation.
-
Perform authentication in Spring Security context using standard Holon platform AuthenticationToken, through the SpringSecurityAuthenticationToken bridge interface.
-
Build a Spring Security AuthenticationProvider using a standard Holon platform Authenticator.
-
Provide adapters and conversion methods from Spring Security GrantedAuthority and standard Holon platform Permission.
7.7.1. AuthContext
API integration
The Holon platform AuthContext API can be integrated with the Spring Security context to use the SecurityContext
as current authentication holder.
The authContext()
and authContext(Realm realm)
methods of the SpringSecurity
integration API allows to create an AuthContext
which uses the Spring Security SecurityContext
as authentication holder.
This establishes a link between the Holon platform authentication Realm and the Spring security authentication context, using the AuthContext
API as bridge between the two architectures.
When a successful authentication operation is performed using Spring Security, and accordingly the current authenticated principal is available from the SecurityContext
, the AuthContext
API will provide the same
authenticated principal, represented as an Authentication. The current Authentication
is an adapter of the concrete Spring Security authenticated principal, providing the principal name, details and authorizations as Holon platform Permission representations.
This way, the AuthContext
API can be seamlessy used to inspect authenticated principal attributes and to perform authorization controls using the Holon Platform conventions.
AuthContext authContext = SpringSecurity.authContext(); (1)
UsernamePasswordAuthenticationToken tkn = new UsernamePasswordAuthenticationToken("user", "pwd",
Arrays.asList(new GrantedAuthority[] { new SimpleGrantedAuthority("role1") }));
SecurityContextHolder.getContext().setAuthentication(tkn); (2)
Authentication authc = authContext.requireAuthentication(); (3)
String name = authc.getName(); (4)
boolean permitted = authContext.isPermitted("role1"); (5)
SecurityContextHolder.getContext().setAuthentication(null); (6)
boolean notAnymore = authContext.isAuthenticated();
1 | Obtain an AuthContext API bound to a default Realm and which uses the Spring Security SecurityContext as authentication holder |
2 | Simulate an authentication operation in the Spring Security context, using a UsernamePasswordAuthenticationToken with user as principal name and role1 as granted authority |
3 | The AuthContext is now authenticated accordingly, and provided the current authenticated principal as a com.holonplatform.auth.Authentication |
4 | The provided Authentication is an adapter of the Spring Security one, with matching principal attributes and permissions. The returned principal name will be user |
5 | The AuthContext API can be used to perform authorization controls, using the granted authorities of the Spring Security authenticated principal: since the role1 authority name was granted to the principal, the isPermitted("role1") call will return true |
6 | When the authenticated principal is removed from the Spring Security context, the bound AuthContext won’t provide an authentication anymore accordingly |
When a Realm
is provided at AuthContext
build time, it can be configured with a set of Authenticator and Authorizer according to the Realm
API definition.
If an authentication operation is performed using the AuthContext
API, the backing Realm
is used for the actual authentication strategy, through the registered authorizers. When an authentication operation is successful, the authentication result is setted back in the Spring Security SecurityContext
as current authenticated principal, with the same principal attributes and permissions, represented as granted authorities.
final Realm realm = Realm.builder().withDefaultAuthorizer().authenticator(Account.authenticator(id -> { (1)
if ("usr".equals(id)) {
return Optional.of(Account.builder(id).credentials(Credentials.builder().secret("pwd").build())
.permission("role1").build());
}
return Optional.empty();
})).build();
AuthContext authContext = SpringSecurity.authContext(realm); (2)
authContext.authenticate(Account.accountCredentialsToken("usr", "pwd")); (3)
org.springframework.security.core.Authentication authc = SecurityContextHolder.getContext().getAuthentication(); (4)
String name = authc.getName(); (5)
Collection<? extends GrantedAuthority> authorities = authc.getAuthorities(); (6)
1 | Create a Realm with default authorizer and a Account authenticator with given accounts provider (in this case, a single Account named usr , with password pwd and role1 as permission) |
2 | Obtain an AuthContext API bound to the specified Realm and which uses the Spring Security SecurityContext as authentication holder |
3 | Perform authentication using the AuthContext API, providing an account credentials AuthenticationToken |
4 | If the authentication is successful, the authentication result is setted back in the Spring Security SecurityContext as current authenticated principal |
5 | The principal name will be usr |
6 | The granted authorities will include a role1 authority |
7.7.2. Use an AuthenticationManager
as a Realm Authenticator
The Spring Security AuthenticationManager
can be used as an Authenticator API, which can registered in a Realm
to provide authentication capabilities using Spring Security authentication tokens.
This allows to integrate Spring Security authentication architecture with the Holon Platform authentication architecture, and to use a Realm
(and accordingly the AuthContext
API) to perform authentication operations using the Holon Platform conventions and APIs.
An Authenticator
can be obtained from a Spring Security AuthenticationManager
using the SpringSecurity API:
AuthenticationManager authenticationManager = getAuthenticationManager(); (1)
Authenticator<SpringSecurityAuthenticationToken> authenticator = SpringSecurity
.authenticator(authenticationManager); (2)
1 | Obtain the Spring Security AuthenticationManager (for example using Spring’s dependency injection) |
2 | Create an Holon Platform Authenticator using the Spring Security AuthenticationManager |
The Authenticator
is bound to a SpringSecurityAuthenticationToken authentication token type, which must be used to provided the authentication credentials.
A SpringSecurityAuthenticationToken
instance can be built from a Spring Security Authentication instance, either using the SpringSecurity
API or the SpringSecurityAuthenticationToken
interface itself.
SpringSecurityAuthenticationToken token = SpringSecurityAuthenticationToken
.create(new UsernamePasswordAuthenticationToken("usr", "pwd")); (1)
token = SpringSecurity.asAuthenticationToken(new UsernamePasswordAuthenticationToken("usr", "pwd")); (2)
1 | Create a SpringSecurityAuthenticationToken from given Spring Security UsernamePasswordAuthenticationToken |
2 | The same operation using the SpringSecurity API |
An account(String accountId, String secret)
convenience method is provided to create Spring Security UsernamePasswordAuthenticationToken
token type.
The Authenticator
created in this way can be registered in a Realm
, thus providing authentication capabilities using the Spring Security context.
AuthenticationManager authenticationManager = getAuthenticationManager(); (1)
Realm realm = Realm.builder().withDefaultAuthorizer()
.authenticator(SpringSecurity.authenticator(authenticationManager)) (2)
.build();
Authentication authc = realm.authenticate(SpringSecurityAuthenticationToken.account("user", "pwd1")); (3)
1 | Obtain the Spring Security AuthenticationManager (for example using Spring’s dependency injection) |
2 | Create a Realm and add an authenticator using the Spring Security AuthenticationManager |
3 | Perform authentication using a username/password type authentication token |
7.7.3. Create a fully integrated AuthContext
API
According to the two previous sections, the AuthContext
can be fully integrated with the Spring Security context, that is:
-
Use the Spring Security context (through the
SecurityContext
API) as the current authentication holder. -
Use the Spring Security
AuthenticationManager
as authenticator to perform authentication operations using the Spring Security environment.
Of course, you can mix the AuthenticationManager
based authentication with any other Authenticator
registered in the Realm
to which the AuthContext
is bound.
The SpringSecurity API provides methods to easily create AuthContext
instances which uses the Spring Security context as the current authentication holder and backed by a Realm
with AuthenticationManager
based authentication capabilities.
AuthenticationManager authenticationManager = getAuthenticationManager(); (1)
AuthContext authContext = SpringSecurity.authContext(authenticationManager); (2)
authContext = SpringSecurity.authContext(authenticationManager, true); (3)
1 | Obtain the Spring Security AuthenticationManager (for example using Spring’s dependency injection) |
2 | Create an AuthContext instance which uses the Spring Security context as the current authentication holder and backed by a Realm with AuthenticationManager based authenticator |
3 | With this method, is also registered an Authenticator for the default AccountCredentialsToken which uses the Spring Security AuthenticationManager to perform the authentication operations. |
7.7.4. Use a Holon Authenticator
as Spring Security AuthenticationProvider
A default Holon Authenticator can be also adapted to be used as a Spring Security AuthenticationProvider, which can be used to implement new authentication strategies in the Spring Security environment.
This way, any Holon platform Authenticator
(either builtin or custom) can be used as a Spring Security authentication processor.
The authenticationProvider(Authenticator<T> authenticator, Class<A> authenticationType, Function<A, T> converter)
method of the SpringSecurity API can be used for this purpose, providing the function to be used to to convert a Spring Security Authentication
into the Holon platform AuthenticationToken
type which will be used as the Authenticator
authentication credentials.
@Configuration
@EnableGlobalAuthentication
class Config {
@Bean
public AccountProvider accountProvider() { (1)
return id -> {
if ("usr1".equals(id)) {
return Optional.of(Account.builder(id).credentials(Credentials.builder().secret("pwd1").build())
.permission("view").build());
}
if ("usr2".equals(id)) {
return Optional.of(Account.builder(id).credentials(Credentials.builder().secret("pwd2").build())
.permission("view").permission("manage").build());
}
return Optional.empty();
};
}
@Bean
public AuthenticationManager authenticationManager(AuthenticationManagerBuilder auth,
AccountProvider accountProvider) throws Exception { (2)
return auth.authenticationProvider( (3)
SpringSecurity.authenticationProvider(Account.authenticator(accountProvider), (4)
UsernamePasswordAuthenticationToken.class, (5)
upt -> AccountCredentialsToken.create(upt.getPrincipal().toString(),
upt.getCredentials().toString()) (6)
)).build();
}
}
1 | Create an AccountProvider bean which provides account infos to be used by the Holon Platform Account authenticator |
2 | Create an AuthenticationManager Spring Security bean, to which the adapted Authenticator will be added |
3 | Add a Spring Security AuthenticationProvider using a Holon Platform Authenticator |
4 | Create an Account type Authenticator |
5 | Use the UsernamePasswordAuthenticationToken class as Spring Security authentication token type |
6 | Provide the function to be used to convert an UsernamePasswordAuthenticationToken into the AccountCredentialsToken token type required by the authenticator |
7.7.5. Permissions and authorizations
The SpringSecurity API provides methods to seamlessy use a Holon platform Permission as a Spring Security GrantedAuthority
and vice-versa.
The String
representation of the two types is guaranteed to be preserved, i.e. the value returned from the getPermission()
method of a Permission
obtained from a GrantedAuthority
will be the same returned by the getAuthority()
method and vice-versa.
GrantedAuthority ga = new SimpleGrantedAuthority("role1");
Permission permission = SpringSecurity.asPermission(ga); (1)
Permission p = Permission.create("role2");
GrantedAuthority grantedAuthority = SpringSecurity.asAuthority(p); (2)
1 | Create a Permission from a GrantedAuthority |
2 | Create a GrantedAuthority from a Permission |
7.7.6. Spring Security starter
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-security</artifactId>
<version>5.1.1</version>
This starter includes the base Holon Platform starter (see the Spring Boot auto-configuration section), the standard Spring Security starter (spring-boot-starter-security
) and the holon-spring-security
dependency, to provide the Holon Platform Spring Security integration components and APIs.
8. 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 theProperty
architecture, includingPropertyBox
, property presenters and renderers -
query
: for logs related toQuery
definition and execution -
datastore
: for logs related toDatastore
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 asRestClient
-
com.holonplatform.spring
: for logs related to Spring integration
9. System requirements
9.1. Java
The Holon Platform core module requires Java 8 or higher.