Concept - Property Groups

Introduction

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();
}

Glossary

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.

Property Group Definition

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).

Annotation-based Property Group Definition

Declare a Property Group

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.

Inheritance

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.

Default Values

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.

Example:
@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.

Secret Properties

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.

Localized Properties

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.

Example:
@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.

Parameterized Properties

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.

Multi Value Properties

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.

Example:
@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.

Property Group Values

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.

Example:
@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.

Selectors and Conditional Properties

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.

Example with String typed selector:
@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).

Example with Enum typed selector:
@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: 

  • On access for property usage the framework returns null (respectively the according default zero, false for primitive types)
  • On access for the property management the framework returns the actually stored values.

Extensible Handler Selector 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.

Validation Constraints

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:

  • Automatic validation of property group values
  • Constraint annotations
  • Localized messages
  • Constraint inheritance

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.

Automatic Validation of Property Group Values

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.

Constraint Annotations

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.

@NotNull

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

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 declaration
@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 {};
}
Validator implementation
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;
    }
} 
Usage
@PropertyGroup
@NotEqualValueProperties(properties={"p1", "p2"}, message="The properties 'p1' and 'p2' must not contain the same value.") 
public interface ValidationExample
{
	String getP1();
	String getP2();
}   

Message Localization

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();
}

Custom Annotations

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:

  • The annotation from the child interface hides the annotation from the parents.
  • It is forbidden that parents provide not equal instances of the same annotation type if the child does not provide an instance of this type.

Working with Property Groups

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 API of accessing property values for usage in the business code
  • The API for managing property group values
  • The way of telling the system where and how property group values are stored and retrieved

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.

Access API for Property Usage

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.

Access API for Property Management

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.

Configuration of the Property Engine

The property engine provided by core has tree customization/extension points:

  • The locale fallback provider,
  • The persistence handlers and
  • The fallback strategies.

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

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:

  • If a request is active take the request locale.
  • If no request is active take the locale from the current application context.
  • If no value is found fall back to the lead locale of the system.

This rule is already registered with the core cartridge:

<fulfill requirement="localeFallbackProvider" of="PropertyEngine">
	<instance name="PropertyEngine-LocaleFallback" with="DefaultLocaleFallback" scope="app"/>
</fulfill>

The Fallback Strategy Provider

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>

The Persistence Handler Provider

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:

  • All natively supported type of ExtensibleObject, except references to other persistent objects (methods getObject() and putObject()
  • All primitive Java data types ( char/ Character is stored as a string, all others are stored according with the best matching type)
  • All instances of Enum<?>
  • All instances of Class<?>
  • Sub property groups

Supported parameter data types for parameterized properties:

  • String
  • java.util.Locale
  • All instances of Enum<?>
  • boolean/ Boolean

Limitations:

  • The text type of ExtensibleObject is not used ( getText()/ putText(). String property are always stored with putString().
  • The object graph must not contain cycles.
  • If a localized sub property group also contains localized properties then the sub property can only be given in the same locale as the parent.
    The same applies for parameterized properties with parameter type 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.

Mapping Example:
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:

  • All attribute value names are prefixed (default with the property group ID + ":").
  • For lists of sub property groups keys with the integer values of the position are created. Value lists are stored direct with the attribute value so no extra keys are needed.
  • 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:

  • Defining a custom prefix per property group, this can also be an empty string, or
  • Defining individual names for single attribute value names.

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>
Example:
<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.

Disclaimer
The information provided in the Knowledge Base may not be applicable to all systems and situations. Intershop Communications will not be liable to any party for any direct or indirect damages resulting from the use of the Customer Support section of the Intershop Corporate Web site, including, without limitation, any lost profits, business interruption, loss of programs or other data on your information handling system.
The Intershop Knowledge Portal uses only technically necessary cookies. We do not track visitors or have visitors tracked by 3rd parties. Please find further information on privacy in the Intershop Privacy Policy and Legal Notice.
Home
Knowledge Base
Product Releases
Log on to continue
This Knowledge Base document is reserved for registered customers.
Log on with your Intershop Entra ID to continue.
Write an email to supportadmin@intershop.de if you experience login issues,
or if you want to register as customer.