Saturday, 11 July 2009

Dynamic Java Compilation & Class Loading in Coldfusion

Ever wanted to compile java classes from coldfusion? Instantiate classes and load jars not on the classpath at runtime?

Who hasn't?

The java class below uses the Java 6 Compiler API to allow you to compile classes on the fly from coldfusion, and a Classloader to load classes and jars not already on the classpath.

package org.adrianwalker.coldfusion.java.util;

import java.io.File;
import java.io.FileFilter;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.List;

import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;

public final class CfJavaUtil {

  private static final String JAVA_EXTENSION = ".java";
  private static final String JAR_EXTENSION = ".jar";

  private final File srcDir;
  private final File destDir;
  private final File[] classDirs;

  private final String classpath;
  private final URL[] classpaths;

  private final URLClassLoader ucl;

  public CfJavaUtil(final String srcPath, final String destPath,
      final String[] classPaths) throws IOException {

    srcDir = checkPath(srcPath, false);
    destDir = checkPath(destPath, true);
    classDirs = checkPaths(classPaths, false);

    classpath = buildClassPath(classDirs);
    classpaths = buildClassPaths(classDirs);

    ucl = new URLClassLoader(classpaths);
  }

  public void compile() throws IOException, MalformedURLException,
      ClassNotFoundException {
    List<String> options = new ArrayList<String>();
    options.add("-classpath");
    options.add(classpath);
    options.add("-d");
    options.add(destDir.getAbsolutePath());

    List<File> files = findFiles(srcDir, FileDirectoryFilter.JAVA_FILTER);
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();

    if (null == compiler) {
      throw new IllegalStateException(
          "Could not create a compiler, make sure you are running in a jdk not a jre");
    }

    StandardJavaFileManager fileManager = compiler.getStandardFileManager(null,
        null, null);
    Iterable<? extends JavaFileObject> compilationUnits = fileManager
        .getJavaFileObjectsFromFiles(files);
    compiler.getTask(null, fileManager, null, options, null, compilationUnits)
        .call();
    fileManager.close();
  }

  public Object createObject(final String className)
      throws MalformedURLException, InstantiationException,
      IllegalAccessException, ClassNotFoundException {

    if (null == className) {
      throw new IllegalArgumentException("Argument 'className' is null");
    }

    return ucl.loadClass(className).newInstance();
  }

  private static String buildClassPath(final File[] classDirs) {

    StringBuilder classPathBuilder = new StringBuilder();
    for (File classDir : classDirs) {
      classPathBuilder.append(classDir.getAbsolutePath());
      List<File> files = findFiles(classDir, FileDirectoryFilter.JAR_FILTER);
      for (File file : files) {
        if (classPathBuilder.length() > 0) {
          classPathBuilder.append(File.pathSeparatorChar);
        }
        classPathBuilder.append(file.getAbsolutePath());
      }
    }
    return classPathBuilder.toString();
  }

  private static URL[] buildClassPaths(final File[] classDirs)
      throws MalformedURLException {
    List<URL> urls = new ArrayList<URL>();
    for (File classDir : classDirs) {
      urls.add(classDir.toURI().toURL());
      List<File> files = findFiles(classDir, FileDirectoryFilter.JAR_FILTER);
      for (File file : files) {
        urls.add(file.toURI().toURL());
      }
    }
    return urls.toArray(new URL[urls.size()]);
  }

  private static File[] checkPaths(final String[] paths, final boolean create)
      throws IOException {
    int pathsLength = paths.length;
    File[] dirs = new File[pathsLength];
    for (int i = 0; i < pathsLength; i++) {
      dirs[i] = checkPath(paths[i], create);
    }

    return dirs;
  }

  private static File checkPath(final String path, final boolean create)
      throws IOException {

    File dir = new File(path);

    if (create) {
      if (dir.exists() && !dir.isDirectory()) {
        throw new IOException(
            "Argument 'dir' exists and is not is not a directory");
      } else if (!dir.exists()) {
        boolean ok = dir.mkdirs();
        if (!ok) {
          throw new IOException("Could not create directory '"
              + dir.getAbsolutePath() + "'");
        }
      }
    } else {
      if (!dir.exists() || !dir.isDirectory()) {
        throw new IllegalArgumentException("Argument 'path' is not a directory");
      }
    }

    return dir;
  }

  private static List<File> findFiles(final File path, final FileFilter filter) {

    List<File> files = new ArrayList<File>();

    File[] fileList = path.listFiles(filter);
    for (File file : fileList) {
      if (file.isDirectory()) {
        files.addAll(findFiles(file, filter));
      } else {
        files.add(file);
      }
    }

    return files;
  }

  private static enum FileDirectoryFilter implements FileFilter {

    JAVA_FILTER(JAVA_EXTENSION), JAR_FILTER(JAR_EXTENSION);

    private final String extension;

    private FileDirectoryFilter(final String extension) {
      this.extension = extension;
    }

    @Override
    public boolean accept(File file) {

      if (file.isDirectory()) {
        return true;
      } else if (file.getName().endsWith(extension)) {
        return true;
      } else {
        return false;
      }
    }
  }
}

Compile the above code into a jar, place it in your C:\ColdFusion8\wwwroot\WEB-INF\lib directory, and restart the server to pic up the jar.

The CF code below shows an example of how the java utility class can be used.

<cfsilent>
  <cfscript>
    srcPath = expandPath("java/src");
    destPath = expandPath("java/bin");
    classPath = [destPath];
    cfjavautil = createObject(
                  "java",
                  "org.adrianwalker.coldfusion.java.util.CfJavaUtil"
                 ).init(srcPath, destPath, classPath);
    cfjavaUtil.compile();
    message = cfjavaUtil.createObject("Message");
  </cfscript>
</cfsilent>

<cfoutput>
  <h1>
    #message.getMessage("Dynamic Java Compilation")#
  </h1>
</cfoutput>

The code creates an instance of the CfJavaUtil class. Uses it to compile some java, then invoke the newly compiled java class.

The project layout for this code has the following structure:

The utility compiles the Message.java source file from the java/src directory to a class file in the java/bin directory. The utility is then called again to create a Message object and call its getMessage method.

The Java and Coldfusion code can be downloaded below. The Java code is built using Apache Maven, or if you can't be bothered to build it yourself, a jar compiled with Java 6 is downloadable below.

To use the Java 6 Compiler API feature you must run the utility in a JDK, not the standard coldfusion JRE. If you see the message below, you must reconfigure your coldfusion server.

To resolve this, point your coldfusion server to a directory containing a Java 6 JDK, and restart.

Source Code

Compiled JAR