Saturday, July 25, 2009

ColdFusion 8: Experiment with compiling Java code from ColdFusion - Part 2

In Part 1 I described how you can use JavaCompiler's run() method to compile source code from a java.io.File. However the api also describes how you might extend the SimpleJavaFileObject class so you can compile source code from a string, rather than a physical file.

Since you can now compile code on-the-fly, you can easily use the CF code from part 1 to create their sample class: JavaSourceFromString.




<cfsavecontent variable="javaCode">
import javax.tools.SimpleJavaFileObject;
import java.net.URI;

/**
* A file object used to represent source coming from a string.
*/
public class JavaSourceFromString extends SimpleJavaFileObject {
/**
* The source code of this "file".
*/
final String code;

/**
* Constructs a new JavaSourceFromString.
* @param name the name of the compilation unit represented by this file object
* @param code the source code for the compilation unit represented by this file object
*/
public JavaSourceFromString(String name, String code) {
super(URI.create("string:///" + name.replace('.','/') + Kind.SOURCE.extension),
Kind.SOURCE);
this.code = code;
}

@Override
public CharSequence getCharContent(boolean ignoreEncodingErrors) {
return code;
}
}
</cfsavecontent>


<cfset pathToInputFile = "C:\myFiles\JavaSourceFromString.java" />
<cfset fileWrite(pathToInputFile, javaCode) />

However, instead of using the JavaLoader this time, we are going to output the class file directly to the WEB-INF\classes\ directory. That way the class will be automatically detected by ColdFusion. Now to redirect the output of the compiler, simply prepend the -d <directory> flag to the list of sources passed into the compiler.


<!--- create a stream to capture errors --->
<cfset errStream = createObject("java", "java.io.ByteArrayOutputStream").init() />

<!--- get a compiler reference --->
<cfset provider = createObject("java", "javax.tools.ToolProvider") />
<cfset compiler = provider.getSystemJavaCompiler() />

<cfset args = [ "-d", "C:\ColdFusion8\wwwroot\WEB-INF\classes\", pathToInputFile ]>
<cfset status = compiler.run( javacast("null", ""),
javacast("null", ""),
errStream,
args
) />

<!--- display the status and any error messages --->
<!--- status: 0 == success --->
<cfset message = toString(errStream.toByteArray()) />
<cfset errStream.close() />
<b>COMPILER RESULTS:</b><hr />
<cfoutput>
STATUS: #status# (0 == success)<br />
ERRORS: #message# <br /><br />
</cfoutput>

Once the code is compiled, ColdFusion should automatically detect the new .class file in WEB-INF\classes\. So if everything went as planned, the new class should be ready to use right away, without rebooting the server. (Note: If you make changes and compile the class a second time, ColdFusion will not automatically reload the class).

Using the new class is very simple. Just create a new instance of JavaSourceFromString, and pass in the name of your class and the source code as strings.

<cfsavecontent variable="javaCode">
import java.util.List;
import java.util.Vector;
public class MyClass
{
public static List<String> test() {
List<String> list = new Vector<String>();
list.add("Congratulations");
list.add("It worked");
return list;
}
}
</cfsavecontent>

<cfset SourceFromString = createObject("java", "JavaSourceFromString") />
<cfset fileObject = SourceFromString.init( "MyClass", javaCode) />
<cfset sourceList = [ fileObject ] />


Now to compile these sources, we are going to try something a little different: using a CompilationTask. This option just provides some additional control over the compile process. We are also going to use a DiagnosticCollector object, which provides an easy way to access error message details.

You can create a CompilationTask using the compiler's getTask() method. Most of the arguments are the same as for the run() method. But there are two additional arguments: options and classNames. The options argument is just an array of settings you want to pass to the compiler. Such as the -d flag used in the previous example. We are going to use that flag again to send the output to the WEB-INF\classes directory. Now to perform the actual compilation, use the call() method.


<cfset provider = createObject("java", "javax.tools.ToolProvider") />
<cfset compiler = provider.getSystemJavaCompiler() />

<cfset collector = createObject("java", "javax.tools.DiagnosticCollector").init() />
<cfset args = ["-d", "C:\ColdFusion8\wwwroot\WEB-INF\classes\" ]>
<cfset task = compiler.getTask( javacast("null", ""),
javacast("null", ""),
collector,
args,
javacast("null", ""),
sourceList
) />
<cfset status = task.call() />


To retrieve any error messages from the DiagnosticCollector, simply call the getDiagnostics() method. It returns an array of Diagnostic objects.


<cfset results = collector.getDiagnostics() />
COMPILER RESULTS:<hr />
<cfoutput>
STATUS: #status# (0 == success) <br />
<cfloop array="#results#" index="diagnostic">
Error on #diagnostic.getLineNumber()#
Starting at #diagnostic.getStartPosition()# <hr />
#diagnostic.getMessage(javacast("null", ""))# <br />
</cfloop>
<br />
</cfoutput>


All that is left is to test the compiled class. If everything went well, your results should look something like this. Ah, cfdump has never looked better ;)


COLDFUSION RESULTS:<hr />
<cfset myObj = createObject("java", "MyClass") />
<cfdump var="#myObj.test()#" label="MyClass.test();" />




As always comments/questions/corrections are welcome.

3 comments:

Mike Henke July 26, 2009 at 7:26 AM  

This is awesome. Thanks, I was wondering how to compile without using the cfcompile bat. Keep up the great posts.

TJ July 26, 2009 at 8:59 AM  

Interesting post! I can't see many use cases where I would want to use it, but it does give one more insight into the inner workings of CF and how closely it is tied to the JVM. I could possibly see this being used to compile a class dynamically onServerStart, although for what purpose I have no idea!

cfSearching August 30, 2009 at 8:12 PM  

Re-post of a user comment deleted by accident

By User: DonOmite
I agree this is interesting, but what can I do with it for a web app? Wouldn't it just be easier to make a cfx or such and use that? If I could run java on the fly and use it, now that would be kewl.

  © Blogger templates The Professional Template by Ourblogtemplates.com 2008

Header image adapted from atomicjeep