ONJava.com -- The Independent Source for Enterprise Java
oreilly.comSafari Books Online.Conferences.

advertisement

AddThis Social Bookmark Button

Servlet Best Practices, Part 1
Pages: 1, 2, 3, 4

Load Configuration Files from the Classpath

From Servlet API 1.0 through Servlet API 2.3, servlets have distinctly lacked a standard mechanism to retrieve external configuration files. Although many server-side libraries require configuration files, servlets have no commonly accepted way to locate them. When a servlet runs under J2EE, it receives support for JNDI, which can provide a certain amount of configuration information. But the common web server configuration file problem remains.



The best solution (or perhaps I should call it the "lesser evil" solution) is to locate files with a search of the classpath and/or the resource path. This lets server admins place server-wide configuration files in the web server's classpath, or place per-application configuration files in WEB-INF/classes found in the resource path. It also works equally well for locating configuration files placed within WAR files and/or deployed across multiple back-end servlet containers. In fact, using files for configuration has several advantages, even when JNDI is available. The component provider can include a set of "sample" or "default" configuration files. One configuration file can be made to work across the entire server. And finally, configuration files are trivially easy to understand for both the developer and deployer.

Example 3-4 demonstrates the search technique with a class called Resource. Given a resource name, the Resource constructor searches the class path and resource path attempting to locate the resource. When the resource is found, it makes available the resource contents as well as its directory location and last modified time (if those are available). The last modified time helps an application know, for example, when to reload the configuration data. The class uses special code to convert file: URL resources to File objects. This proves handy because URLs, even file: URLs, often don't expose special features such as a modified time. By searching both the class path and the resource path this class can find server-wide resources and per-application resources. The source code for this class can also be downloaded from http://www.servlets.com.

Example 3-4: A standard Resource locator

import java.io.*;
import java.net.*;
import java.util.*;

/**
 * A class to locate resources, retrieve their contents, and determine their
 * last modified time. To find the resource the class searches the CLASSPATH
 * first, then Resource.class.getResource("/" + name). If the Resource finds
 * a "file:" URL, the file path will be treated as a file. Otherwise, the
 * path is treated as a URL and has limited last modified info.
 */
public class Resource implements Serializable {

  private String name;
  private File file;
  private URL url;

  public Resource(String name) throws IOException {
    this.name = name;
    SecurityException exception = null;

    try {
      // Search using the CLASSPATH. If found, "file" is set and the call
      // returns true.  A SecurityException might bubble up.
      if (tryClasspath(name)) {
        return;
      }
    }
    catch (SecurityException e) {
      exception = e;  // Save for later.
    }

    try {
      // Search using the classloader getResource(  ). If found as a file,
      // "file" is set; if found as a URL, "url" is set.
      if (tryLoader(name)) {
        return;
      }
    }
    catch (SecurityException e) {
      exception = e;  // Save for later.
    }

    // If you get here, something went wrong. Report the exception.
    String msg = "";
    if (exception != null) {
      msg = ": " + exception;
    }

    throw new IOException("Resource '" + name + "' could not be found in " +
      "the CLASSPATH (" + System.getProperty("java.class.path") +
      "), nor could it be located by the classloader responsible for the " +
      "web application (WEB-INF/classes)" + msg);
  }

  /**
   * Returns the resource name, as passed to the constructor
   */
  public String getName(  ) {
    return name;
  }

  /**
   * Returns an input stream to read the resource contents
   */
  public InputStream getInputStream(  ) throws IOException {
    if (file != null) {
      return new BufferedInputStream(new FileInputStream(file));
    }
    else if (url != null) {
      return new BufferedInputStream(url.openStream(  ));
    }
    return null;
  }

  /**
   * Returns when the resource was last modified. If the resource 
   * was found using a URL, this method will work only if the URL 
   * connection supports last modified information. If there's no 
   * support, Long.MAX_VALUE is returned. Perhaps this should return 
   * -1, but you should return MAX_VALUE on the assumption that if
   * you can't determine the time, it's maximally new.
   */
  public long lastModified(  ) {
    if (file != null) {
      return file.lastModified(  );
    }
    else if (url != null) {
      try {
        return url.openConnection(  ).getLastModified(  );  // Hail Mary
      }
      catch (IOException e) { return Long.MAX_VALUE; }
    }
    return 0;  // can't happen
  }
   
  /**
   * Returns the directory containing the resource, or null if the 
   * resource isn't directly available on the filesystem. 
   * This value can be used to locate the configuration file on disk,
   * or to write files in the same directory.
   */
  public String getDirectory(  ) {
    if (file != null) {
      return file.getParent(  );
    }
    else if (url != null) {
      return null;
    }
    return null;
  }
 
  // Returns true if found
  private boolean tryClasspath(String filename) {
    String classpath = System.getProperty("java.class.path");
    String[  ] paths = split(classpath, File.pathSeparator);
    file = searchDirectories(paths, filename);
    return (file != null);
  }

  private static File searchDirectories(String[  ] paths, String filename) {
    SecurityException exception = null;
    for (int i = 0; i < paths.length; i++) {
      try {
        File file = new File(paths[i], filename);
        if (file.exists(  ) && !file.isDirectory(  )) {
          return file;
        }
      }
      catch (SecurityException e) {
        // Security exceptions can usually be ignored, but if all attempts
        // to find the file fail, report the (last) security exception.
        exception = e;
      }
    }
    // Couldn't find any match
    if (exception != null) {
      throw exception;
    }
    else {
      return null;
    }
  }

  // Splits a String into pieces according to a delimiter.
  // Uses JDK 1.1 classes for backward compatibility.
  // JDK 1.4 actually has a split(  ) method now.
  private static String[  ] split(String str, String delim) {
    // Use a Vector to hold the split strings.
    Vector v = new Vector(  );

    // Use a StringTokenizer to do the splitting.
    StringTokenizer tokenizer = new StringTokenizer(str, delim);
    while (tokenizer.hasMoreTokens(  )) {
      v.addElement(tokenizer.nextToken(  ));
    }

    String[  ] ret = new String[v.size(  )];
    v.copyInto(ret);
    return ret;
  }

  // Returns true if found
  private boolean tryLoader(String name) {
    name = "/" + name;
    URL res = Resource.class.getResource(name);
    if (res =  = null) {
      return false;
    }

    // Try converting from a URL to a File.
    File resFile = urlToFile(res);
    if (resFile != null) {
      file = resFile;
    }
    else {
      url = res;
    }
  }

  private static File urlToFile(URL res) {
    String externalForm = res.toExternalForm(  );
    if (externalForm.startsWith("file:")) {
      return new File(externalForm.substring(5));
    }
    return null;
  }

  public String toString(  ) {
    return "[Resource: File: " + file + " URL: " + url + "]";
  }
}

Example 3-4 shows a fairly realistic example of how the class can be used. Assume your servlet library component needs to load some chunk of raw data from the filesystem. This file can be named anything, but the name must be entered in a library.properties main configuration file. Because the data, in some situations, takes a while to process in its raw form, the library keeps a serialized version of the data around in a second file named library.ser to speed up load times. The cache file, if any, resides in the same directory as the main configuration file. Example 3-5 gives the code implementing this logic, building on the Resource class.

Example 3-5: Loading configuration information from a Resource

import java.io.*;
import java.util.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class LibraryLoader {

  static final String CONFIG_FILE = "library.properties";
  static final String CACHE_FILE =  "library.ser";

  public ConfigData load(  ) throws IOException {
    // Find the configuration file and fetch its contents as Properties.
    Resource config = new Resource(CONFIG_FILE);
    Properties props = new Properties(  );
    InputStream in = null;
    try {
      in = config.getInputStream(  );
      props.load(in);
    }
    finally {
      if (in != null) in.close(  );   // IOException propagates up.
    }

    // Determine the source directory of the configuration file and 
	// look for a cache file next to it containing a full
    // representation of your program state. If you find a
    // cache file and it is current, load and return that data.
    if (config.getDirectory(  ) != null) {
      File cache = new File(config.getDirectory(  ), CACHE_FILE);
      if (cache.exists(  ) &&
            cache.lastModified(  ) >= config.lastModified(  )) {
        try {
          return loadCache(new FileInputStream(cache));
        }
        catch (IOException ignored) { }
      }
    }

    // You get here if there's no cache file or it's stale and 
    // you need to do a full reload. Locate the name of the raw 
    // datafile from the configuration file and return its contents 
	// using Resource.
    Resource data = new Resource(props.getProperty("data.file"));
    return loadData(data.getInputStream(  ));
  }

  private ConfigData loadCache(InputStream in) {
    // Read the file, perhaps as a serialized object.
    return null;
  }

  private ConfigData loadData(InputStream in) {
    // Read the file, perhaps as XML.
    return null;
  }

  class ConfigData {
    // An example class that would hold configuration data
  }
}

The loading code doesn't need to concern itself with where the resource might be located. The Resource class searches the class path and resource path and pulls from the WAR if necessary.

Pages: 1, 2, 3, 4

Next Pagearrow