/**
* Copyright (C) 2010 Orbeon, Inc.
*
* This program is free software; you can redistribute it and/or modify it under the terms of the
* GNU Lesser General Public License as published by the Free Software Foundation; either version
* 2.1 of the License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
* without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
* See the GNU Lesser General Public License for more details.
*
* The full text of the license is available at http://www.gnu.org/copyleft/lesser.html
*/
package org.orbeon.oxf.processor;
import org.apache.commons.lang3.StringUtils;
import org.apache.log4j.Logger;
import org.orbeon.dom.Document;
import org.orbeon.dom.Element;
import org.orbeon.oxf.cache.InternalCacheKey;
import org.orbeon.oxf.cache.ObjectCache;
import org.orbeon.oxf.cache.OutputCacheKey;
import org.orbeon.oxf.common.OXFException;
import org.orbeon.oxf.common.ValidationException;
import org.orbeon.oxf.externalcontext.ExternalContext;
import org.orbeon.oxf.pipeline.api.PipelineContext;
import org.orbeon.oxf.xml.XMLReceiver;
import org.orbeon.oxf.processor.impl.ProcessorInputImpl;
import org.orbeon.oxf.resources.ExpirationMap;
import org.orbeon.oxf.resources.ResourceManagerWrapper;
import org.orbeon.oxf.resources.URLFactory;
import org.orbeon.oxf.util.LoggerFactory;
import org.orbeon.oxf.util.StringBuilderWriter;
import org.orbeon.oxf.util.SystemUtils;
import org.orbeon.oxf.xml.dom4j.LocationData;
import java.io.*;
import java.lang.reflect.Method;
import java.net.*;
import java.util.*;
/**
* The Java processor dynamically creates processors by compiling Java files on the fly.
*/
public class JavaProcessor extends ProcessorImpl {
static private Logger logger = LoggerFactory.createLogger(JavaProcessor.class);
private final static String PATH_SEPARATOR = System.getProperty("path.separator");
private final static ExpirationMap lastModifiedMap = new ExpirationMap(1000);
public static final String JARPATH_PROPERTY = "jarpath";
public static final String CLASSPATH_PROPERTY = "classpath";
public static final String COMPILER_CLASS_PROPERTY = "compiler-class";
public static final String COMPILER_JAR_PROPERTY = "compiler-jar";
public static final String DEFAULT_COMPILER_MAIN = "com.sun.tools.javac.Main";
public static final String JAVA_CONFIG_NAMESPACE_URI = "http://www.orbeon.org/oxf/xml/java";
public JavaProcessor() {
addInputInfo(new ProcessorInputOutputInfo(INPUT_CONFIG, JAVA_CONFIG_NAMESPACE_URI));
}
public ProcessorOutput createOutput(final String name) {
final ProcessorOutput output = new ProcessorOutputImpl(JavaProcessor.this, name) {
public void readImpl(PipelineContext context, XMLReceiver xmlReceiver) {
getInput(context).getOutput().read(context, xmlReceiver);
}
@Override
public OutputCacheKey getKeyImpl(PipelineContext pipelineContext) {
return isInputInCache(pipelineContext, INPUT_CONFIG)
? getInputKey(pipelineContext, getInput(pipelineContext)) : null;
}
@Override
public Object getValidityImpl(PipelineContext pipelineContext) {
return isInputInCache(pipelineContext, INPUT_CONFIG)
? getInputValidity(pipelineContext, getInput(pipelineContext)) : null;
}
private ProcessorInput getInput(PipelineContext context) {
start(context);
State state = (State) getState(context);
return state.bottomInputs.get(name);
}
};
addOutput(name, output);
return output;
}
@Override
public void start(PipelineContext context) {
State state = (State) getState(context);
if (!state.started) {
Processor processor = JavaProcessor.this.getProcessor(context);
// Check user processor does not have a config input
for (Iterator i = processor.getInputsInfo().iterator(); i.hasNext();) {
final ProcessorInputOutputInfo inputInfo = (ProcessorInputOutputInfo) i.next();
if (inputInfo.getName().equals(INPUT_CONFIG))
throw new OXFException("Processor used by Java processor cannot have a config input");
}
Map<String, List<ProcessorInput>> inputMap = getConnectedInputs();
for (Iterator<String> i = inputMap.keySet().iterator(); i.hasNext();) {
String inputName = i.next();
List<ProcessorInput> inputsForName = inputMap.get(inputName);
for (Iterator<ProcessorInput> j = inputsForName.iterator(); j.hasNext();) {
final ProcessorInput javaProcessorInput = j.next();
// Skip our own config input
if (inputName.equals(INPUT_CONFIG))
continue;
// Delegate
ProcessorInput userProcessorInput = processor.createInput(inputName);
ProcessorOutput topOutput = new ProcessorOutputImpl(JavaProcessor.this, inputName) {
protected void readImpl(PipelineContext context, XMLReceiver xmlReceiver) {
javaProcessorInput.getOutput().read(context, xmlReceiver);
}
};
// Connect
userProcessorInput.setOutput(topOutput);
topOutput.setInput(userProcessorInput);
}
}
boolean hasOutputs = false;
Map<String, ProcessorOutput> outputMap = getConnectedOutputs();
// Connect processor outputs
for (Iterator<String> i = outputMap.keySet().iterator(); i.hasNext();) {
hasOutputs = true;
String outputName = i.next();
ProcessorOutput processorOutput = processor.createOutput(outputName);
ProcessorInput bottomInput = new ProcessorInputImpl(this, outputName);
processorOutput.setInput(bottomInput);
bottomInput.setOutput(processorOutput);
state.bottomInputs.put(outputName, bottomInput);
}
// Reset and start processor if required
processor.reset(context);
if (!hasOutputs) {
processor.start(context);
}
state.started = true;
}
}
private Processor getProcessor(PipelineContext context) {
try {
// Read config input into a String, cache if possible
ProcessorInput input = getInputByName(INPUT_CONFIG);
final Config config = readCacheInputAsObject(context, input, new CacheableInputReader<Config>() {
public Config read(PipelineContext context, ProcessorInput input) {
Document configDocument = readInputAsOrbeonDom(context, INPUT_CONFIG);
Element configElement = configDocument.getRootElement();
Config config = new Config();
config.clazz = configElement.attributeValue("class");
// Get source path
String sourcePathAttributeValue = configElement.attributeValue("sourcepath");
if (sourcePathAttributeValue == null)
sourcePathAttributeValue = ".";
File sourcePath = getFileFromURL(sourcePathAttributeValue, JavaProcessor.this.getLocationData());
if (!sourcePath.isDirectory())
throw new ValidationException("Invalid sourcepath attribute: cannot find directory for URL: " + sourcePathAttributeValue, (LocationData) configElement.getData());
try {
config.sourcepath = sourcePath.getCanonicalPath();
} catch (IOException e) {
throw new ValidationException("Invalid sourcepath attribute: cannot find directory for URL: " + sourcePathAttributeValue, (LocationData) configElement.getData());
}
return config;
}
});
// Check if need to compile
String sourceFile = config.sourcepath + "/" + config.clazz.replace('.', '/') + ".java";
String destinationDirectory = SystemUtils.getTemporaryDirectory().getAbsolutePath();
String destinationFile = destinationDirectory + "/" + config.clazz.replace('.', '/') + ".class";
// Check if file is up-to-date
long currentTimeMillis = System.currentTimeMillis();
Long sourceLastModified;
Long destinationLastModified;
synchronized (lastModifiedMap) {
sourceLastModified = (Long) lastModifiedMap.get(currentTimeMillis, sourceFile);
if (sourceLastModified == null) {
sourceLastModified = new Long(new File(sourceFile).lastModified());
lastModifiedMap.put(currentTimeMillis, sourceFile, sourceLastModified);
}
destinationLastModified = (Long) lastModifiedMap.get(currentTimeMillis, destinationFile);
if (destinationLastModified == null) {
destinationLastModified = new Long(new File(destinationFile).lastModified());
lastModifiedMap.put(currentTimeMillis, destinationFile, destinationLastModified);
}
}
boolean fileUpToDate = sourceLastModified.longValue() < destinationLastModified.longValue();
// Compile
if (!fileUpToDate) {
StringBuilderWriter javacOutput = new StringBuilderWriter();
final ArrayList<String> argLst = new ArrayList<String>();
final String[] cmdLine;
{
argLst.add("-g");
final String cp = buildClassPath(context);
if (cp != null) {
argLst.add("-classpath");
argLst.add(cp);
}
if (config.sourcepath != null && config.sourcepath.length() > 0) {
argLst.add("-sourcepath");
argLst.add(config.sourcepath);
}
argLst.add("-d");
final File tmp = SystemUtils.getTemporaryDirectory();
final String tmpPth = tmp.getAbsolutePath();
argLst.add(tmpPth);
final String fnam = config.sourcepath + "/"
+ config.clazz.replace('.', '/') + ".java";
argLst.add(fnam);
cmdLine = new String[argLst.size()];
argLst.toArray(cmdLine);
}
if (logger.isDebugEnabled()) {
logger.debug("Compiling class '" + config.clazz + "'");
logger.debug("javac " + argLst.toString());
}
Throwable thrown = null;
int exitCode = 1;
try {
// Get compiler class, either user-specified or default to Sun compiler
String compilerMain = getPropertySet().getString(COMPILER_CLASS_PROPERTY, DEFAULT_COMPILER_MAIN);
ClassLoader classLoader;
{
URI compilerJarURI = getPropertySet().getURI(COMPILER_JAR_PROPERTY);
if (compilerJarURI != null) {
// 1: Always honor user-specified compiler JAR if present
// Use special class loader pointing to this URL
classLoader = new URLClassLoader(new URL[]{compilerJarURI.toURL()}, JavaProcessor.class.getClassLoader());
if (logger.isDebugEnabled())
logger.debug("Java processor using user-specified compiler JAR: " + compilerJarURI.toString());
} else {
// 2: Try to use the class loader that loaded this class
classLoader = JavaProcessor.class.getClassLoader();
try {
Class.forName(compilerMain, true, classLoader);
logger.debug("Java processor using current class loader");
} catch (ClassNotFoundException e) {
// Class not found
// 3: Try to get to Sun tools.jar
String javaHome = System.getProperty("java.home");
if (javaHome != null) {
File javaHomeFile = new File(javaHome);
if (javaHomeFile.getName().equals("jre")) {
File toolsFile = new File(javaHomeFile.getParentFile(), "lib" + File.separator + "tools.jar");
if (toolsFile.exists()) {
// JAR file exists, will use it to load compiler class
classLoader = new URLClassLoader(new URL[]{toolsFile.toURI().toURL()}, JavaProcessor.class.getClassLoader());
if (logger.isDebugEnabled())
logger.debug("Java processor using default tools.jar under " + toolsFile.toString());
}
}
}
}
}
}
// Load compiler class using class loader defined above
Class compilerClass = Class.forName(compilerMain, true, classLoader);
// Get method and run compiler
Method compileMethod = compilerClass.getMethod("compile", new Class[]{String[].class, PrintWriter.class});
Object result = compileMethod.invoke(null, cmdLine, new PrintWriter(javacOutput));
exitCode = ((Integer) result).intValue();
} catch (final Throwable t) {
thrown = t;
}
if (exitCode != 0) {
String javacOutputString = "\n" + javacOutput.toString();
javacOutputString = StringUtils.replace(javacOutputString, "\n", "\n ");
throw new OXFException("Error compiling '" + argLst.toString() + "'" + javacOutputString, thrown);
}
}
// Try to get sourcepath info
InternalCacheKey sourcepathKey = new InternalCacheKey(JavaProcessor.this, "javaFile", config.sourcepath);
Object sourcepathValidity = new Long(0);
Sourcepath sourcepath = (Sourcepath) ObjectCache.instance()
.findValid(sourcepathKey, sourcepathValidity);
// Create classloader
if (sourcepath == null || (sourcepath.callNameToProcessorClass.containsKey(config.clazz) && !fileUpToDate)) {
if (logger.isDebugEnabled())
logger.debug("Creating classloader for sourcepath '" + config.sourcepath + "'");
sourcepath = new Sourcepath();
sourcepath.classLoader = new URLClassLoader
(new URL[]{SystemUtils.getTemporaryDirectory().toURI().toURL(), new File(config.sourcepath).toURI().toURL()},
this.getClass().getClassLoader());
ObjectCache.instance().add(sourcepathKey, sourcepathValidity, sourcepath);
}
// Get processor class
Class<Processor> processorClass = sourcepath.callNameToProcessorClass.get(config.clazz);
if (processorClass == null) {
processorClass = (Class<Processor>) sourcepath.classLoader.loadClass(config.clazz);
sourcepath.callNameToProcessorClass.put(config.clazz, processorClass);
}
// Create processor from class
Thread.currentThread().setContextClassLoader(processorClass.getClassLoader());
return processorClass.newInstance();
} catch (final IOException e) {
throw new OXFException(e);
} catch (final IllegalAccessException e) {
throw new OXFException(e);
} catch (final InstantiationException e) {
throw new OXFException(e);
} catch (final ClassNotFoundException e) {
throw new OXFException(e);
}
}
/**
* Return a File object based on an URL string that can be relative to the specified
* LocationData. The protocols supported are "oxf:" and "file:". If the file doesn't exist, an
* exception is thrown.
*/
public static File getFileFromURL(String urlString, LocationData locationData) {
URL sourcePathURL = (locationData != null && locationData.file() != null)
? URLFactory.createURL(locationData.file(), urlString)
: URLFactory.createURL(urlString);
// Make sure the protocol is oxf: or file:
if (sourcePathURL.getProtocol().equals("file")) {
String fileName = sourcePathURL.getFile();
File file = new File(fileName);
if (!file.exists()) {
// Try to decode only if we cannot find the file
try {
fileName = URLDecoder.decode(fileName, "utf-8");
} catch (UnsupportedEncodingException e) {
// Should not happen
throw new ValidationException(e, locationData);
}
file = new File(fileName);
if (!file.exists())
throw new ValidationException("Invalid sourcepath attribute: cannot find resource for URL: " + urlString, locationData);
}
return file;
} else if (sourcePathURL.getProtocol().equals("oxf")) {
// Get real path to source path
String path = sourcePathURL.getFile();
return new File(ResourceManagerWrapper.instance().getRealPath(path));
} else {
throw new ValidationException("Invalid sourcepath attribute: '" + urlString
+ "'. The Java processor only supports the oxf: and file: protocols for the sourcepath attribute.", locationData);
}
}
private String buildClassPath(PipelineContext context) throws UnsupportedEncodingException {
StringBuilder classpath = new StringBuilder();
StringBuilder jarpath = new StringBuilder();
String propJarpath = getPropertySet().getString(JARPATH_PROPERTY);
String propClasspath = getPropertySet().getString(CLASSPATH_PROPERTY);
// Add class path from properties if available
if (propClasspath != null)
classpath.append(propClasspath).append(PATH_SEPARATOR);
// Add JAR path from properties if available
if (propJarpath != null)
jarpath.append(propJarpath).append(PATH_SEPARATOR);
// Add JAR path and class path from webapp if available
ExternalContext externalContext = (ExternalContext) context.getAttribute(PipelineContext.EXTERNAL_CONTEXT);
boolean gotLibDir = false;
if (externalContext != null) {
String webInfLibPath = externalContext.getWebAppContext().getRealPath("WEB-INF/lib");
if (webInfLibPath != null) {
jarpath.append(webInfLibPath).append(PATH_SEPARATOR);
gotLibDir = true;
}
String webInfClasses = externalContext.getWebAppContext().getRealPath("WEB-INF/classes");
if (webInfClasses != null)
classpath.append(webInfClasses).append(PATH_SEPARATOR);
}
// Get class path based on class loader hierarchy
{
final String pathFromLoaders = SystemUtils.pathFromLoaders(JavaProcessor.class);
classpath.append(pathFromLoaders);
if (!pathFromLoaders.endsWith(File.pathSeparator))
classpath.append(File.pathSeparatorChar);
}
if (!gotLibDir) {
// WEB-INF/lib was not found, this SHOULD mean we are running from the command-line or
// embedded rather than in a regular web app
// Try to add directory containing current JAR file
String pathToCurrentJarDir = SystemUtils.getJarPath(getClass());
if (pathToCurrentJarDir != null) {
if (logger.isDebugEnabled())
logger.debug("Found current JAR directory: " + pathToCurrentJarDir);
jarpath.append(pathToCurrentJarDir).append(PATH_SEPARATOR);
}
}
for (StringTokenizer tokenizer = new StringTokenizer(jarpath.toString(), PATH_SEPARATOR); tokenizer.hasMoreElements();) {
String path = tokenizer.nextToken();
// Find jars in path
File[] jars = new File(path).listFiles(new FileFilter() {
public boolean accept(File pathname) {
String absolutePath = pathname.getAbsolutePath();
return absolutePath.endsWith(".jar") || absolutePath.endsWith(".zip");
}
});
// Add them to string buffer
if (jars != null) {
for (int i = 0; i < jars.length; i++)
classpath.append(jars[i].getAbsolutePath()).append(PATH_SEPARATOR);
}
}
if (logger.isDebugEnabled())
logger.debug("Classpath: " + classpath.toString());
return classpath.length() == 0 ? null : classpath.toString();
}
@Override
public void reset(PipelineContext context) {
setState(context, new State());
}
private static class Config {
public String sourcepath;
public String clazz;
}
private static class State {
public boolean started = false;
public Map<String, ProcessorInput> bottomInputs = new HashMap<String, ProcessorInput>();
}
private static class Sourcepath {
public ClassLoader classLoader;
public Map<String, Class<Processor>> callNameToProcessorClass = new HashMap<String, Class<Processor>>();
}
}