package org.marketcetera.strategy;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.Map.Entry;
import javax.tools.Diagnostic;
import javax.tools.DiagnosticCollector;
import javax.tools.FileObject;
import javax.tools.ForwardingJavaFileManager;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.SimpleJavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.StandardLocation;
import javax.tools.ToolProvider;
import javax.tools.JavaCompiler.CompilationTask;
import javax.tools.JavaFileObject.Kind;
import org.marketcetera.core.ClassVersion;
import org.marketcetera.event.impl.LogEventBuilder;
import org.marketcetera.module.ModuleManager;
import org.marketcetera.util.log.I18NBoundMessage1P;
import org.marketcetera.util.log.SLF4JLoggerProxy;
/* $License$ */
/**
* Executes a Java strategy using the <a href="http://www.jcp.org/en/jsr/detail?id=199">Java Compiler API</code>.
*
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $
* @since 1.0.0
*/
@ClassVersion("$Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $")
public class JavaCompilerExecutionEngine
implements ExecutionEngine, Messages
{
/**
* System properties key to set to inform the Java compiler of dependencies it should include on the classpath for compiling Java strategies
*/
public static final String CLASSPATH_KEY = "metc.java.class.path"; //$NON-NLS-1$
/**
* the strategy to be executed
*/
private Strategy strategy;
/**
* the processed/prepared script from the strategy
*/
private String processedScript;
/**
* map of cannonical classname to fully-qualified classname generated by the compiler and used by the classloader
*/
private Map<String,String> fullyQualifiedClassnames = new HashMap<String,String>();
/* (non-Javadoc)
* @see org.marketcetera.strategy.ExecutionEngine#prepare(org.marketcetera.strategy.Strategy, java.lang.String)
*/
@Override
public void prepare(Strategy inStrategy,
String inProcessedScript)
throws StrategyException
{
strategy = inStrategy;
processedScript = inProcessedScript;
}
/* (non-Javadoc)
* @see org.marketcetera.strategy.ExecutionEngine#start()
*/
@Override
public Object start()
throws StrategyException
{
// the compiler object to use
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
if(compiler == null) {
throw new StrategyException(MISSING_JAVA_COMPILER);
}
// A map of class names to the InMemoryJavaFileObject that holds
// the compiled-code for that class. This is the cache of
// compiled classes.
Map<String,InMemoryJavaFileObject> output = new HashMap<String,InMemoryJavaFileObject>();
// Create a classloader for our new classes based on the current classloader
// this classloader will stop operating when the enclosing object goes out of scope or
// is replaced by a start/stop cycle of the strategy
ClassLoader loader = new InMemoryClassLoader(output,
ModuleManager.getInstance() == null ? StrategyModule.class.getClassLoader() : ModuleManager.getInstance().getClassLoader());
// this object is for the compile phase - it stores errors and warnings generated by the compilation
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<JavaFileObject>();
// the fileManager manages sources and targets for the compiler - this is the basic model
// which we'll specialize next to make compilation in-memory
StandardJavaFileManager standardFileManager = compiler.getStandardFileManager(diagnostics,
null,
null);
// this is the specialized file manager that produces source and stores byte-code all in-memory
InMemoryFileManager specializedFileManager = new InMemoryFileManager(standardFileManager,
output,
loader);
// source file objects are produced for each thing to be compiled. For us, this is the strategy script, which
// contains 1 or more classes. notice that this is where the strategy name is associated with the source. this
// is required by the java compiler which dictates that a class name must match the file name
SourceJavaFileObject sourceObject;
try {
sourceObject = new SourceJavaFileObject(strategy.getName(),
processedScript);
} catch (URISyntaxException e) {
throw new StrategyException(e,
new I18NBoundMessage1P(INVALID_STRATEGY_NAME,
strategy.toString()));
}
// prepare the options to pass to the compiler
// collect classpath entries
Set<String> classpathEntries = new LinkedHashSet<String>();
// add the system classpath
String systemPath = System.getProperty("java.class.path"); //$NON-NLS-1$
if(systemPath != null) {
String[] entries = systemPath.split(File.pathSeparator);
classpathEntries.addAll(Arrays.asList(entries));
}
// add jars we are given by the parent class loaders, if any
ClassLoader currentLoader = loader;
do {
if(currentLoader instanceof URLClassLoader) {
for(URL url: ((URLClassLoader)currentLoader).getURLs()) {
try {
classpathEntries.add(url.toURI().getPath());
} catch (URISyntaxException e) {
Messages.ERROR_CONVERTING_CLASSPATH_URL.warn(this,
e,
url);
}
}
}
} while((currentLoader = currentLoader.getParent()) != null);
// add our custom classpath
String customPath = System.getProperty(CLASSPATH_KEY);
if(customPath != null) {
String[] entries = customPath.split(File.pathSeparator);
classpathEntries.addAll(Arrays.asList(entries));
}
String strategyPath = System.getProperty(Strategy.CLASSPATH_PROPERTYNAME);
if(strategyPath != null) {
String[] entries = strategyPath.split(File.pathSeparator);
classpathEntries.addAll(Arrays.asList(entries));
}
// put the classpath string in place with the classpath command-line option
List<String> options = new ArrayList<String>();
// make debug symbols available in the compiled strategy
options.add("-g"); //$NON-NLS-1$
options.add("-cp"); //$NON-NLS-1$
StringBuilder classpathString = new StringBuilder();
for(String entry : classpathEntries) {
classpathString.append(entry).append(File.pathSeparator);
}
options.add(classpathString.toString());
SLF4JLoggerProxy.debug(JavaCompilerExecutionEngine.class,
"Java compiler compiling {} with options {} (classpath length: {})", //$NON-NLS-1$
strategy.getName(),
Arrays.toString(options.toArray()),
classpathString.length());
// schedule the compilation task
CompilationTask compilationJob = compiler.getTask(null, // out-writer not needed because we're using the in-memory file manager
specializedFileManager,
diagnostics,
options,
null, // no annotation processing needed
Arrays.asList(sourceObject));
// wait for the compilation job to complete
if (!compilationJob.call()) {
// compilation failed, deal with the errors
CompilationFailed failed = new CompilationFailed(strategy);
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
if(diagnostic.getKind().equals(Diagnostic.Kind.ERROR)) {
failed.addDiagnostic(CompilationFailed.Diagnostic.error(diagnostic.toString()));
} else {
failed.addDiagnostic(CompilationFailed.Diagnostic.warning(diagnostic.toString()));
}
}
StrategyModule.log(LogEventBuilder.error().withMessage(COMPILATION_FAILED,
String.valueOf(strategy),
failed.toString())
.withException(failed).create(),
strategy);
throw failed;
} else {
// compilation succeeded with or without warnings
for (Diagnostic<? extends JavaFileObject> diagnostic : diagnostics.getDiagnostics()) {
StrategyModule.log(LogEventBuilder.warn().withMessage(COMPILATION_FAILED_DIAGNOSTIC,
String.valueOf(diagnostic.getKind()),
String.valueOf(diagnostic)).create(),
strategy);
}
}
// strategy has compiled successfully and is now held in our specializedFileManager
try {
// load the class from the specialized class loader that caches the compiled strategy classes
// remember that the strategy name is specified without a package name, but the classloader needs
// to know the fully-qualified classname with package, so check the mappings we created for fully-qualified
// class names
String fullyQualifiedClassname = fullyQualifiedClassnames.get(strategy.getName());
SLF4JLoggerProxy.debug(JavaCompilerExecutionEngine.class,
"The fully-qualified name of {} is {}", //$NON-NLS-1$
strategy.getName(),
fullyQualifiedClassname);
assert(fullyQualifiedClassname != null);
Class<?> c = Class.forName(fullyQualifiedClassname,
true,
loader);
// the strategy class is supposed to be a subclass of Strategy and have a default constructor
// note that this implicitly loads helper classes as necessary
return c.newInstance();
} catch (Exception e) {
// the myriad of exceptions that can be thrown with the above couple of lines all amount to the same
// thing: the black magic of the compiler, in-memory objects, and the classloader somehow malfunctioned.
// this would be a warranty repair: nothing the user can do. might as well call it a compilation problem
// as well as call it anything else.
StrategyModule.log(LogEventBuilder.error().withMessage(COMPILATION_FAILED,
String.valueOf(strategy),
String.valueOf(e))
.withException(e).create(),
strategy);
throw new CompilationFailed(e,
strategy);
}
}
/* (non-Javadoc)
* @see org.marketcetera.strategy.ExecutionEngine#stop()
*/
@Override
public void stop()
throws StrategyException
{
// nothing to do
}
/**
* Represents the Java source of a strategy containing one or more classes to compile.
*
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $
* @since 1.0.0
*/
@ClassVersion("$Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $")
private static class SourceJavaFileObject
extends SimpleJavaFileObject
{
/**
* indicates a java file
*/
private static final String JAVA_EXTENSION = ".java"; //$NON-NLS-1$
/**
* the source to compile
*/
private final String source;
/**
* Create a new SourceJavaFileObject instance.
*
* @param inClassName a <code>String</code> value containing the name of the class
* @param inClassSource a <code>String</code> value containing the contents of the pseudo-file
* @throws URISyntaxException if the given <code>inClassName</code> cannot be translated to a <code>URI</code>
*/
private SourceJavaFileObject(String inClassName,
String inClassSource)
throws URISyntaxException
{
super(new URI(String.format("%s%s", //$NON-NLS-1$
inClassName,
JAVA_EXTENSION)),
Kind.SOURCE);
source = inClassSource;
}
/* (non-Javadoc)
* @see javax.tools.SimpleJavaFileObject#getCharContent(boolean)
*/
@Override
public CharSequence getCharContent(boolean inIgnoreEncodingErrors)
{
return source;
}
}
/**
* Classloader that caches the definitions of some classes in memory and
* defers to its parent for others.
*
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $
* @since 1.0.0
*/
@ClassVersion("$Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $")
private static class InMemoryClassLoader
extends ClassLoader
{
/**
* the cache of class definitions by name
*/
private final Map<String,InMemoryJavaFileObject> cache;
/**
* Create a new InMemoryClassLoader instance.
*
* @param inOutput a <code>Map<String,InMemoryJavaFileObject></code> value in which to cache class definitions
* @param inParent a <code>ClassLoader</code> value containing the parent classloader to use
*/
private InMemoryClassLoader(Map<String,InMemoryJavaFileObject> inOutput,
ClassLoader inParent)
{
super(inParent);
cache = inOutput;
}
/* (non-Javadoc)
* @see java.lang.ClassLoader#findClass(java.lang.String)
*/
@Override
protected Class<?> findClass(String inName)
throws ClassNotFoundException
{
// check our cache for the class
InMemoryJavaFileObject fileObject = cache.get(inName);
if (fileObject != null) {
// class is in our cache, return that version of it
byte[] bytes = fileObject.getBytes();
return defineClass(inName,
bytes,
0,
bytes.length);
}
// class is not in-cache, return the parent's definition (if it has one)
return super.findClass(inName);
}
}
/**
* File manager which maintains file contents in memory.
*
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $
* @since 1.0.0
*/
@ClassVersion("$Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $")
private class InMemoryFileManager
extends ForwardingJavaFileManager<StandardJavaFileManager>
{
/**
* the mapping of classname to file object
*/
private final Map<String,InMemoryJavaFileObject> output;
/**
* the classloader to use to load the classes
*/
private final ClassLoader loader;
/**
* Create a new InMemoryFileManager instance.
*
* @param inFileManager a <code>StandardJavaFileManager</code> value to use as the parent file manager
* @param inOutput a <code>Map<String,InMemoryJavaFileObject></code> value containing class definitions by name
* @param inClassLoader a <code>ClassLoader</code> value containing the classloader to use
*/
private InMemoryFileManager(StandardJavaFileManager inFileManager,
Map<String,InMemoryJavaFileObject> inOutput,
ClassLoader inClassLoader)
{
super(inFileManager);
output = inOutput;
loader = inClassLoader;
}
/* (non-Javadoc)
* @see javax.tools.ForwardingJavaFileManager#getJavaFileForOutput(javax.tools.JavaFileManager.Location, java.lang.String, javax.tools.JavaFileObject.Kind, javax.tools.FileObject)
*/
@Override
public JavaFileObject getJavaFileForOutput(Location inLocation,
String inFullyQualifiedClassname,
Kind inKind,
FileObject inSibling)
throws IOException
{
InMemoryJavaFileObject javaFileObject;
try {
javaFileObject = new InMemoryJavaFileObject(inFullyQualifiedClassname,
inKind);
} catch (URISyntaxException e) {
throw new IOException(e);
}
// add the java file object in the cache by name
// sort out the cannonical name from the fully-qualified name if the strategy code specified a package
String cannonicalClassname = getCannonicalClassname(inFullyQualifiedClassname);
// map the cannonical name to the fully-qualified name (to be used later to load the class)
fullyQualifiedClassnames.put(cannonicalClassname,
inFullyQualifiedClassname);
// put the fully-qualified name in the classname cache
output.put(inFullyQualifiedClassname,
javaFileObject);
return javaFileObject;
}
/**
* Calculates the cannonical classname from the given fully-qualified classname.
*
* @param inFullyQualifiedClassname a <code>String</code> value
* @return a <code>String</code> value
*/
private String getCannonicalClassname(String inFullyQualifiedClassname)
{
String[] nameSegments = inFullyQualifiedClassname.split("\\."); //$NON-NLS-1$
return (nameSegments.length > 0 ? nameSegments[nameSegments.length-1] : inFullyQualifiedClassname);
}
/* (non-Javadoc)
* @see javax.tools.ForwardingJavaFileManager#getClassLoader(javax.tools.JavaFileManager.Location)
*/
@Override
public ClassLoader getClassLoader(Location inLocation)
{
return loader;
}
/* (non-Javadoc)
* @see javax.tools.ForwardingJavaFileManager#inferBinaryName(javax.tools.JavaFileManager.Location, javax.tools.JavaFileObject)
*/
@Override
public String inferBinaryName(Location inLocation,
JavaFileObject inJavaFileObject)
{
String result;
if(inLocation == StandardLocation.CLASS_PATH &&
inJavaFileObject instanceof InMemoryJavaFileObject) {
result = inJavaFileObject.getName();
} else {
result = super.inferBinaryName(inLocation,
inJavaFileObject);
}
return result;
}
/* (non-Javadoc)
* @see javax.tools.ForwardingJavaFileManager#list(javax.tools.JavaFileManager.Location, java.lang.String, java.util.Set, boolean)
*/
@Override
public Iterable<JavaFileObject> list(Location inLocation,
String inPackageName,
Set<Kind> inKinds,
boolean inShouldRecurse)
throws IOException
{
// get the list of files from the parent implementation
Iterable<JavaFileObject> result = super.list(inLocation,
inPackageName,
inKinds,
inShouldRecurse);
// the "list" function allows a classloader to indicate what classes it knows about.
// this next bit of code allows our in-memory classloader to list the classes that were
// just compiled. the package name is set oddly in this case (as implied below). this
// behavior isn't strictly necessary right now, but may be helpful in the future
if(inLocation == StandardLocation.CLASS_PATH &&
inPackageName.equals("just.generated") && //$NON-NLS-1$
inKinds.contains(JavaFileObject.Kind.CLASS)) {
// take the parent's classes
List<JavaFileObject> temp = new ArrayList<JavaFileObject>();
for (JavaFileObject fileObject : result) {
temp.add(fileObject);
}
// and add our in-memory classes
for (Entry<String,InMemoryJavaFileObject> entry : output.entrySet()) {
temp.add(entry.getValue());
}
result = temp;
}
return result;
}
}
/**
* An in-memory representation of a Java File Object.
*
* @author <a href="mailto:colin@marketcetera.com">Colin DuPlantis</a>
* @version $Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $
* @since 1.0.0
*/
@ClassVersion("$Id: JavaCompilerExecutionEngine.java 16844 2014-02-24 22:51:16Z colin $")
private static class InMemoryJavaFileObject
extends SimpleJavaFileObject
{
/**
* the bytes of the java file object
*/
private ByteArrayOutputStream bytes;
/**
* Create a new InMemoryJavaFileObject instance.
*
* @param name a <code>String</code> value
* @param kind a <code>Kind</code> value
* @throws URISyntaxException if the name cannot be made into a <code>URI</code>
*/
private InMemoryJavaFileObject(String name,
Kind kind)
throws URISyntaxException
{
super(new URI(name),
kind);
}
/* (non-Javadoc)
* @see javax.tools.SimpleJavaFileObject#openOutputStream()
*/
@Override
public OutputStream openOutputStream()
{
return bytes = new ByteArrayOutputStream();
}
/**
* Returns the bytes that make up this file object.
*
* @return a <code>byte[]</code> value
*/
private byte[] getBytes()
{
return bytes.toByteArray();
}
}
}