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 enum
s 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