A Digression: Java enums
Before looking at the code for the Places
class, let's digress for a moment and talk about how Java enum
s 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 enum
s, 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 enum
s 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();
enum
s 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 theenum
definition (as a constructor argument) is present. The key passed to-D
(or the environment name), must look exactly like theenum
-element name. GivenPlaces.TMP
, you'd use-DTMP=/my/Directory
, or an environment variable namedTMP
. - 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 theenum
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; } }