2017-11-25

Tamaya Metamodel (Configuration of Tamaya) (Extension Module)

Tamaya metamodel is an extension module. Refer to the extensions documentation for further details.

What functionality this module provides ?

The Tamaya metamodel module provides support for configuring the Tamaya system itself. It allows, like a logging configuration, to configure how your configuration framework should work, where to find configuration and how it is combined using overrides, filters etc.

By default it uses an XML based configuration format as illustrated below:

Extract from tamaya-config.xml
<configuration>
    <!-- Context is evaluated first. -->
    <context>
        <context-entry name="stage">${properties:system:STAGE?default=DEV}</context-entry>
        <context-entry name="configdir">${properties:system:configdir?default=.}</context-entry>
        <context-entry name="app">${properties:system.APP?default=NONE}</context-entry>
        <context-entry name="context">${java:org.apache.tamaya.context.Context#id()}</context-entry>
        <context-entry name="company">Trivadis</context-entry>
        <context-entry name="default-formats">yaml,json</context-entry>
        <context-entry name="default-refresh-period">5 SECOND</context-entry>
    </context>

    <!-- combinationPolicy type="" / -->

    <!-- Configuration definition. -->

    <property-sources>
       <source enabled="${stage=TEST || stage=PTA || stage=PROD}"
           type="env-properties">
           <filter type="PropertyMapping">
               <param name="mapTarget">ENV.</param>
           </filter>
           <filter type="AccessMask">
               <param name="roles">admin,power-user</param>
               <param name="policy">mask</param>
               <param name="mask">*****</param>
               <param name="matchExpression">SEC_</param>
           </filter>
       </source>
       <source type="sys-properties" >
           <filter type="ImmutablePropertySource" />
       </source>
       <source type="file" refreshable="true">
           <name>config.json</name>
           <param name="location">config.json</param>
       </source>
       ...
    </property-sources>
</configuration>

The module basically provides an XML representation to the ConfigurationContextBuilder API. It creates a corresponding ConfigurationContext and registers the corresponding Configuration as the system’s default configuration (accessible from ConfigurationProvider.getConfiguration().

Compatibility

The module is based on Java 7, so it will not run on Java 7 and beyond.

Installation

To use metamodel features you only must add the corresponding dependency to your module:

<dependency>
  <groupId>org.apache.tamaya.ext</groupId>
  <artifactId>tamaya-model</artifactId>
  <version>{tamaya_version}</version>
</dependency>

Creating a Configuration using Meta-Configuration

The basic feature of this module is the capability of creating a Configuration completely based on a meta-configuration file. For this the MetaConfiguration main singleton provides different methods:

[source, java)

public final class MetaConfiguration {
    public static void configure();
    public static void configure(URL metaConfig);
    public static ConfigurationContextBuilder createContextBuilder(URL metaConfig);
    public static Configuration createConfiguration(URL metaConfig);
  • If you have supplied your meta-configuration at META-INF/tamaya-config.xml you simply call MetaConfiguration.configure();. This will read the meta-configuration and configure Tamaya’s default configuration. Alternatively you can choose your own metaconfiguration location by passing an alternate URL ro read from.

  • With MetaConfiguration.createContextBuilder() you can stop a step earlier: a new instance of ConfigurationContextBuilder is created and configured with all the entries found in your meta-configuration. Also here you can optionally pass your custom location for the meta-configuration resouce.

  • Finally MetaConfiguration.createConfiguration(URL) allows you to create an arbitrary Configuration instance using a meta-configuration file. The Configuration instance is completely independent and not registered as default configuration, so it’s lifecycle and usage is completely under your control.

MetaContext

When thinking what are the various input parameters for determining a correct configuration, there might be different things relevant in different scenarios, especially for developers in different companies. A good example of such an input parameter is the current STAGE. All these kinf od inputs can be summarized in some sort of meta-configuration, commonly known as a context. So the metamodel extension ships with a MetaContext class that allows to define a common meta-context, that can be accessed by components as needed to determine the correct settings to be applied:

[source, java)

public final class MetaContext {

    ...

    public static MetaContext getInstance(String contextName);

    /**
     * Access the default context. Contexts are managed as weak references in this class. If no
     * such context exists, a new instance is created.
     * @return the context instance, never null.
     */
    public static MetaContext getDefaultInstance();

    /**
     * Access a context by name. Contexts are managed as weak references in this class. If no
     * such valid context exists, a new instance is created, using the given {@code validSupplier}.
     * @param contextName the context name, not null.
     * @return the context instance, never null.
     */
    public static MetaContext getInstance(String contextName, Supplier<Boolean> validSupplier);

    /**
     * Access the thread-based context. If no such context
     * exists a new one will be created.
     * @param reinit if true, clear's the thread's context.
     * @return the corresponding context, never null.
     */
    public static MetaContext getThreadInstance(boolean reinit);

    /**
     * Access the current context, which actually is the current context, combined with the thread based
     * context (overriding).
     * @return the corresponding context, never null.
     */
    public MetaContext getCurrentInstance();

     /**
     * Access the current context, which actually is the current context, combined with the thread based
     * context (overriding).
     * @param reinit if true, clear's the thread's context.
     * @return the corresponding context, never null.
     */
    public MetaContext getCurrentInstance(boolean reinit);


    /**
     * Method to evaluate if a context is valid. This basically depends on the
     * {@code validSupplier}, if any is set. If no supplier is present the context is valid.
     *
     * @return true, if this context is valid.
     */
    public boolean isValid();

    /**
     * Combine this context with the other contexts given, hereby only contexts are included
     * which are {@code valid}, see {@link #isValid()}.
     * @param contexts the context to merge with this context.
     * @return the newly created Context.
     */
    public MetaContext combineWith(MetaContext... contexts);

    /**
     * Access the given context property.
     * @param key the key, not null
     * @return the value, or null.
     */
    public String getProperty(String key);

    /**
     * Access the given context property.
     * @param key the key, not the default value.
     * @param defaultValue the default value to be returned, if no value is defined, or the
     *                     stored value's TTL has been reached.
     * @return the value, default value or null.
     */
    public String getProperty(String key, String defaultValue);

    /**
     * Sets the given context property.
     * @param key the key, not null.
     * @param value the value, not null.
     * @return the porevious value, or null.
     */
    public String setProperty(String key, String value);

    /**
     * Sets the given context property.
     * @param key the key, not null.
     * @param value the value, not null.
     * @param ttl the time to live. Zero or less than zero means, no timeout.
     * @param unit the target time unit.
     * @return the porevious value, or null.
     */
    public String setProperty(String key, String value, int ttl, TimeUnit unit);

    /**
     * Sets the given property unless there is already a value defined.
     * @param key the key, not null.
     * @param value the value, not null.
     */
    public void setPropertyIfAbsent(String key, String value);

    /**
     * Sets the given property unless there is already a value defined.
     * @param key the key, not null.
     * @param value the value, not null.
     * @param ttl the time to live. Zero or less than zero means, no timeout.
     * @param unit the target time unit.
     */
    public void setPropertyIfAbsent(String key, String value, long ttl, TimeUnit unit);

    /**
     * Adds all properties given, overriding any existing properties.
     * @param properties the properties, not null.
     */
    public void setProperties(Map<String,String> properties);

    /**
     * Adds all properties given, overriding any existing properties.
     * @param properties the properties, not null.
     * @param ttl the time to live. Zero or less than zero means, no timeout.
     * @param unit the target time unit.
     */
    public void setProperties(Map<String,String> properties, long ttl, TimeUnit unit);

    /**
     * Checks if all the given properties are present.
     * @param keys the keys to check, not null.
     * @return true, if all the given keys are existing.
     */
    public boolean checkProperties(String... keys);

    /**
     * Access all the current context properties.
     * @return the properties, never null.
     */
    public Map<String,String> getProperties();
}

As you see, a MetaContext has the following aspects:

  • there are multiple context’s possible, identified by their name.

  • Accessing an instance that does not yet exist, will create a new one.

  • there is one shared default instance.

  • they store ordinary String,String key, value pairs.

  • they can be combined into a overriging hierarchy

  • accessing the default MetaContext returns the global instance combined with a threaded override instance. Passing reinit will clear the thread instance’s data.

Configuring MetaContexts

MetaContext instances can be configured in the meta-configuration in the first meta-context section as illustrated below:

<!-- Configuring the default context -->
<context>
    <context-entry name="stage">${properties:system:STAGE?default=DEV}</context-entry>
    <context-entry name="configdir">${properties:system:configdir?default=.}</context-entry>
    <context-entry name="app">${properties:system.APP?default=NONE}</context-entry>
    <context-entry name="context">${java:org.apache.tamaya.context.Context#id()}</context-entry>
    <context-entry name="company">Trivadis</context-entry>
    <context-entry name="default-formats">yaml,json</context-entry>
    <context-entry name="default-refresh-period">5 SECOND</context-entry>
</context>
<!-- Configuring a context named 'APP' -->
<context name="APP">
    <context-entry name="application">someAppName</context-entry>
</context>

As shown above multiple contexts can be configured. Keys and values are of type String.

Using Expressions

As shown before, it is possible to add simple expressions, enclosed in ${}. Hereby the contents must be formatted as evaluator:expression, which then internally must be interpreted by the org.apache.tamaya.metamodel.internal.SimpleResolver, which effectively reads and applied context entries.

Currently the following placeholders for context entries are provided:

  • properties - mapping to system properties (properties:sys:KEY) or environment properties (properties:env:KEY) or other MetaContext entries initialized already (properties:ctx[:CTXNAME]:KEY)

  • java - mapping to a static method or field, returning a String value.

General Extensions

Working with meta-models requires additional aspects to be generalized to separate concerns and reuse some of the common functionality. These concepts are shown in the following subsections.

Enabled

Things can be dynamically enabled or disabled, e.g. based on context. This can be modelled by the Enabled interface:

public interface Enabled {

    /**
     * Returns the enabled property.
     * @return the enabled value.
     */
    boolean isEnabled();

    /**
     * Enables/disables this property source.
     * @param enabled the enabled value.
     */
    void setEnabled(boolean enabled);
}

Enabled can be used as a mixin-logic, e.g. for decorating property sources, property source providers, filters and converters. The decorator can also, if not set explicitly, evaluate the enabled property based on the current runtime context.

Refreshable

Similar to Enabled things can also be refreshable.

public interface Refreshable {

    /**
     * Refreshes the given instance.
     */
    void refresh();
}

This can be used to define a common API for refreshing artifctas. Similar to Enabled this can be applied as a decorator/mix-in interface to property sources and property source providers. This property also is supported in the XML metaconfiguration, e.g.

<property-sources>
    <source type="file" refreshable="true">
       <name>config.json</name>
       <param name="location">config.json</param>
    </source>
</property-sources>

The MetaConfiguration XML Structure

In general the tamaya-config.xml file does never apply an XML schema or similar. Nevertheless there is a common DSL structure, which can be extended as well (see next chapter).

<configuration>
    <!-- PART ONE: Contexts initialization. -->
    <context>
        <context-entry name="stage">${properties:system:STAGE?default=DEV}</context-entry>
        <context-entry name="configdir">${properties:system:configdir?default=.}</context-entry>
        ...
    </context>
    <context name="APP">
        <context-entry name="application">someAppName</context-entry>
    </context>

    <!-- PART TWO: Global settings of ConfigurationContext. -->
    <!-- combinationPolicy type="" / -->

    <!-- PART THREE: Configuration definition. -->

    <property-sources>
       <source enabled="${stage=TEST || stage=PTA || stage=PROD}"
           type="env-properties">
           <filter type="PropertyMapping">
               <param name="mapTarget">ENV.</param>
           </filter>
           <filter type="AccessMask">
               <param name="roles">admin,power-user</param>
               <param name="policy">mask</param>
               <param name="mask">*****</param>
               <param name="matchExpression">SEC_</param>
           </filter>
       </source>
       <source type="sys-properties" >
           <filter type="ImmutablePropertySource" />
       </source>
       <source type="file" refreshable="true">
           <name>config.json</name>
           <param name="location">config.json</param>
       </source>
        <source type="file" refreshable="true">
            <name>config.xml</name>
            <param name="location">config.xml</param>
            <param name="formats">xml-properties</param>
        </source>
       <source-provider type="resource">
           <name>classpath:application-config.yml</name>
           <param name="location">/META-INF/application-config.yml</param>
       </source-provider>
       <source type="ch.mypack.MyClassSource" />
       <!--<include enabled="${stage==TEST}">TEST-config.xml</include>-->
       <source-provider type="resource" enabled="${configdir != null}">
           <name>config-dir</name>
           <param name="location">/${configdir}/**/*.json</param>
       </source-provider>
       <source type="url" refreshable="true">
           <name>remote</name>
           <param name="location">https://www.confdrive.com/cfg/customerId=1234</param>
           <param name="formats">json</param>
           <filter type="CachedPropertySource">
               <param name="ttl">30 SECOND</param>
           </filter>
       </source>
    </property-sources>
    <property-filters>
        <filter type="UsageTrackerFilter"/>
        <filter type="AccessControl">
            <param name="roles">admin,power-user</param>
            <param name="policy">hide</param>
            <param name="expression">*.secret</param>
        </filter>
        <filter type="Cache">
            <param name="ttl">30000</param>
            <param name="expression">cached.*</param>
        </filter>
    </property-filters>
    <property-converters>
    <!--<converter type="AllInOneConverter"/>-->
        <default-converters/>
    </property-converters>
</configuration>

The different parts in fact are not hardcoded, but implemented as independent components, where each of them gets access to the XML DOM tree to read the configuration aspects of interest. Instances related must implement the ++ interface and register it to the ServiceContext. Reading order is mapped using @Priority annotations. For further details refer to the SPI section in this document.

Model SPI

Extending the XML DSL

The XML DSL can be extended in various ways:

  • Basically adding a new feature maps to adding a new section to the meta-config XML. This can be easily done, by implementing MetaConfigurationReader and do whatever is appropriate for your use case.

  • For adding new expression capabilities for `MetaContext`entries SimpleResolver must be implemented.

  • For allowing customized parameterization of artifacts, e.g. property sources, property source providers, converters and filters etc. you may implement ItemFactory instances.

MetaConfigurationReader

XML metaconfiguration is effectively processed by instances of type org.apache.tamaya.metamodel.spi.MetaConfigurationReader:

public interface MetaConfigurationReader {

     /**
      * Reads meta-configuration from the given document and configures the current
      * context builder. The priority of readers is determined by the priorization policy
      * implemented by the {@link org.apache.tamaya.spi.ServiceContext},
      * @param document the meta-configuration document
      * @param contextBuilder the context builder to use.
      */
     void read(Document document, ConfigurationContextBuilder contextBuilder);

 }

Hereby we also see that an instance of ConfigurationContextBuilder is passed. Remember, we mentioned earlier that meta-configuration basically is a XML API to the building a configuration using a ConfigurationContextBuilder. So all you can do with the meta-config XML can also be done programmatically using the Java API.

This module provides instances of this class for reading of meta-context, property-sources, property source providers, converters, filters and more. Look into the org.apache.tamaya.metamodel.internal package for further details.

New instances implementing this interface must be registered into the current ServiceContext, by default the ServiceLoader is used.

ItemFactory

Instances of ItemFactory allow to configure artifacts using XML data:

public interface ItemFactory<T> {

    /**
     * Get the factory name.
     * @return the factory name, not null.
     */
    String getName();

    /**
     * Create a new instance.
     * @param parameters the parameters for configuring the instance.
     * @return the new instance, not null.
     */
    T create(Map<String,String> parameters);

    /**
     * Get the target type created by this factory. This can be used to
     * assign the factory to an acording item base type, e.g. a PropertySource,
     * PropertySourceProvider, PropertyFilter etc.
     * @return the target type, not null.
     */
    Class<? extends T> getType();

}

The factory’s name hereby is used as a short cut, e.g. have a look at the following XML snippet defining a PropertySource to be added:

<source type="file" refreshable="true">
   <name>config.json</name>
   <param name="location">config.json</param>
</source>

In the above snippet file equals to the factory name, which provides the user a simple to use short name, instead of adding the fully qualified classname (which is always possible).

The location paramter with its value is passed as Map to the create method.

ItemFactoryManager

This singleton class manages the ItemFactory instances found, hereby allowing accessing and registering instances. This singleton is actually used by the component parsers (type MetaConfigurationReader).

public final class ItemFactoryManager {

    ...

    public static ItemFactoryManager getInstance();

    public <T> List<ItemFactory<T>> getFactories(Class<T> type);
    public <T> ItemFactory<T> getFactory(Class<T> type, String id);

    public <T> void registerItemFactory(ItemFactory<T> factory);

}

Extended Implementations

The package org.apache.tamaya.metamodel.ext contains a few useful implementations that also can be used in your meta-configuration and show how mixin-functionality can be added without touching property source implementations.

As of now the package contains

  • EnabledPropertySource: a decorator for a PropertySource adding the capability to enable/disable the property source.

  • EnabledPropertySourceProvider a decorator for a PropertySourceProvider adding the capability to enable/disable the property source provider.

  • RefreshablePropertySource: a decorator for a PropertySource adding the capability to refresh the property source.

  • EnabledPropertySourceProvider a decorator for a PropertySourceProvider adding the capability to refresh the property source provider.

Not yet implemented but planned are implementations to add the following functionality:

  • caching of entries for a given time.

  • immutability of entries, so a configuration data (or parts of it) will never change later.