Channels ▼
RSS

Web Development

Type-Safe File-Based Configuration


The Support Layer

Moving on to the support interface and object (Listing Three, available for download here), the first thing to notice is that Configuration is an interface, and Support is a static inner class of that interface. I like to use that class-in-an-interface approach when the class has absolutely no use outside of the context of the interface. That is, inner classes of interfaces support the implementor of the interface in some way. They could be default implementations; they could be classes that make the implementer's life easer (as is the case here); but, you'll never use these classes unless you're also implementing the interface. Nesting the support classes inside the interface namespace reduces clutter at the global level, and allows you to use generic names (such as Support) that would be problematic in a global-level library method.

More Insights

White Papers

More >>

Reports

More >>

Webcasts

More >>

The particular interface contains one dependent interface (Verifier, on line 81) and three classes: Support (line 90) provides all the enum-level support, and the two out-of-the-box verifiers: RegExVerifier (line 422) and RangeVerifier (line 470).

Now let's return to the Configuration.Support constructor (Listing Three, line 95). Four arguments are supported. The first two are just the list of enum values, and a File object that identifies which "properties" file to read.

The paramsWithDefaultsNotRequiredInConfigFile and allowUnusedKeys arguments let you control what to do with missing or extra key=value pairs in the properties file. On one hand, it's nice to have an empty configuration file where everything defaults. On the other, it's annoying to forget to define a variable and have it silently default to the wrong value. Since neither approach is "correct" in every situation, you can control the system's behavior by setting paramsWithDefaultsNotRequiredInConfigFile to true to allow missing variables to default without an error. If the variable is false, every enum element has to have a matching key in the properties file, and the default value is used if there is no value specified in the file. It's always a hard error (a ConfigurationError) if there is neither a default value nor a value specified in the file.

By the same token, you may or may not want to permit extra key=value pairs in the properties file. You might, for example, be using a single properties file to initialize several enums. However, when defaults are on the scene, an "extra" key might actually be a misspelling of a real key, and your replacement value would be ignored. The safest approach is to make an extra key an error, but that would preclude the multiple-enums-for-one-file approach. Setting allowUnusedKeys true tells the system to log, but otherwise ignore, unused keys when it finds them. If the allowUnusedKeys is false, the system throws a ConfigurationError if it finds an unexpected key.

Most of the work happens in this constructor, inside the loop on line 144, which processes every enum element in turn. The basic logic gets the default value (if there is one) and the value from the file (if there is one) and creates an object of the type returned from the enum's type() overload if necessary. If there's no value defined in the file, and the default object is already the correct type, the constructor just uses the default object (which it caches away in the values Map (defined on line 93).

If the default can't be used directly, the constructor must create the value from a String — either the String to the right of the equals sign in the properties file, or the String returned from a toString() call on the default-value object. That work is done by createFromString(...) (Listing Three, line 282).

The actual work is only two lines of code:

    Constructor<?> constructor = type.getConstructor(String.class);
    return constructor.newInstance(initialValue);

The Java reflection APIs create objects by calling the constructor, in this case, the one with a String argument (that's the String.class passed to getConstuctor(...)). The value passed to newInstance(...) becomes the constructor argument.

The other 50 lines of this method handle the impossible number of exceptions that can be thrown by those two method calls. Because I didn't want the user of my class to deal with six different exception types, I catch all the possible exceptions, create an error message that a normal human being can understand, and then throw a ConfigurationError with the original exception as a "cause." Errors are also logged.

The only other interesting methods in the Support class are createConfigFile(Writer) and createConfigFileUsingDefaults(...) (lines 343 and 373), which build a configuration file from scratch, useful when there's no configuration file at all and your program wants to create a default one.

The first method is not static, so you'll need an initialized Support object to invoke it. This variant creates the file using values that it may have read from another file. The second ("using defaults") variant is static, so you can call it in situations where there's no configuration file to load. It creates the file from the default values.

The remainder of the file are the two out-of-the-box Verifier implementations, which are straightforward enough that further explanation isn't necessary.

Conculsion

I've found this approach to configuration to be really useful in practice. You can just use configuration parameters without having to worry if they're valid or not, all of the annoying error-related code and default values are handled for you in the background, and implementing a new enum on top of the Support class is trivial. Hopefully, you'll find this technique useful.

Next month, I'm planning on changing the topic slightly by looking at how I tested the classes I've been discussing for the past few columns. Testing object-oriented systems has it's own peculiarities, and I plan to discuss the testing approaches that I use most often: mock-based tests built on top of Mockito and PowerMock (which are, themselves, built on JUnit).

Related Articles

Solving the Configuration Problem for Java Apps

Custom Configuring Java Apps: Extending log4j



Related Reading






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 
Dr. Dobb's TV