The cartridge pf_property provides the possibility for defining so called Property Groups. Those groups are intended to defining an minimum set of properties that belong together to control a feature, e.g., an account property group would consist of a login and a password. The cartridge pf_property supplies features to define such groups, reading/storing from/to a persistent layer and for the lookup if a fallback hierarchy needs to be established.
The cartridge pf_property provides the way to define such property groups via annotated Java interfaces. This way it is possible to write code that referes to such properties of a property group.
@PropertyGroup public interface Account { @NotNull String getLogin(); @NotNull String getPassword(); }
This glossary describes the terms used here ( typewriter
style denoted terms directly correspond with classes/interfaces names):
Term | Description |
---|---|
Property Group | An atomic set of properties required for a single feature. |
Property | A named attribute of a property group that holds a value. |
The result of a property definition is a com.intershop.platform.property.capi.model.PropertyGroupDescriptor
. Although, pf_property is actually independent of the actual creation of such a PropertyGroupDescriptor
. It is recommended to define static property groups as annotated Java interfaces. The advantages are that certain checks are performed at compile time and references for usage do not need to rely on string equality of weakly dependent components (that can easily break without notification for the developer).
A property group is defined by adding the annotation @PropertyGroup
to a Java interface.
@PropertyGroup public interface MyProperties { String getProperty1(); int getProperty2(); }
This example defines the property group MyProperties
which contains the properties property1
and property2
.
The names of the properties strictly follow the rule of the Java bean specification. Actually the java.beans.Introspector is used to discover the properties.
Java interface may inherit from each other. Thus property groups can also inherit all attribute from a base property group.
@PropertyGroup public interface MyExtendedProperties extends MyProperties { String getProperty3(); }
This example creates a new property group MyExtendedProperties
that consist of the properties property1
, property2
and property3
.
All properties of super interfaces are also included regardless of they are declared as a property group or not.
Property values are owned by a (business) object. Initially this object does not contain a value for a property group, but the code using the value should not need to perform consistency checks. So if the code really requires a value and there is a technically defined meaningful default the framework provides a way to define such a default within the property group definition.
A default value for a property group can be defined with the annotation @DefaultObject
. This annotation takes a parameter which must be a class that implements the property group interface. The default constructor is used for instantiation.
@PropertyGroup @DefaultObject(DefaultGroup.Default.class) public static interface DefaultGroup { String getValue(); public static class Default implements DefaultGroup { @Override public String getValue() { return "default"; } } }
Instances of the class that is the default value will always be created when a property value lookup has to return the default value. This allows, e.g., for localized properties to return a correct value according to the current executional context.
When configuring account information, e.g., to external systems like payment services, there is often a secret part that must not be exposed to unauthorized people. Such a secret like a password can be marked with the annotation @Secret
. Therefore, all code that accesses such a property should not log, audit or expose in any other way a actual value for this property.
Example:
@PropertyGroup interface Account { String getUserName(); @Secret String getPassword(); }
By default secret properties will be shown as password input fields in the default property editor and will not be displayed as clear text. This may be inconvenient for secret properties like credit card numbers where it's desirable that only a certain part of the number is displayed and the rest is garbled (e.g., "************1234"). For this reason the secret annotation features some attributes which allow to specify a different display type:
displayType
- May be one of the following:ENCRYPTED
- Value will be displayed encrypted completely (Default).
GARBLED_LEFT
- Value will be displayed encrypted on the left side, the rightmost characters will be visible as clear text
GARBLED_RIGHT
- Value will be displayed encrypted on the right side, the leftmost characters will be visible as clear text
clearTextLength
- Specifies the amount of characters which will be displayed as clear text if the display type is set to GARBLED_LEFT
or GARBLED_RIGHT
. By default the clearTextLength
is 0 - all hidden.Garbled secrets will use a standard text editor when editing in the property editor by default.
A property can be localized by adding of the annotation @Localized
to the declared read method. This means that the persistence layer can store different values for different locales. On read access when actually a method without parameter is called the system returns the value for the current locale retrieved from the execution context.
@PropertyGroup public interface MyRule { String getRuleExpression(); @Localized String getRuleDescription(); }
The PersistenceHandler
that is responsible for storing and reading such properties actually does not make a difference between localized and parameterized properties. The actual parameter type is java.util.Locale
.
The locale fallback is only performed within the same instance of the property group. The property group fallback chain is not related to the standard locale fallback. See The Fallback Strategy Provider for details.
In certain scenarios a property value depends not only on the owning object of the property group but also on other parameters that are available at runtime. Thus parameterized properties can be defined.
Example:
@PropertyGroup public interface BasketProperties { Money getMaxOrderValue(Currency currency); }
Unlike in pure Java, overloading is not allowed and overriding must match the method signature exactly.
The PersistenceHandler
for such a property group must be able to cope with parameters of the defined type.
On property access no automatic fallback is performed.
Properties may hold multiple values at once. To achieve this it is sufficient to declare the type of the property to be a list. The filled generic parameter of java.util.List<E>
tells the framework the actual property data type.
@PropertyGroup public interface MyListProperties { List<Integer> getIntegers(); List<? extends String> getStrings(); }
On read access the framework ensures that the method never returns null
and the returned list are not modifiable.
Other types of collections including sub types of List
and arrays are not handled as multi value properties by the framework. They would appear as single element properties. Thus there are neither generic editors nor generic persistency handlers.
A property group may hold values that are also property groups of their own. A special handling for them makes it possible to reuse the editors and persistence for basic (primitive) types to build complex data structure with configurations.
@PropertyGroup interface Point { int getX(); int getY(); } @PropertyGroup interface Rectangle { Point getUpperLeft(); Point getLowerRight(); }
Self references and cyclic references are supported:
@PropertyGroup interface SelfReference { SelfReference getAnother(); } @PropertyGroup interface Cycle1 { Cycle2 getCycle2(); } @PropertyGroup interface Cycle2 { Cycle1 getCycle1(); }
For actual object graphs currently only trees are supported. Any PersistenceHandler
implementation should be able to cope with tree structures that are not too large.
A property group may hold properties that have a dependency to another property that signals which values are actually needed for a feature. To support this the framework provides the possibility to define a property that is a selector of properties and so called conditional (depending) properties. Conditional properties are only needed (for the feature) if the selector holds a certain value.
Two types of return values for selector properties are supported: String
and Enum
.
@PropertyGroup interface Shapes { @Selector({"Circle", "Square", "RoundedSquare"}) String getShape(); @Selection({"Circle", "RoundedSquare"}) float getRadius(); @Selection({"Square", "RoundedSquare"}) float getWidth(); }
To be able to refer to the real enum values at the depending properties custom Java annotations must be declared. This is because of a limitation for declaring annotations that only allow concrete enum types and not a base class for them. The custom selection annotation must be annotated with @SelectionDefinition
and contain the method value()
that returns an array of values of the enum type.
Also the custom annotation needs to be marked with RetentionPolicy.RUNTIME
and ElementType.METHOD)
.
@PropertyGroup interface Shapes { @SelectionDefinition @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.METHOD) @interface Selection { enum Option {CIRCLE, SQUARE, ROUNDEDSQUARE}; Option[] value(); }; @Selector Selection.Option getShape(); @Selection({CIRCLE, ROUNDEDSQUARE}) float getRadius(); @Selection({SQUARE, ROUNDEDSQUARE}) float getWidth(); }
If multiple selectors should be provided within a single property group, for the conditional properties it has to be defined to which selector they belong. This is especially true if the selectors return the same type of object (string or the same enum). Therefore, an additional annotation is provided: @SelectionGroup
.
Example:
@PropertyGroup interface ComplexConditionalExample { @Selector({"A", "B"}) String getSelector1(); @Selector({"B", "C"}) String getSelector2(); // Enabled if selector1 holds value "A" @Selection("A") int getA(); // Enabled if selector1 or selector2 holds value "B" @Selection("B") int getB1(); // Enabled if selector1 holds value "B" @SelectionGroup("selector1") @Selection("B") int getB2(); // Enabled if selector1 or selector2 is not null @SelectionGroup({"selector1", "selector2"}) int getX(); // Always enabled int getY(); // A selector property can also depend on another selection // Enabled if selector1 holds value "A" or selector2 holds value "C" @Selection({"A", "C"}) @Selector({"S", "T"}) String getSelector3(); }
Selector properties can be neither localized nor parameterized.
For not active properties:
A handler selector property means a property that delivers a custom instance of a defined interface. In contrast, the instance that implements the interface is configured with parameters only defined by the actual class of the instance (and not of the interface).
This means, that within the code something like this has to be implemented:
MyHandler handler = configuration.getHandler(); handler.doSomething();
But at this point it is only known that a handler with a defined interface exists.
interface MyHandler { void doSomething(); }
Also there should be a configuration that can provide a handler instance:
interface MyHandlerConfiguration { MyHandler getHandler(); }
Actual implementations of the MyHandler
interface are subject of extensions and should be able to provide their own set of configuration parameters.
This is achieved with the annotation HandlerSelector
for the configuration property:
@PropertyGroup interface MyHandlerConfiguration { @HandlerSelector(@ExtensionPoint(id="handler", type=MyHandlerFactory.class)) MyHandler getHandler(); }
with MyHandlerFactory
defined as:
// C is the placeholder for the actual configuration type interface MyHandlerFactory<C> extends HandlerFactory<MyHandler, C> { // needed methods are defined HandlerFactory }
The HandlerSelector
annotation requires an extension point declaration that can easily be used in extension/custom code:
class MyHandlerImpl implements Handler { MyHandlerImpl(MyHandlerImplConfig config) { ... } @Overwrite void doSomething(String message) { ... } } @PropertyGroup interface MyHandlerImplConfig { ... } public static class MyHandlerImplFactory implements MyHandlerFactory<MyHandlerImplConfig> { @Override public Class<MyHandlerImplConfig> getConfigurationType() { return MyHandlerImplConfig.class; } @Override public MyHandler getHandler(MyHandlerImplConfig configuration) { return new MyHandlerImpl(configuration); } }
<extensionBindings type="java" extensionPoint="MyHandlerFactory-handler" extension="MyHandlerImplFactory"/>
With this kind of declaration the code that uses a handler configuration is completely independent of the actual configuration of the handler implementations, but the UI still allows to present the list of available handler implementations and (depending on the selection) the configuration parameters for this selected implementation.
Extension points are application specific. This means the list of available handlers in general differs for different applications. This implies that the management of the handler configuration and the usage should be made in the context of the same application, or the compatibility must be ensured by any other means for the management application and the application that uses the handler configuration.
The JSR-349 defines a mechanism for validating Java Beans based on annotated Java classes. It defines a set of predefined annotations and a way to define custom annotations for declaring constraint on classes, methods and parameters. This Java Bean Validation standard also specifies details for the validation process and error reporting.
Property groups are not Java Beans and the Bean Validation Specification is strictly dedicated to them. Nevertheless, we want to rely on this specification to define constraints for property groups. The main limitation of this standard is the usage of the Java Beans. This means only an object graph of Java Beans can be validated and this object graph is also the source for the constraints that have to be validated, and the constraints must be annotations. The property groups are the data access level that internally relies on an object graph, built based on generic Java objects with a relation to a dedicated meta model that contains the constraints.
That is why no standard implementation of the Java Beans Validation specification can be used. This means that only a well defined sub set of feature will work. There are also some differences in special features.
Currently the following features are implemented:
The following sub sections show only the relation and integration of the Bean Validation standard for the property groups feature.
Enforcing the constraints is performed by the property engine. See section Working with Property Groups for details.
The Bean Validation standard that requires to explicitly use the annotation @Valid
to perform validation of a complete graph. Unlike this, for properties with a value type that is also a property group the validation is automatically performed for those property values.
Since Intershop 7 relies on the Hibernate Validator implementation the standard annotations are supported. The Hibernate Validator comes also with some additional useful constraint that can be used (like @NotEmpty
, @NotBlank
and @URL
).
Custom constraint annotations can be defined as described in the standard (or more precisely as the Hibernate Validator has implemented the standard). This includes building constraint compositions and multi-valued Constraints.
The @NotNull annotation declares that a property of a property group is only valid if it is not null
. This annotation has no effect for multi value properties of a property group. For localized and parameterized properties, this only means that null
values can not be stored, but there is no guarantee that a value for each locale or parameter exist.
@PropertyGroup public interface Account { @NotNull String getLogin(); @NotNull String getPassword(); }
Mandatory input fields for users are a standard use case. The descriptor for a property provides a mandatory flag to mark such fields. This flag is also influenced by this @NotNull annotation independently if the property is marked directly with @NotNull
or the property is marked with a composite constraint that includes @NotNull
at any level. @NotEmpty
and @NotBlank
are example for such composites.
Custom constraints can be defined as it is described in the Beans Validation specification. There is a small limitation when implementing custom constraint validators: The methods for adding nodes in the ConstraintViolationBuilder
have no function.
It is possible to place constraints on properties and also on the property group itself. In that case the actually object to be evaluated is not an instance of the property group interface, but a PropertyGroupValue
.
Example for a constraint that is directly attached to a property group:
@Constraint(validatedBy = {NotEqualValuePropertiesConstraintValidator.class}) @Target({ElementType.TYPE}) @Retention(value = RetentionPolicy.RUNTIME) @Documented public @interface NotEqualValueProperties { // a property for the custom constraint String[] properties(); // The required message property String message() default "{examples.NotEqualValueProperties.message}"; // The Bean Validation standard requires the group and payload properties, but they are not used for property group validation Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; }
import com.intershop.platform.property.capi.PropertyGroupValue; public static class NotEqualValuePropertiesConstraintValidator implements ConstraintValidator<NotEqualValueProperties, PropertyGroupValue> { private HashSet<String> properties; @Override public void initialize(NotEqualValueProperties constraintAnnotation) { properties = new HashSet<String>(Arrays.asList(constraintAnnotation.properties())); } @Override public boolean isValid(PropertyGroupValue value, ConstraintValidatorContext context) { if (value == null) { return true; } ArrayList<Object> values = new ArrayList<Object>(properties.size()); for(String p : properties) { Object v = value.getProperty(p); if (v != null) { if (values.contains(v)) { return false; } values.add(v); } } return true; } }
@PropertyGroup @NotEqualValueProperties(properties={"p1", "p2"}, message="The properties 'p1' and 'p2' must not contain the same value.") public interface ValidationExample { String getP1(); String getP2(); }
A validation failure messages can be set via the message
property of the constraint annotation or via the ConstraintValidatorContext
within a ConstraintValidator
implementation. But the localization mechanism does not fulfill all features mentioned in the specification. Especially parameterized messages are not supported.
Message text can come from the localization framework or can be placed directly into the property group definition:
@PropertyGroup public interface MessagesExample { @MyConstraint(message="{this.is.localization.key}") String getProperty1(); @MyConstraint(message="This text comes is directly printed.") String getProperty1(); }
Property group interfaces and methods may also have custom annotations. These annotations are accessible via the PropertyGroupDescriptor
and PropertyDescriptor
for those property groups. Methods are:
<A extends Annotation> A getAnnotation(Class<A> annotationClass); Collection<? extends Annotation> getAnnotations(); boolean isAnnotationPresent(Class<? extends Annotation> annotationClass);
Unlike as for Java classes the descriptors for property groups contain also all annotations for from super interfaces. There is a special rule if the same annotation type is used multiple times in the inheritance tree:
To gain profit of the declarative way for defining property groups there are three additional use cases that need to be covered. These are:
The central point for those use cases is the PropertyEngine
(provided by pf_property). The PropertyEngine
manages the annotation based property group definition and provides the generic API to read and store property values. The cartridge pf_property provides currently an abstract implementation for such an engine because essential features depend on a specific definition of certain aspects that are only known at application level. The actual implementation of the PropertyEngine
is contained in the cartridge core where a singleton instance is managed by the PropertyEngineMgr
.
Business objects provide a more convenient API that hides the PropertyEngine
from application developers.
public interface PropertyEngine { // Return value for annotation based defined property groups <T> T getConfiguration(Object start, Class<T> propertyGroupInterface) throws IllegalArgumentException, ConstraintViolationException; // Return value for any property group either annotation based or custom <T> T getConfiguration(Object start, PropertyGroupDescriptor propertyGroup) throws IllegalArgumentException, ConstraintViolationException; ... }
public interface BusinessObject { // Return value for annotation based defined property groups for the current object as lookup start <T> T getConfiguration(Class<T> propertyGroupInterface) throws IllegalArgumentException, ConstraintViolationException; // Return value for any property group either annotation based or custom for the current object as lookup start <T> T getConfiguration(PropertyGroupDescriptor propertyGroup) throws IllegalArgumentException, ConstraintViolationException; ... }
Both methods return the property group value for a given property group and owning object. The owning object is called start object here since it is not meant to be the physical owner of the returned value but the most specific owner for the property group value in a specific business context. E.g., the business logic may ask the engine for a value for a user, but the concrete value can be managed/stored at a user group that the user belongs to. Also it may happen that no value is defined at all by a business user, then a technical default value defined with the property group is returned (defining such a default value is optional).
If the annotation based approach is used then both methods return instances of the property group defining interface (but only the first ensures this at compile time). If a custom PropertyGroupDescriptor
is used then a java.util.Map
is returned that holds the property group values.
The methods are using the IllegalArgumentException
to signal if something is wrong with the property group definition or the property engine configuration. The ConstraintViolationException
is thrown if the stored object is not valid. The persistence handler provider was configured to throw an exception in this case, see The Persistence Handler Provider.
See Configuration of the Property Engine for more information how the actual implementation retrieves property values.
public interface PropertyEngine { ... // Return the directly stored values from the owning object for an annotation based property group PropertyGroupValue getConfigurationForOwner(Object owner, PropertyGroupDescriptor propertyGroup) throws IllegalArgumentException; // Store values for any property group at an owning object void setConfigurationAtOwner(Object owner, PropertyGroupValue value) throws IllegalArgumentException, ConstraintViolationException; // Remove the values stored for a property group from the owner object void removeConfigurationFromOwner(Object owner, PropertyGroupDescriptor propertyGroup) throws IllegalArgumentException; // Return the actual meta model for a property group defined via Java annotations PropertyGroupDescriptor getPropertyGroupDescriptor(Class<?> propertyGroupInterface) throws IllegalArgumentException; }
Managing properties/configurations is usually made by business users via an UI component. Such an UI component internally works always with a generic meta and data model. That is why the management method that read and write on their owning objects use the PropertyGroupValue
to pass the property values.
Values for parameterized properties are mapped with the parameter value as the key and the property value as the entry value.
Note
If a property is of type of a property group the value of such a property is also stored as a PropertyGroupValue
(unlike to the usage API methods). Similarily, localized properties are represented here in the same ways as a parameterized property with parameter type java.util.Locale
.
The property engine provided by core has tree customization/extension points:
All three customization/extension points can be overwritten/filled in a application specific way (in terms of Intershop 7 applications). This is achieved by defining the application specific object PropertyEngine
via the Component Framework, which is actually only the configuration used by the global singleton engine instance.
Defintion provided by core:
<implementation name="PropertyEngine" class="com.intershop.beehive.core.internal.property.PropertyEngineConfiguration" start="start"> <requires name="localeFallbackProvider" contract="LocaleFallbackProvider" cardinality="1..1" /> <requires name="persistenceHandlerProvider" contract="PersistenceHandlerProviderEntry" cardinality="0..n" /> <requires name="fallbackStrategyProvider" contract="FallbackStrategyProviderEntry" cardinality="0..n" /> </implementation> <instance name="PropertyEngine" with="PropertyEngine" scope="app"/>
The locale fallback provider defines the lookup order for localized properties. This means if a user searches a value for a certain locale, but there is no value stored for this locale a fallback is made to another locale and so on.
An implementation must fulfill to interface:
public interface LocaleFallbackProvider { // Return an ordered list of locales List<Locale> getLocales(); }
The core
cartridge contains an implementation that follows the Intershop 7 standard rule:
This rule is already registered with the core cartridge:
<fulfill requirement="localeFallbackProvider" of="PropertyEngine"> <instance name="PropertyEngine-LocaleFallback" with="DefaultLocaleFallback" scope="app"/> </fulfill>
When retrieving a property group value for business usage it is not of interest if the actual values are stored at the same business object as it is currently requested for. E.g., if a value for a user is wanted, but the value is actually only managed at the user group, then a fallback strategy can be registered so that for a user the user group is returned that owns the concrete value.
The registration is split into three parts the FallbackStrategyProviderEntry
, the FallbackStrategyProvider
and the FallbackStrategy
.
The FallbackStrategy
determines for a given object a parent object:
public interface FallbackStrategy<O> { Object getParentScope(O currentScope); }
The FallbackStrategyProvider
selects for a given object/(object type) and property group an appropriate FallbackStrategy
:
public interface FallbackStrategyProvider { <O> FallbackStrategy<? super O> getFallbackStrategy(O currentScope, PropertyGroupDescriptor propertyGroup); }
The FallbackStrategyProviderEntry
is used to bring all defined providers into a well defined order so that it is possible to define generic fallback and override them for special cases in custom code:
<implementation name="FallbackStrategyProviderEntry" class="..."> <!-- highest priority values first --> <requires name="priority" contract="Double" cardinality="1..1" /> <requires name="provider" contract="FallbackStrategyProvider" cardinality="1..1" /> </implementation>
The core cartridge already defines one FallbackStrategyProvider
:
<implementation name="TypeAndGroupConditionalFallbackStrategyProvider" class="..."> <requires name="ownerType" contract="String" cardinality="0..n" /> <requires name="propertyGroup" contract="String" cardinality="0..n" /> <requires name="strategy" contract="FallbackStrategy" cardinality="1..1" /> </implementation>
This TypeAndGroupConditionalFallbackStrategyProvider
selects the declared strategy if an only if a value for an owning object of the given data type and given property group is requested. An empty type or property group list means "any" type/group.
Thus the registration would look as follows:
<fulfill requirement="fallbackStrategyProvider" of="PropertyEngine"> <instance with="FallbackStrategyProviderEntry" scope="app"> <fulfill requirement="priority" value="10"/> <fulfill requirement="provider"> <instance with="TypeAndGroupConditionalFallbackStrategyProvider"> <fulfill requirement="ownerType" value="com.intershop.beehive.core.capi.user.User"/> <fulfill requirement="propertyGroup" value="com.example.UserProperty"/> <fulfill requirement="strategy"> <instance with="com.example.UserPropertyFallbackStrategy"/> </fulfill> </instance> </fulfill> </instance> </fulfill>
Values of property groups can be stored in many different ways, e.g., in property files, in custom attributes of extensible objects, in preference tables or in native attributes of their owning objects or what ever is imaginable. A persistence handler is the class that actually performs the storing and reading of the data from Java into a target and vice versa.
The registration is split into three parts the PersistenceHandlerProviderEntry
, the PersistenceHandlerProvider
and the PersistenceHandler
.
The PersistenceHandler
handler performs the reading and writing of property group values:
public interface PersistenceHandler<O> { // Check if the owner object type is supported by this implementation boolean isOwnerSupported(Object owner); // Check if the property group data types is supported by this implementation boolean isGroupSupported(PropertyGroupDescriptor propertyGroup); // read the property group value PropertyGroupValue getValue(O owner, PropertyGroupDescriptor propertyGroup); // store the value void setValue(O owner, PropertyGroupValue value); // remove the property group value void removeValue(O owner, PropertyGroupDescriptor propertyGroup); }
The PersistenceHandlerProvider
selects for a given object/(object type) and property group an appropriate PersistenceHandler
and tells the PropertyEngine
if validation needs to be performed:
public interface PersistenceHandlerProvider { <O> PersistenceHandler<? super O> getPersistenceHandler(O owner, PropertyGroupDescriptor propertyGroup); ValidationMode getReadValidationMode(); }
The PersistenceHandlerProviderEntry
is used to bring all defined providers into a well defined order so that it is possible to define generic persistence and override them for special cases in custom code:
<implementation name="PersistenceHandlerProviderEntry" class="..."> <!-- highest priority values first --> <requires name="priority" contract="Double" cardinality="1..1" /> <requires name="provider" contract="PersistenceHandlerProvider" cardinality="1..1" /> </implementation>
The core cartridge already defines one PersistenceHandlerProvider
:
<implementation name="TypeAndGroupConditionalPersistenceHandlerProvider" class="..."> <requires name="ownerType" contract="String" cardinality="0..n" /> <requires name="propertyGroup" contract="String" cardinality="0..n" /> <requires name="handler" contract="PersistenceHandler" cardinality="1..1" /> <requires name="validateAfterRead" contract="PersistenceHandlerProvider.ValidationMode" cardinality="0..1"/> </implementation>
This TypeAndGroupConditionalPersistenceHandlerProvider
selects the declared strategy if an only if a value for an owning object of the given data type and given property group is requested. An empty type or property group list means "any" type/group. For validateAfterRead
the values NO_VALIDATION
, VALIDATE_AND_LOG
and VALIDATE_AND_THROW
are supported. If no value is defined then no validation is performed.
Thus the registration would look as follows:
<fulfill requirement="persistenceHandlerProvider" of="PropertyEngine"> <instance with="PersistenceHandlerProviderEntry" scope="app"> <fulfill requirement="priority" value="10"/> <fulfill requirement="provider"> <instance with="TypeAndGroupConditionalPersistenceHandlerProvider"> <fulfill requirement="ownerType" value="com.intershop.beehive.core.capi.user.User"/> <fulfill requirement="propertyGroup" value="com.example.UserProperty"/> <fulfill requirement="handler"> <instance with="com.example.UserPropertyPersistenceHandler"/> </fulfill> </instance> </fulfill> </instance> </fulfill>
ExtensibleObjectAttributeValuePersistenceHandler
The ExtensibleObjectAttributeValuePersistenceHandler
is a generic persistence handler that stores property group values as AttributeValues
at any ExtensibleObject
. Secret string properties are stored encrypted.
Supported property data types:
ExtensibleObject
, except references to other persistent objects (methods getObject()
and putObject()
char
/ Character
is stored as a string, all others are stored according with the best matching type)Enum<?>
Class<?>
Supported parameter data types for parameterized properties:
String
java.util.Locale
Enum<?>
boolean
/ Boolean
Limitations:
ExtensibleObject
is not used ( getText()
/ putText()
. String property are always stored with putString()
.Locale
.Mapping of properties to attribute names:
The ExtensibleObjectAttributeValuePersistenceHandler
has a built-in default mapping of property group property names into attribute value names. This default mapping can be overwritten by defining a custom mapping for the properties.
package example; @PropertyGroup interface Tree { String getName(); List<Integer> getNodeValue(String parameter); List<Tree> getChildren(); }
// as JSON Tree : { "name": "root", "children": [ { "name": "c0" }, { "name": "c1", "parameterizedNodeValue": { "a": [1, 2], "b": [3, 4] } } ] } // Stored attribute values example.Tree:name = "root"; example.Tree:children.0.name = "c0"; example.Tree:children.1.name = "c1"; example.Tree:children.1.nodeValue.a = [1, 2]; // multiple integer attribute value example.Tree:children.1.nodeValue.b = [3, 4];
As the example shows:
Parameters are stored in the keys. That is why types are limited.
Note
The special characters "\." are escaped with a leading "\". Locales for localized and locale parameterized properties do not become part of the attribute value names instead localized attribute values are stored.
Customizing the attribute value names can be done by:
Configuration via Component File:
// Definitions: <implementation name="ExtensibleObjectAttributeValuePersistenceHandler" implements="PersistenceHandler" class="..."> <requires name="propertyGroup" contract="PropertyPersistenceHandler.Mapping" cardinality="0..n" /> </implementation> <implementation name="PropertyPersistenceHandler.Mapping" implements="PropertyPersistenceHandler.Mapping" class="..."> <requires name="propertyGroup" contract="String" cardinality="1..1" /> <requires name="prefix" contract="String" cardinality="0..1" /> <requires name="property" contract="PropertyPersistenceHandler.Mapping.Key" cardinality="0..n"/> </implementation> <implementation name="PropertyPersistenceHandler.Mapping.Key" implements="PropertyPersistenceHandler.Mapping.Key" class="..."> <requires name="property" contract="String" cardinality="1..1" /> <requires name="key" contract="String" cardinality="1..1" /> </implementation>
<instance with="PropertyPersistenceHandler.Mapping"> <fulfill requirement="propertyGroup" value="example.Tree"/> <fulfill requirement="prefix" value="tree:"/> <fulfill requirement="property"> <instance with="PropertyPersistenceHandler.Mapping.Key"> <fulfill requirement="property" value="name"/> <fulfill requirement="key" value="root"/> </instance> </fulfill> </instance>
results in:
// Stored attribute values tree:root = "root"; tree:children.0.name = "c0"; tree:children.1.name = "c1"; tree:children.1.nodeValue.a = [1, 2]; // multiple integer attribute value tree:children.1.nodeValue.b = [3, 4];
ExtensibleObjectBOAttributeValuePersistenceHandler
The ExtensibleObjectBOAttributeValuePersistenceHandler
is the equivalent to the ExtensibleObjectAttributeValuePersistenceHandler
for business objects with implementation inheriting from AbstractExtensibleObjectBO<E extends ExtensibleObject>
.
Configuration via Component File:
// Definitions: <implementation name="ExtensibleObjectBOAttributeValuePersistenceHandler" implements="PersistenceHandler" class="..."> <requires name="propertyGroup" contract="PropertyPersistenceHandler.Mapping" cardinality="0..n" /> </implementation>
For further details refer to ExtensibleObjectAttributeValuePersistenceHandler.