Channels ▼
RSS

Design

Type-Safe File-Based Configuration


The enum Layer

Now let's look at how I implement all of this. I use a two-layer system, where a very lightweight enum defines the configuration keys, and the bulk of the implementation is delegated to a support class. This structure makes it a trivial matter to implement your own custom configuration.

More Insights

White Papers

More >>

Reports

More >>

Webcasts

More >>

Starting with the outermost layer, the TestConfiguration enum is defined in Listing One.

Listing One

package com.holub.testSupport;

import com.holub.util.ConfigurationError;
import com.holub.util.Configurations;
import com.holub.util.CalendarDate;
import com.holub.util.Places;

import java.io.File;
import java.net.URL;
import java.util.Date;

/**
 * A class for testing the Configurations class, represents a small configuration
 * file that tests all possible configuration types.
 *  
 * @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://holub.com/license.html</a>.
 * </div>
 */

public enum TestConfiguration implements Configurations
{
	key_string	( String.class,	 "Doo What Ditty",		null),
	key_int		( Integer.class, 10,					null),	// "boxed" to new Integer(10)
	key_long	( Long.class, 	 Long.MAX_VALUE - 1, 	null),	// "boxed" to new Long(...)
	key_double	( Double.class,  1.23,					null),	// "boxed" to new Double(1.23)
	key_boolean	( Boolean.class, false,					null),	// "boxed" to new Boolean(...)
	key_location( File.class, 	 "/tmp/foo",			null),	// could also use new File("/tmp/foo").
	key_url		( URL.class,	 "http://www.holub.com",null),	// can't catch MalformedURLException from new URL(...),
																	// so use String. Support constructor creates URL object.
	
	key_dateNow ( CalendarDate.class, CalendarDate.NOW,	null),		
	key_dateThen( CalendarDate.class, "2001-07-04", 	null),		
	
	intBound 	( Integer.class, 2, new RangeVerifier(1, 3) ),	// Defaults to 2. 1 <= value <= 3;
	phoneNumber	( String.class, "(510)555-1212",
					new RegExVerifier("(\\(\\d\\d\\d\\))?\\s*\\d\\d\\d[-.]\\d\\d\\d\\d")),
	dateBound   ( CalendarDate.class, "2001-07-04",
							new RangeVerifier( new CalendarDate("2001-01-01"), new CalendarDate("2001-12-31")) ),
					
	intNoDefault( Integer.class, null, null),	// no default, must be defined in the file
	strNoDefault( String.class,  null, null);	// no default, must be defined in the file
	
	//----------------------------------------------------------------------
	/** The name of the underlying properties file (currently test.properties),
	 *  which must be in a Directory that {@link Places#CONFIG} can find.
	 */
	public static final File INPUT_FILE = Places.CONFIG.file("test.properties");
	
	//----------------------------------------------------------------------
	private static Configurations.Support<TestConfiguration> support = null;	 //{=TestConfiguration.supportDeclaration}
	
	/** After this call, the first call to any of the value methods (or to
	 *  {@link #load()}) will reload the properties from the underlying file.
	 */
	public static void reset(){ support = null; }
	
	/** Create the support object only if it doesn't already exist. Can call this
	 *  method at any time, but bear in mind that it can throw an error. To force a load,
	 *  call {@link #reset()}, then call the current method. If you load with this
	 *  method, all enum fields must be present as keys in the properties file, and
	 *  unused keys are not permitted in the file.
	 *  
	 *  @throws ConfigurationError if there are any problems with the configuration file
	 */
	public static void load()
	{	if( support == null )
			support = new Configurations.Support<TestConfiguration>( values(), INPUT_FILE, false, false );
	}
	
	/** This method exists primarily for testing. You would typically create the support object
	 *  in the correct mode automatically without having the end user decide which mode to use.
	 */
	
	public static void load( boolean paramsWithDefaultNotRequiredInPropertiesFile, boolean allowUnusedKeys  )
	{	if( support == null )
			support = new Configurations.Support<TestConfiguration>( values(), INPUT_FILE,
											paramsWithDefaultNotRequiredInPropertiesFile, allowUnusedKeys );
	}

	//----------------------------------------------------------------------
	private final Object	defaultValue;
	private final Verifier	verifier;
	private final Class<?>	type;
	
	private TestConfiguration( Class<?> type, Object defaultValue, Verifier verifier )
	{	
		this.type = type;
		this.defaultValue = defaultValue;
		this.verifier = verifier;
	}

	@Override public Object   defaultValue(){ return defaultValue;	}
	@Override public Class<?> type() 		{ return type;			}
	@Override public Verifier getVerifier()	{ return verifier;      }
	//----------------------------------------------------------------------
	
	/** Same as {@link #stringValue()} */
	@Override public String toString(){ return stringValue(); }
	
	/** Return the value of the current enum as a String. toString just
	 *  calls this method, but the value-object's toString() method is
	 *  used to create the returned string..
	 */
	public Object	value		(){	load(); return support.value(this);			}
	public String	stringValue	(){	load(); return support.stringValue(this);	}
	public Integer	intValue	(){ load(); return support.intValue(this);		}
	public Long		longValue	(){ load(); return support.longValue(this);		}
	public Double	doubleValue	(){ load(); return support.doubleValue(this);	}
	public File		fileValue	(){ load(); return support.fileValue(this);		}
	public URL		URLValue	(){ load(); return support.URLValue(this);		}
	public Boolean  booleanValue(){ load(); return support.booleanValue(this);	}
}

The enum elements are definded the top of the file; the first few elements being simple types:

    key_string  ( String.class,  "Doo What Ditty",      null),
    key_int     ( Integer.class, 10,                    null),
    key_long    ( Long.class,    Long.MAX_VALUE - 1,    null),
    key_double  ( Double.class,  1.23,                  null),
    key_boolean ( Boolean.class, false,                 null),
    key_location( File.class,    "/tmp/foo",            null),

The properties-file names must be identical to the enum-element names, except underscores are mapped to dots. For example, the key_string property looks like this in the properties file:

    key.string= the value

Leading and trailing whitespace on the value side of the equals sign is removed, so you can format the file for readability.

Three constructor arguments are specified for each element. The first is a Class object that identifies the class of object that will hold the value. These objects are created using the reflection APIs, which call the constructor with a single String argument, so the class must have a String constructor. All of Java's numeric classes (and of course String) meet this requirement, as do classes like URL: Literally, any class with a String constructor can be used here, including ones that you write. (I'll show you how in a moment.)

The second argument is the default value, which can be null if you want to force your users to define a value in the properties file. (In general, if you specify a default, then the key can be omitted from the properties file and the default will be used in its place.) Here are a couple fields with no default:

    intNoDefault( Integer.class, null, null),   // no default, must be defined in the file
    strNoDefault( String.class,  null, null);   // no default, must be defined in the file

so something like

    intNoDefult=10
    strNoDefult=the value

must appear in the underlying properties file.

The object used to specify a default value in the enum-element definition is typically an instance of the class specified in the first argument to the constructor. In the case of numeric types, the compiler will "autobox" numeric constants such as int to the equivalent wrapper class (Integer) for you. For example, the compiler translates

    key_int( Integer.class, 10, null),

to

    key_int( Integer.class, new Integer(10), null),

for you.

If the classes don't match exactly, then the default value is created by calling toString() on the second argument, then passing the resulting String to the first-argument-class String constructor. For example, I've delared the key_location field as follows:

    key_location( File.class, "/tmp/foo", null),

with the default value provided as a String, not a File. The system effectively treats this case as if I had said:

    Object defaultValueArgument = "/tmp/foo";
    //...

    key_location( File.class, new File( defaultValueArgument.toString() ), null),

except that, when I use a String default, the new File(...) is effectively executed when the configuration file is loaded, not when the enum is declared. It's occasionally handy to deliberately leverage this behavior. For example, I really have to use

    key_url( URL.class,  "http://www.holub.com", null),

instead of

    key_url( URL.class, new URL("http://www.holub.com"), null),

The problem is that new URL() throws a MalformedUrlException if the URL is illegal, and that's a checked exception, so I must catch it. However, there's no way to catch that exception in an enum definition: Java won't let me surround an enum-field definition with a try/catch block.

By using a String, I'll defer the default-value construction until the load() call, which calls the constructor using the reflection APIs. That's a much more convenient time to catch exceptions because I can call load() explicitly and provide the try/catch if I care to. (Note that load() converts the MalformedURLException to a ConfigurationError, and I probably won't bother to catch it, as finding a malformed URL in the configuration file should probably terminate the program.)

In the case of the File argument, I could just as easily have said:

    key_location( File.class, new File("/tmp/foo"), null ),

because that particular constructor doesn't throw any checked exceptions.


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.
 

Video