Channels ▼
RSS

Web Development

Type-Safe File-Based Configuration


The one class that you may want to use here that will give you grief is Date because the Date(String) constructor is deprecated. Fortunately, we don't have to go to the extreme that log4j forced us to go to last month (creating a wrapper around Date that implemented all of the methods of Date). The CalendarDate class (Listing Two) solves the problem with a String constructor that gets a date in the officially correct way (via the DateFormat) and then copies the underlying value into the current object. The CalendarDate also defines an explicit value that you can pass to the constructor to represent "now." (The Date class uses the no-arg constructor for this purpose, but that won't work here.)

Listing Two

package com.holub.util;

import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * A {@link Date} with a non-deprecated {@link String} constructor. Use
 * {@code new DateTime(DateTime.NOW)} to create a `DateTime` that represents the
 * time that the object is created. This class is intended be used by configuration
 * enum that's wrapping a {@link Configurations} object, so the
 * constructor throws a {@link ConfigurationError} if it fails.
 * 
 * @author Allen Holub
 *
 * <div style='font-size:8pt; margin-top:.25in;'>
 * ©2012 <!--copyright 2012--> Allen I Holub. All rights reserved.
 * This code is licensed under a variant on the BSD license. View
 * the complete text at <a href="http://holub.com/license.html">
 * http://www.holub.com/license.html</a>.
 * </div>
 */

public class CalendarDate extends java.util.Date
{	
	private static final long serialVersionUID = 1L;	// Required because Date is serializable
	private static final ExtendedLogger log    = ExtendedLogger.getLogger(CalendarDate.class);
	
	/** Pass to the constructor to create a date representing the moment
	 *  the object was created. 
	 */
	public static final String NOW = "now";
	
	/** Create a date using {@link SimpleDateFormat#parse(String)}. The only
	 *  recognized date format is yyyy-MM-dd
	 * 
	 * @param initialValue
	 * @throws ConfigurationError
	 */
	public CalendarDate( String initialValue ) throws ConfigurationError
	{	
		try
		{	
			DateFormat formatter =	new SimpleDateFormat("yyyy-MM-dd"); // DateFormat.getDateInstance();
			formatter.setLenient(true);
			
			Date value = initialValue.equals( NOW )
							? new Date() 
							: formatter.parse( initialValue );
			setTime( value.getTime() );
		}
		catch (ParseException e)
		{	throw new ConfigurationError
			(	log.error("Illegal initializer string for Date (%s):\n\t%s", initialValue, e.getMessage() )
			);
		}
	}
}

More Insights

White Papers

More >>

Reports

More >>

Webcasts

More >>

Here's how I define a couple date keys in the enum:

    key_dateNow ( CalendarDate.class, CalendarDate.NOW, null),      
    key_dateThen( CalendarDate.class, "2001-07-04",     null),      

So far, the only load-time error checking we've performed is that the value in the properties is acceptable to the field's String constructor. Sometimes that's not enough. You'd like to assure that a number is within an acceptable range, for example, or that something like a zip code or a phone number follows a required format. The third constructor argument (which has had the value null up to this point) is used for this purpose. We use this argument to specify an optional verifier, a class that implements the Configurations.Verifier interface:

    public interface Verifier
    {   public void verify(Object input) throws ConfigurationError;
    }

Verifier implementations check that a value is valid, and throw a ConfigurationError if they're not.

I provide two, out-of-the-box verifiers for handling the most-common scenarios. Here's how the first one is used:

    intBound    ( Integer.class, 2, new RangeVerifier(1, 3) ),  // Defaults to 2. 1<=value<=3;

The RangeVerifier verifies that the value is in the specified range. (In the case of intBound that range is between 1 and 3 inclusive.) The RangeVerifier does its work using java.util.Comparable (not by converting to Number), so this particular verifier works with any class that implements Comparable. All the Number derivatives do that, but and do classes like String and Date. For example, I can verify that a data falls somewhere in the year 2001 as follows:

dateBound ( CalendarDate.class, "2001-07-04",
                   new RangeVerifier( new CalendarDate("2001-01-01"), new CalendarDate("2001-12-31")) ),

The phoneNumber field uses the second out-of-the-box verifier (RegExVerifier):

phoneNumber ( String.class, "(510)555-1212",                // must take the form: (ddd)ddd-dddd
                new RegExVerifier("(\\(\\d\\d\\d\\))?\\s*\\d\\d\\d[-.]\\d\\d\\d\\d") ),

This verifier checks that the presented value matches the regular express specified in the constructor. Here, I'm checking for a standard US phone-number format, with parenthesis around the area code.

The verifiers are run on both the default version of the object (as specified in the enum definition) and the version that's pulled from the properties file, so it's not possible for a bad value to slip by, unnoticed.

Moving on to more mundane details...

First, I have to get a pointer to the actual properties file. Because of the location-based configuration class that I showed you a couple months back, that's trivial to do. The code

    public static final File INPUT_FILE = Places.CONFIG.file("test.properties");

(on line 53) creates a File object for a file called test.properties. That file must be in the directory specified by the either by the CONFIG environment or the -DCONFIG=location command-line switch, or in the default location ~/config as a last resort. The program terminates with a ConfigurationError if none of these places are specified, or if they don't exist. Go back and read Solving the Configuration Problem for Java Apps to see how this magic works; it's a great example of why it's worth taking the trouble to build the sorts of classes that we're looking at this month. Hard stuff becomes easy when you do the work in the right place (inside, not outside, the class that has the data).

All the enum elements delegate the real work to a shared Configurations.Support object, which is a class-level object, shared by all the enum elements. (We'll look at the implementation in a moment.) The act of creating the Support object causes the configuration file to be loaded, and for all enum elements to be initialized (from either the file or a default value, as required) and verified (using the Verifier), so the constructor is the most important method of the Support class. The (static) reference to shared Support object is is declared on line 56, and the load() method creates the object if it doesn't exist, yet:

    public static void load( boolean paramsWithDefaultNotRequiredInPropertiesFile, boolean allowUnusedKeys  )
    {   if( support == null )
            support = new Configurations.Support<TestConfiguration>( values(), INPUT_FILE,
                                            paramsWithDefaultNotRequiredInPropertiesFile, allowUnusedKeys );
    }

The reset() method forces the Support object to be recreated (the file is re-read) on the next access by setting support to null — convenient when you're debugging and want to guarantee a clean state for every test case. I'll discuss the arguments to the Configurations.Support constructor in a moment.

The enum constructor (Listing One, line 26), which creates each individual enum element, just copies its argument into local variables. The three accessor methods that come just after the constructor provide these values to the underlying Support object as needed. I'm not particularly happy that these methods need to exist and I'm really unhappy that they're public (because the methods exist only for the Support object to use, so I'd like to restrict access), but I see no alternative.

I can't pass the element-specific information to the Support-object constructor because those values are unique to each enum element, but the single Support object is shared by all the elements. The object happens to be created by whichever enum element is used first, but that enum element doesn't know what's in the other elements. Pushing the information into the Support object doesn't really work either. (A push is usually preferable better than a pull because the dependencies are more tractable.) The Support object keeps the list of all the enum elements, and it's easy for the Support object to use that list to pull information from each element. A push would require that the Support object keep local copies of information that's also stored in every enum element, and I'm reluctant to duplicate information unnecessarily. Duplicates are just bugs waiting to happen if they get out of phase.

So, the only viable alternative seems to be accessors, and these accessors have to be declared in an interface (Configurations) that's implemented by the TestConfiguration enum. (Remember, enums are just classes, so they can indeed implement interfaces.) I really hate to do that, however, because these methods shouldn't be generally accessible to every object in the program. If Java had something like C++'s friend mechanism, I'd certainly use it here. That is, I'd love to be able to tell the compiler that the three accessor methods can be called only from a Configurations.Support object, and no one else. I suppose I could enforce that rule at runtime by getting the name of the calling class off the runtime stack, but that's a lot of work, and would create a runtime — rather than compile-time — error, so I'm stuck with a straight interface.

The remainder of the methods are just one-liner wrappers that delegate to similarly named Configuration.Support methods. I'll look at those in depth in a moment.


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