Tuesday, April 1, 2014

Composite Configuration

Composite Configuration

Modeling Common Aspects

Looking at Configuration my working analysis was to model it mainly as a Map<String,String> with additional meta data added. As we have seen this concept comes with several advantages:
  • The basic API ( java.util.Map) is already defined by the JDK.
  • Since keys as well as values are simple Strings, we inherit all the advantages of the final and immutable  String class, like type and thread safety.
  • since we constraint our API to this simple types, we ensure no or minimal overlaps with CDI in the EE context.
  • our model is fully compatible with Java SE, providing therefore maximal compatibility also with the SE platform.
Applied to the configuration format we would define two distinct artifacts:
  • a PropertyMap, which models the minimal requirements for a configuration map.
  • a Configuration, which extends PropertyMapand provides additional functionalities, such as extension points, type support etc.
public interface PropertyMap extends Map<String,String>{

    Set<String> getSources();
    Map<String,String> getMetaInfo(String key);
    Map<String,String> getMetaInfo();

    void reload();
    boolean isMutable();
}

public interface Configuration extends PropertyMap{

    String getConfigId();

    Boolean getBoolean(String key);
    Boolean getBooleanOrDefault(String key,
                               Boolean defaultValue);
    Byte getByte(String key);
    ...
    <T> T getAdapted(String key, PropertyAdapter<T> adapter);
    <T> T getAdaptedOrDefault(String key,
                    PropertyAdapter<T> adapter, T defaultValue);

    <T> T get(String key, Class<T> type);
    <T> T getOrDefault(String key, Class<T> type, 
                                               T defaultValue);
    Set<String> getAreas();
    Set<String> getTransitiveAreas();
    Set<String> getAreas(Predicate<String> predicate);
    Set<String> getTransitiveAreas(Predicate<String> predicate);
    boolean containsArea(String key);

    Configuration with(ConfigurationAdjuster adjuster);
    <T> T query(ConfigurationQuery<T> query);
}

A configuration instance then can be built using a PropertyMap, e.g.

PropertyMap myPropertyMap = ...;
Configuration config = new BuildableConfiguration
                                       .Builder("myTestConfig")
                   .withUnits(myPropertyMap);

So we can provide partial configurations by just implementing the PropertyMap interface. For convenience an AbstractPropertyMap class can be defined that additionally supports implementing this interface. 

public class MyPropertyMap extends AbstractPropertyMap{
    protected Map<String,String> initContentDelegate(){
      // in reality, provide something useful here...
      return Collections.emptyMap();
   }
}

Using Composites to Build Complex Configurations

Given the simple basic PropertyMap interface we can start thinking on how building more complex configurations by combining existing combinations. Basically the ingredients required are:
  • two (or more) existing configurations
  • a combination algorithm or policy
Now thinking on mathematical sets, we may provide similar functionality when combining configurations:
  • union
  • intersection
  • subtraction
Additionally we have to think ow we should resolve conflicts (different values with the same key), most important policies are:
  • ignore duplicates (keeping the original values from former entries)
  • override existing previous values by later values
  • throw an exception, when conflicting entries are encountered
This can be modeled by a corresponding policy enum:

public enum AggregationPolicy{
    IGNORE,
    OVERRIDE,
    EXCEPTION
}

Finally we can provide a factory class that provides as 
  • commonly used property maps by reading from resolvable paths, using common configuration formats, e.g. .property-files (the resolution capabilities hereby can be extended by implementing and registering a corresponding SPI)
  • most commonly used compositions of partial configurations (maps)
This can be modeled with a simple singleton as follows:

public final class PropertyMaps{

    private PropertyMaps(){ }

    // factory methods
    public static PropertyMap fromArgs(
              Map<String,String> metaInfo, String... args);
    public static PropertyMap fromPaths(
              Map<String,String> metaInfo, String... paths);
    public static PropertyMap from(
              Map<String,String> metaInfo, 
              Map<String,String> map);
    public static PropertyMap fromArgs(String... args);
    public static PropertyMap fromPaths(String... paths);
    public static PropertyMap from(Map<String,String> map);
    public static PropertyMap fromEnvironmentProperties();
    public static PropertyMap fromSystemProperties();

    // combinations
    public static PropertyMap unionSet(
              PropertyMap... propertyMaps);
    public static PropertyMap unionSet(
              AggregationPolicy policy,
              PropertyMap... propertyMaps);
    public static PropertyMap intersectedSet(
              PropertyMap... propertyMaps);
    public static PropertyMap subtractedSet(
              PropertyMap target, PropertyMap... subtrahendSets);
    public static PropertyMap filterSets(
              Predicate<String> filter, PropertyMap propertyMap);
}

With the given mechanism we are able to define complex configurations, realizing some complex override and configuration rules quite easily:

String[] cliArgs = ...;
Map<String,String> defaultMap = ...;

Configuration config = new BuildableConfiguration.Builder(
                            "myTestConfig").withUnits(
       PropertyMaps.from(defaultMap),
       PropertyMaps.fromPaths("classpath:test.properties"),
       PropertyMaps.fromPaths("classpath:cfg/test.xml"),
       PropertyMaps.fromSystemProperties(),
       PropertyMaps.fromPaths(
                  "url:http://1.2.3.4/remoteCfg.xml"),
       PropertyMaps.fromArgs(cliArgs),
      )
      .build();

Basically the above creates a full fledged Configuration instance that:
  • is built from properties contained in the given default map.
  • that may be overridden by entries in test.properties, read from the classpath
  • that may be overridden by entries in cfg/test.xml, using the JDKs xml property format (also read from the classpath)
  • that may be overridden by entries from the resource loaded from http://1.2.3.4/remoteCfg.xml
  • that may be overridden by entries  from the CLI arguments
Of course, this example uses always the same keys for all different partial configuration sources, which might not be a realistic setup. But adding a mapping of provided keys to some other keys is basically a trivial task.

Summary

Summarizing separating configuration into a simple basic interface (PropertyMap) and a more complex extended variant (Configuration), allows us to easily build composite configurations by combining more simpler partial property maps. Most commonly configuration locations, formats and combination strategies can also provided easily by according factory classes. Also in most cases, implementing the more simpler PropertyMapinterface should completely sufficient.
Putting all this to reality, we have defined a quite powerful mechanism, that allows us to implement also complex use cases with only a few abstractions.

No comments:

Post a Comment