Channels ▼
RSS

.NET

Solving the Configuration Problem for Java Apps


A Digression: Java enums

Before looking at the code for the Places class, let's digress for a moment and talk about how Java enums work. Before the enum keyword was introduced, everybody used an idiom described by Joshua Bloch in his book Effective Java.

The basic principle is to limit the number of instances of a class, and then use each instance as an enum value. For example, you'd implement simple Java enum:

    enum Size { SMALL, MEDIUM, LARGE }

with the following class:

final class Size extends Enum   // Enum is part of the java language
{   
     public static final Size SMALL  = new Size("SMALL");
     public static final Size MEDIUM = new Size("MEDIUM");
     public static final Size LARGE  = new Size("LARGE");
  
     private final name = name;
     private static final List values = new ArrayList(); // List of all enum elements 
 
     private Size( String name )
     {   this.name = name;
         values.add( this );
     }
  
     public String name(){ return name; }
     public Static Iterator values(){ return values.iterator(); }
     
     //...
     public void normalMethod(){ /*...*/ }
     public static void staticMethod(){ /*...*/ }
}

The constructor is private, so I can create objects only inside the class itself. Only three instances of the Size class can possibly exist (SMALL, MEDIUM, LARGE). I remember the name of the field so that I can access it later by calling name(), and I keep a static collection of all possible values around (values on line 8). I can access an iterator across those values by calling values().

To understand Java enums, all you need remember is that these two implementations of the enum are exactly the same. The Java compiler effectively translates the enum definition into the aforementioned code (including the values() and name() methods, among others).

Also, since enums really are just a compact way to implement a standard class idiom, you can put methods in them. The earlier example had two extra methods at the bottom, and I can declare these in Java as follows:

enum Size
{   SMALL, MEDIUM, LARGE;

    public void normalMethod(){ /*...*/ }
    public static void staticMethod(){ /*...*/ }
}

You'd call them exactly as you would any other Java method:

    Size.SMALL.normalMethod();
    Size.staticMethod();

enums can be a lot more complicated than this. For more details, I'll refer you to the official Language Guide.

The Places enum

The next step is to define the Places enum (Listing Two), which I demonstrated earlier. Use it as follows:

File d = Places.CONFIG.directory();         // directory defined by -DCONFIG= or the CONFIG environment
File f = Places.CONFIG.file("x");           // file named x in the directory defined by -DCONFIG= or the CONFIG environment
String default = Places.CONFIG.default();   // default used if no -DCONFIG= or CONFIG environment exists

The enum also defines TMP and HOME places, which work the same way as CONFIG.

The first time any place element is accessed, all of them are verified. Consequently, you can front-load an exhaustive test of all the variables by accessing any of the enum elements early in main(), in a servlet's ServletConfig() method, or somewhere equivalent.

In particular, the initial tests verify that:

  • The -D argument, environment variable, or a default coded into the enum definition (as a constructor argument) is present. The key passed to -D (or the environment name), must look exactly like the enum-element name. Given Places.TMP, you'd use -DTMP=/my/Directory, or an environment variable named TMP.
  • There are values defined for the field.
  • The directory defined in the value actually exists. Some elements (TMP) relax this rule, but only to the extent that the enum creates the directory if it doesn't exist. In all cases, the directory will exist by the time the initial load finishes.

The load process also replaces any tildes found in the value string with the contents of the System user.home property before it checks for directory existence, so something like -DTMP=~/tmp works fine. (The tilde expansion is done automatically by UNIX shells, but not by Java, so I need to do it in the code.)

The definition of the Places enum in Listing Two is pretty trivial, since all the work is delegated to an underlying support object that I'll discuss shortly. This simplicity is not accidental. Since the real work is done elsewhere, it's easy to write your own version of Places if you need to do location-based configuration.

The three elements (CONFIG, HOME, and TMP) are defined at the top of the class definition. Note that constructor arguments are provided that specify the default locations to use as a last resort. The TMP constructor takes an additional boolean argument that tells the class to create the directory if it doesn't exist. If this argument is false (or missing), then the directory must exist or a ConfigurationError is thrown. The constructors themselves (line 62) don't do anything but save the arguments for later use by a support class. In fact, the defaultValue() and createIfNecessary() methods only exist so that the support object can access this information when it needs it.

The support object is created the first time you access a value (by the directory() method on line 77). The LocationsSupport constructor does all the verification I just described. The reset() method (on line 121) sets the support reference to null, so the enum will recreate the support object the next time you access a value. That's handy if you intend to set values programmically. Remember, a -Dkey=value does nothing but set a System property that you access with

     String value = System.getProperty("key");

There's also a

     System.setProperty("key", "newValue");

that you can use to preempt a -D passed on the JVM command line. If you do that, however, you should call reset() to make sure your new value is loaded. The main reason the method exists is to facilitate testing (I'll look at exhaustive set of tests in a future article). If I set up a (perhaps illegal) value in a test to see what will happen, I'll need to call reset() to make sure that my test value is loaded properly.

There are a bunch of design issues here. Should the support object really pull information out of the container? This seems to violate the "Delegation" principle. The alternative, however, is some sort of complicated callback. I can't pass enum-element-specific information into the LocationsSupport constructor because the LocationsSupport object is a class-level object. There's only one reference to it, stored in a static variable. If I passed information for each enum element, I'd effectively be passing a collection of enum-element-information objects, but how is that different from just passing the enum elements themselves and having the support object just ask for the information it needs. I can't pass element-specific information into the support object in the element-level constructor because the support object doesn't exist when the constructor executes, and I don't want to move up the support creation because the creation process could throw an exception, and I don't want that exception to be uncatchable. Putting accessors into the enum seemed to be the simplest solution.

Listing Two: Places.java, the Places enum.

package com.holub.util;

import java.io.File;
import java.util.*;

/**
 * A class to make it easy and safe to use the standard locations
 * in the file system. Locations are guaranteed to exist, and
 * this test is made on first load, so you don't have to test
 * on every use.
 * Default locations are hard coded in the current class as follows:
 * <PRE>
	CONFIG = "~/projects/src/config"
	HOME   = "~"
	TMP	   = "~/tmp"
 * </PRE>
 * The CONFIG and HOME directories must exist, but the the TMP
 * directory is created if it doesn't exist.
 * These default can be overridden by
 * environment variables that have exactly the same name as the enum
 * element, or with a -D command-line switch (also with the same name
 * as the enum element; e.g.: -DTMP=/tmp).
 * You can also set
 * a system property dynamically at runtime:
 * <PRE>
 * System.setProperty("TMP", "/tmp");
 * Places.reset();
 * </PRE>
 * Values are loaded
 * and checked the first time a location is used (and also the
 * first time any location is used after a {@link #reset()} call).
 * 
 * @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 enum Places implements Locations
{
	CONFIG ("~/config"),	
	HOME   ("~"),
	TMP	   ("~/tmp", true);
	
	//----------------------------------------------------------------------
	private static LocationsSupport<Places>	support = null;
	private final	String					defaultValue;
	private final	boolean 				createIfNecessary;
	
	//----------------------------------------------------------------------
	
	/** Create an enum element
	 * @param defaultValue	The default value. All ~ characters are replaced by
	 * 						the contents of the user.home system property.
	 * @param createIfNecessary
	 */
	
	Places( String defaultValue, boolean createIfNecessary )	//{=Places.Places}
	{	this.defaultValue	   = defaultValue;
		this.createIfNecessary = createIfNecessary;
	}
	
	/** Convenience constructor, same as
	 *  {@link #Places(String, boolean) Places(defaultValue,false)}
	 */
	
	Places( String defaultValue )
	{	this(defaultValue,false);
	}
	
	//----------------------------------------------------------------------
	@Override 
	public File directory() throws IllegalStateException	//{=Places.directory}
	{	if( support == null )
			support = new LocationsSupport<Places>( values() );
		return support.directory(this);
	}
	
	@Override
	public File file( String name )
	{	return new File( directory(), name );
	}
	
	@Override
	public String defaultValue()
	{	return defaultValue;
	}
	
	@Override
	public boolean createIfNecessary()
	{	return createIfNecessary;
	}
	//----------------------------------------------------------------------
	/** Returns the location (value) associated with this
	 *  enum as a String. The {@link #directory()} method returns
	 *  the same value, but as a {@link File}.
	 */
	@Override public String toString()
	{	return LocationsSupport.asString( directory() );
	}
	
	//----------------------------------------------------------------------
	/** A simple wrapper around {@Location#exporProperties)},
	 *  copies all the enum elements, along with the full
	 *  path names to the associated directories, into the
	 *  {@code target}.
	 */
	public static Properties export(Properties target)
	{	return support.export(target);
	}
	
	/** Call this method after programmatically setting a
	 *  System property that would change a location value.
	 *  This call forces all values to be reloaded
	 *  on the next enum-element access.
	 */
	public static void reset()	//{=Places.reset}
	{	support = null;
	}
}


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