/** * 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>>(); } }