Thursday, March 20, 2014

Adding Additional Type Support to Configuration

Adding Additional Type Support to Configuration

In my previous blog I discussed roughly what configuration is. I also stopped on a working assumption that configuration mainly is a Map<String,String> type with additional metadata. In this blog I would like to elaborate, how non literal types can be supported. Summarizing I would define the following requirements:
  • it should be possible to access configuration as non literal type
  • all types contained in java.lang should be supported.
  • nevertheless arbitrary other types should also be enabled
  • it should be possible to register "converters"
  • it should also be possible to pass a matching "converter" programmatically
First of all we have to think about, what kind of functionality we want to add here to the basic Configuration interface (this is also the reason why converter is written in italic face above).
Basically adding type support requires a configuration entry's value, that is a String to be compatible with some arbitrary type:
 "...allows the interface of an existing class to be used from another interface."
This is exactly the GoF's adapter pattern. So let as define an adapter:

@FunctionalInterface
public interface PropertyAdapter<T>{
  <T> T adapt(String value);
}

Now we can extend
Configuration to allow access on configuration using such an adapter:

public interface Configuration{
   ...
   <T> T getAdapted(String key, PropertyAdapter<T> adapter);

}

Obviously this is a very minimalistic approach. Now thinking on all basic types defined in java.lang, it is a good idea to add corresponding convenience methods:


public interface Configuration{
   ...
   Character getCharacter(String key);
   Byte getByte(String key);
   Short getShort(String key);
   Integer getInteger(String key);
   Long getLong(String key);
   Float getFloat(String key);
   Double getDouble(String key);     

}

By default, I would suggest throwing a RuntimeException, if a value is missing, is a good idea, so these methods never will return null values. Additinoally it might b e a good idea to let also default values to be returned, so we add also the following methods:


   Character getCharacterOrDefault(String key,
                                  
Character defaultValue);
   Byte getByteOrDefault(String key, Byte defaultValue);
   Short getShortOrDefault(String key, Short defaultValue);
   Integer getIntegerOrDefault(String key, Integer defaultValue);
   Long getLongOrDefault(String key, Long defaultValue);
   Float getFloatOrDefault(String key, Float defaultValue);
   Double getDoubleOrDefault(String key, Double defaultValue);
   <T> T getAdaptedOrDefault(String key, Adapter<T> adapter,
                             T defaultValue
);
 

With the above signatures passing null as a default value is completely valid. So one might write:

Byte myNumber = config.getByte("minNumber", null);
if(myNumber==null){
   // do whatever needed
}

Though such code above is quite common, it might be worthwil to think on additional utility functionality, e.g. using JDK 8 features:

Configurator configurator = Configurator.of(config);
configurator.forByte("minNumber", MyNumbers::configure};

Though it would be interesting to investigate this area more, I would like to go one step back and ask, if a single String configuration value is always enough to create/implement every type of adapted interface. We have seen that a configuration entry is basically
  • a configuration key
  • a configuration value
  • configuration entry metadata
So to cover that we would extend our interface slightly as follows:

@FunctionalInterface
public interface PropertyAdapter<T>{
  <T> T adapt(String key, String value,
              Map<String,String> metadata);
}
 
But this is still not optimal, we would be much more flexible by passing the Configuration instance, on which the adapter is used, to the adapter implementation:

@FunctionalInterface
public interface PropertyAdapter<T>{
  <T> T adapt(String key, Configuration config);
}

More generally the above is simply a specialization of a query against a configuration, but just for one specific key:

@FunctionalInterface
public interface ConfigurationQuery<T> {
   T queryFrom(Configuration config);
}

Now these concepts can be useful, when thinking on collection support. Image the following configuration:

foo.a=aValue
foo.b=bValue
foo.c=cValue

Given this configuration foo can be adapted in different ways:
  • It can be adapted to a Map with a=aValue, b=bValue, c=cValue
  • But it can also be mapped to a List (interpreting a,b,c as ordering predicate) with aValue, bValue, cValue.
  • Similarly it could also be mapped to a tree:
              (root)
             /  |   \
           a    b    c
           |    |    |   
      aValue  bValue cValue

With the extended adapter definition all that is possible.

With that our Configuration is already very flexible, for example think on the following entry: 

javax.persistence.unit.MyUnit=https://myconfigserver.net/myApp/persistence/MyUnit.xml?myinstance=${instance.host} 

This entry could reference a persistence.xml, that is provided remotely. With ${instance.host} EL could be used to enable also dynamic aspects included into the configuration.
Now an adapter might return a String, containing the descriptor file, but this would load the configuration completely into memory. As an alternate we might allow to return an InputStream:

InputStream myUnitConfigStream = config.getAdapted(
                           "javax.persistence.unit.MyUnit",

                           URLResolver.of());


URLResolver would implements hereby an adapter that creates an URL and tries to load it, returning the InputStream. It is accessed using a static factory method:

public final class URLResolver implements PropertyAdapter<InputStream>{

  private static final URLResolver INSTANCE = new URLResolver();








  private URLResolver(){}

  
  public InputStream adapt(String key, Configuration config){
    try{
      URL url = new URL(config.getValueOrDefault(key, null));
      if(url!=null){
        return url.openStream();
      }

    }
    catch (Exception e){
      // log error

    }
    return null;
  }

  public static URLResolver of(){
    return INSTANCE;
  }

}
  
So we have seen that with rather small additional to the Configuration interface, we already have gained much flexibility, what we can do with it. Thinking on the new features of Java 8 configuration will be for sure get much more fun than it was in the past.
I would be interested, on what you think would be useful scenarios, using the mechanism presented in this blog. So you are invited to leave your comments and ideas!



No comments:

Post a Comment