Channels ▼
RSS

.NET

Solving the Configuration Problem for Java Apps


The Real Work (LocationsSupport)

All of the real work is done by the workhorse class LocationsSupport, in Listing Four, and most of what it does is in its constructor. Before looking at the constructor, notice that this class is parametrized, which takes some explanation). The definition looks like this:

    public class LocationsSupport<T extends Enum<T> & Locations>
    {   //...
    }

and the declaration (from inside the Places enum) looks like this:

    enum Places implements Location
    {
        LocationsSupport<Places> support;
        //...
        support = new LocationsSupport<Places>( values() ); // Pass in an iterator across all enum elements
        //...
    }

So, the class we're passing in (represented as T inside the LocationsSupport definition), is the Places enum. Places extends Enum<Places> implicitly (all enum classes extend Enum in this way), and Places also implements the Locations interface in Listing Three (Places indeed implements Locations). For some reason, Java-generics represent this situation as <T extends Enum<T> & Locations>, but just read the & as "implements."

The point of this exercise is that our support object will be communicating with a specific enum (Places) whose name it doesn't know when it's compiled. The generics mechanism lets us avoid all the casts that would be required by this situation. Without generic types, every T would require Object, and that Object would have to be cast into something — but into what? Generics solve the problem.

For purposes of the current example, every time you see T in LocationsSupport.java, mentally replace it with Places.

The next item of interest is the dictionary, on line line 37. This is a hash map that will hold all the keys (TMP, HOME, etc.) defined in the containing enum along with File objects representing the associated locations. These aren't the original strings, but are initialized using the original strings after things like tilde substitution occur.

Most of the real work of loading and verifying the values is done in the constructor, which does all the validation I discussed earlier. The code is, I hope, self explanatory. The only notable thing is that each iteration of the main loop adds a line to a buffer that is logged at the bottom of the method (as opposed to logging in each iteration). All I'm doing is cleaning up the log file a little, but that's not a bad thing. Note that I'm logging the actual values used (extracted from the File object), not the input strings because the real values are what I'm actually interested in when I'm debugging my server.

Listing Three: Locations.java, the Locations workhorse class.

package com.holub.util;

import java.io.File;



/**
 * Support for enums that encapsulate locations (e.g. {@link Places}). 
 * Note that the methods of this class let you ask the enum about
 * itself, but do not let you modify the Enum's value (ie. there
 * are no "setter" methods). That's quite deliberate.
 * 
 * @author Allen Holub
 *
 * <div style='font-size:7pt; 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 interface Locations
{
	/** Return a File object representing the
	 *  directory specified in the current enum element's contents.
	 */
	File		directory();
	
	/** Return a File object representing the named file in the
	 *  directory represented by the current enum element's contents.
	 *  This method is effectively a convenience wrapper for:
	 *  <blockquote>
	 *      <code>new {@link File}({@link #directory()}, name );</code>
	 *  </blockquote>
	 *  Note that there's no requirement that the specified file
	 *  exists. Use methods of the {@link File} class to test for
	 *  existence, etc.
	 */
	File		file( String name );
	
	/** Return the default passed into this enum-element's constructor. Note that
	 *  any tilde (~) characters found in the returned string are NOT replaced
	 *  by the user.home system property string. That replacement is effectively
	 *  done by the {@link #directory()} method.
	 */
	String		defaultValue();
	
	/** Return true if the directory associated with the enum element should
	 *  be created if it doesn't already exist.
	 */
	boolean		createIfNecessary();
}

Listing 4: LocationsSupport.java, the LocationsSupport workhorse class.

package com.holub.util;

import java.io.File;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;
import java.util.Properties;
import java.util.Map.Entry;

import org.apache.log4j.Logger;
import org.apache.log4j.helpers.NullEnumeration;

//----------------------------------------------------------------------
public class LocationsSupport<T extends Enum<T> & Locations>
{	
	/** 
	 * Create the logger.
	 * Note that the ExtendedLogger uses the LocationsSupport class (via the Places enum).
	 * Consequently, we can't use it to access the log4j.properties file. I've solved the
	 * problem by putting log4j.properties onto the classpath as well, but if you don't
	 * do that, we'll just print log messages to stderr instead of using log4j. Also,
	 * we'll use a standard log4j Logger instead of an ExtendedLogger:
	 */
	private static final Logger log = Logger.getLogger(Locations.class);
	
	
	/** We have two choices of where to store the File object associated
	 *  with a given key: we can push it into the enum element using some
	 *  sort of accessor method, or we can store it locally and the enum can
	 *  pull it out of the current object. The main problem with the push-it-into-the-enum
	 *  approach is that that would expose a "set" method in the enum element that anybody could
	 *  call at any point to change the File value, and I don't want that to
	 *  be possible. So, I'll put up with the minor inefficiency of looking up the
	 *  File value locally every time we need it.
	 */ 
	
	private Map<String,File> dictionary = new HashMap<String,File>();
	
	/** For every enum element in the array, treat keys[i].name() as a key
	 *  and load the associated value from the following places (in order):
	 *  <ol>
	 *  <li>a -D command-line switch (in System properties).
	 *  <li>if no -D value found, an environment variable with the same name as the key.
	 *  <li>if no environment found, the default stored in the Enum element itself.
	 *  </ol>
	 *  That value must identify an existing directory in the file system, and a
	 *  File representing that location can be retrieved from {@link #directory(Enum)}.
	 * 
	 * @throws IllegalStateException if a given key doesn't have value associated with it or if
	 * 			that value doesn't identify an existing directory.
	 * 
	 * @param keys The values() array associated with the enum that's using this helper class.
	 * @throws IOException 
	 */
	public LocationsSupport( T[] keys ) throws IllegalStateException
	{
		StringBuilder logMessage = new StringBuilder( "Loaded environment/-D properties:\n" );
		try
		{
			for( T element : keys )
			{	
				String how	 = "????";
				String key   = element.name();
				
				String value;
				
				if( (value = System.getProperty(key)) != null )
					how = "from system property (-D)";
				else if( (value = System.getenv(key)) != null )
					how = "from environment";
				else if( (value = element.defaultValue()) != null )
					how = "from default. mapped from: " + value;
				
				if( value != null )
					value = value.replaceAll("~", System.getProperty("user.home") ).trim();

				if( value == null || value.length()==0 )
					throw new IllegalStateException("Value for " + key + " cannot be null or empty." );
				
				File location = new File( value );
				
				try
				{	if( element.createIfNecessary() )
						if( !location.exists() )
						{	
							log.info("Creating directory: " + asString(location) );
							if( !location.mkdirs() )
								throw new IllegalStateException( "Couldn't create " + asString(location) );
						}
				}
				catch(SecurityException e) // unexpected, but catch it on the off chance.
				{	throw new IllegalStateException( "Not permitted to create " + asString(location) );
				}
					
				if( !location.isDirectory() )
					throw new IllegalStateException(
							  "Location specified in " 
							+ key 
							+ " (" 
							+ asString(location) 
							+ ") does not exist or is not a directory" );
				
				dictionary.put(key, location);
				logMessage.append("\t");
				logMessage.append(key);
				logMessage.append("=");
				logMessage.append( asString(location) );
				logMessage.append(" (");
				logMessage.append(how);
				logMessage.append(" )\n");
			}
		}
		finally
		{	
			// If logging isn't active, yet, then print to stderr.
			
			if( log.getAllAppenders() instanceof NullEnumeration )
				System.err.println(logMessage);
			else
				log.info(logMessage);
		}
	}
	
	/** Return the location associated with the indicated enum Element.
	 * 
	 * @param key
	 * @return
	 */
	public File directory( T element )
	{	return dictionary.get(element.name());
	}
	
	/** Export all the cached properties to the indicated
	 *  properties object. Handy if you need to pass a
	 *  properties object to some subsystem for initialization.
	 *  The locations are represented in "canonical" form if possible;
	 *  otherwise, the "absolute" form is used.
	 *  
	 *  @param target Load this object with the properties. If null,
	 *  			create a new properties object and load that. 
	 *  @return the properties object into which the values were put.
	 */
	public Properties export( Properties target )
	{
		if( target == null )
			target = new Properties();
		
		for( String key: dictionary.keySet() )
		{
			File location = dictionary.get(key);
			String value = asString( location );
			target.setProperty(key, value);
		}
		
		return target;
	}
	
	/** A convenience method that returns a string holding the
	 *  path to the desired location. Uses the File's "canonical"
	 *  form if possible, otherwise uses the "absolute" form.
	 */
	public static String asString( File location )
	{
		try
		{	return location.getCanonicalPath();
		} catch (IOException e)
		{	return location.getAbsolutePath();
		}
	}
	
	/** Returns the current set as a newline-separated list
	 *  of key=value pairs, suitable for exporting as a
	 *  properties file. If you want a more compact format, use
	 *  <code>{@link #export(Properties)}.toString()</code>, which
	 *  invokes {@link Properties#toString()}.
	 */
	@Override public String toString()
	{
		StringBuilder b = new StringBuilder();
		for( Entry<Object, Object> entry : export(null).entrySet() )
		{	
			b.append( (String)(entry.getKey()) );
			b.append( "=" );
			b.append( (String)(entry.getValue()) );
			b.append( "\n" );
		}
		return b.toString();
	}
}

Conclusion

So that's it. There is actually not all that much code here, and all the hard work is in the reusable LocationsSupport class, so it's easy to leverage this technique to create your own enums to track configurable locations. Location-based configuration is only the first part of the puzzle, though. In upcoming articles, I'll present a more complex enum that handles configuration files that hold key=value pairs where the values can be verified by type. That enum is based on the same principles as Places, but is a bit more complicated. (It requires the Java "reflection" APIs, for example.) I also plan to take an extensive look at the mock-based tests that I use for these classes so that you can get a handle on how to test this stuff.


Allen Holub is a speaker, lecturer, and writer on software development. His recent books have discussed Java, multithreading, and the use of patterns. He is a frequent contributor to Dr. Dobb's.

More Articles by Allen Holub

Getting Started with The Cloud: The Ecosystem

Getting Started with Google Apps and OAuth

Secure Login in AJAX Applications


Related Reading


More Insights






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