/* * Copyright (c) 1998-2011 Caucho Technology -- all rights reserved * * This file is part of Resin(R) Open Source * * Each copy or derived work must preserve the copyright notice and this * notice unmodified. * * Resin Open Source is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 2 of the License, or * (at your option) any later version. * * Resin Open Source 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, or any warranty * of NON-INFRINGEMENT. See the GNU General Public License for more * details. * * You should have received a copy of the GNU General Public License * along with Resin Open Source; if not, write to the * * Free Software Foundation, Inc. * 59 Temple Place, Suite 330 * Boston, MA 02111-1307 USA * * @author Scott Ferguson */ package com.caucho.loader; import com.caucho.config.ConfigException; import com.caucho.java.CompileClassNotFound; import com.caucho.java.JavaCompiler; import com.caucho.make.AlwaysModified; import com.caucho.make.Make; import com.caucho.server.util.CauchoSystem; import com.caucho.util.Alarm; import com.caucho.util.CharBuffer; import com.caucho.util.L10N; import com.caucho.vfs.Depend; import com.caucho.vfs.Path; import javax.annotation.PostConstruct; import java.io.IOException; import java.net.URL; import java.security.CodeSource; import java.security.cert.Certificate; import java.util.ArrayList; import java.util.HashSet; import java.util.logging.Level; import java.util.logging.Logger; /** * A class loader that automatically compiles Java. */ public class CompilingLoader extends Loader implements Make { private static final Logger log = Logger.getLogger(CompilingLoader.class.getName()); private static final L10N L = new L10N(CompilingLoader.class); private static final char []INNER_CLASS_SEPARATORS = new char[] {'$', '+', '-'}; // classpath private String _classPath; private String _compiler; private String _sourceExt = ".java"; // source directory private Path _sourceDir; // directory where classes are stored private Path _classDir; private CodeSource _codeSource; private ArrayList<String> _args; private String _encoding; private boolean _requireSource; private HashSet<String> _excludedDirectories = new HashSet<String>(); private long _lastMakeTime; private boolean _isBatch = true; public CompilingLoader() { this(Thread.currentThread().getContextClassLoader()); } public CompilingLoader(ClassLoader loader) { super(loader); _excludedDirectories.add("CVS"); _excludedDirectories.add(".svn"); } /** * Creates a new compiling class loader * * @param classDir generated class directory root */ public CompilingLoader(ClassLoader loader, Path classDir) { this(loader, classDir, classDir, null, null); } /** * Creates a new compiling class loader * * @param classDir generated class directory root * @param sourceDir Java source directory root * @param args Javac arguments * @param encoding javac encoding */ public CompilingLoader(ClassLoader loader, Path classDir, Path sourceDir, String args, String encoding) { this(loader); if (classDir.getScheme().equals("http") || classDir.getScheme().equals("https")) { throw new ConfigException(L.l("compiling class loader can't be '{0}'. Use compile=false.", classDir)); } _sourceDir = sourceDir; _classDir = classDir; _encoding = encoding; // loader.addCodeBasePath(classDir.getFullPath()); /* try { if (args != null) _args = new Regexp("[\\s,]+").split(args); } catch (Exception e) { log.log(Level.FINER, e.toString(), e); } */ } /** * Create a class loader based on the compiling loader * * @param path traditional classpath * * @return the new ClassLoader */ public static DynamicClassLoader create(Path path) { DynamicClassLoader loader = new DynamicClassLoader(null); CompilingLoader compilingLoader = new CompilingLoader(loader, path); compilingLoader.init(); loader.init(); return loader; } /** * Sets the class path. */ public void setPath(Path path) { _classDir = path; if (_sourceDir == null) _sourceDir = path; } /** * Gets the class path. */ public Path getPath() { return _classDir; } /** * Sets the source path. */ public void setSource(Path path) { _sourceDir = path; } /** * Sets the source extension. */ public void setSourceExtension(String ext) throws ConfigException { if (! ext.startsWith(".")) throw new ConfigException(L.l("source-extension '{0}' must begin with '.'", ext)); _sourceExt = ext; } /** * Sets the compiler. */ public void setCompiler(String compiler) throws ConfigException { _compiler = compiler; } /** * Sets the source path. */ public Path getSource() { return _sourceDir; } /** * Sets the arguments. */ public void setArgs(String arg) { int i = 0; int len = arg.length(); CharBuffer cb = new CharBuffer(); while (i < len) { char ch; for (; i < len && Character.isWhitespace(ch = arg.charAt(i)); i++) { } if (len <= i) return; cb.clear(); for (; i < len && ! Character.isWhitespace(ch = arg.charAt(i)); i++) cb.append(ch); addArg(cb.toString()); } } /** * Adds an argument. */ public void addArg(String arg) { if (_args == null) _args = new ArrayList<String>(); _args.add(arg); } /** * Sets the encoding. */ public void setEncoding(String encoding) { _encoding = encoding; } /** * Sets true if source is required. */ public void setRequireSource(boolean requireSource) { _requireSource = requireSource; } /** * Sets true if compilation should batch as many files as possible. */ public void setBatch(boolean isBatch) { _isBatch = isBatch; } @Override public boolean isDirectoryLoader() { return true; } /** * Initialize. */ @PostConstruct @Override public void init() throws ConfigException { if (_classDir == null) throw new ConfigException(L.l("'path' is a required attribute of <compiling-loader>.")); String scheme = _classDir.getScheme(); if (scheme != null && ! scheme.equals("memory") && ! scheme.equals("error")) { try { _classDir.mkdirs(); } catch (IOException e) { log.log(Level.FINE, e.toString(), e); } try { _codeSource = new CodeSource(new URL(_classDir.getURL()), (Certificate []) null); } catch (Exception e) { log.log(Level.FINE, e.toString(), e); } } super.init(); getClassLoader().addURL(_classDir); } /** * Creates a new compiling class loader * * @param classDir generated class directory root * @param sourceDir Java source directory root * @param args Javac arguments * @param encoding javac encoding */ public static DynamicClassLoader create(ClassLoader parent, Path classDir, Path sourceDir, String args, String encoding) { DynamicClassLoader loader = new DynamicClassLoader(parent); loader.addLoader(new CompilingLoader(loader, classDir, sourceDir, args, encoding)); loader.init(); return loader; } public String getClassPath() { if (_classPath == null) _classPath = getClassLoader().getClassPath(); return _classPath; } /** * Compiles all changed files in the class directory. */ @Override public void make() throws IOException, ClassNotFoundException { synchronized (this) { if (Alarm.getCurrentTime() < _lastMakeTime + 2000) return; makeImpl(); _lastMakeTime = Alarm.getCurrentTime(); } } private void makeImpl() throws IOException, ClassNotFoundException { if (_sourceDir.isDirectory() && ! _classDir.isDirectory()) _classDir.mkdirs(); String sourcePath = prefixClassPath(getClassPath()); ArrayList<String> files = new ArrayList<String>(); findAllModifiedClasses("", _sourceDir, _classDir, sourcePath, files); if (files.size() == 0) return; if (_isBatch) { String []paths = files.toArray(new String[files.size()]); compileBatch(paths, true); } else { while (files.size() > 0) { String path = files.remove(0); String []paths = new String[] { path }; compileBatch(paths, true); } } } /** * Returns the classes which need compilation. */ private void findAllModifiedClasses(String name, Path sourceDir, Path classDir, String sourcePath, ArrayList<String> sources) throws IOException, ClassNotFoundException { String []list; try { list = sourceDir.list(); } catch (IOException e) { return; } for (int i = 0; list != null && i < list.length; i++) { if (list[i].startsWith(".")) continue; if (_excludedDirectories.contains(list[i])) continue; Path subSource = sourceDir.lookup(list[i]); if (subSource.isDirectory()) { findAllModifiedClasses(name + list[i] + "/", subSource, classDir.lookup(list[i]), sourcePath, sources); } else if (list[i].endsWith(_sourceExt)) { int tail = list[i].length() - _sourceExt.length(); String prefix = list[i].substring(0, tail); Path subClass = classDir.lookup(prefix + ".class"); if (subClass.getLastModified() < subSource.getLastModified()) { sources.add(name + list[i]); } } } if (! _requireSource) return; try { list = classDir.list(); } catch (IOException e) { return; } for (int i = 0; list != null && i < list.length; i++) { if (list[i].startsWith(".")) continue; if (_excludedDirectories.contains(list[i])) continue; Path subClass = classDir.lookup(list[i]); if (list[i].endsWith(".class")) { String prefix = list[i].substring(0, list[i].length() - 6); Path subSource = sourceDir.lookup(prefix + _sourceExt); if (! subSource.exists()) { String tail = subSource.getTail(); boolean doRemove = true; if (tail.indexOf('$') > 0) { String subTail = tail.substring(0, tail.indexOf('$')) + _sourceExt; Path subJava = subSource.getParent().lookup(subTail); if (subJava.exists()) doRemove = false; } if (doRemove) { log.finer(L.l("removing obsolete class '{0}'.", subClass.getPath())); subClass.remove(); } } } } } /** * Loads the specified class, compiling if necessary. */ @Override protected ClassEntry getClassEntry(String name, String pathName) throws ClassNotFoundException { Path classFile = _classDir.lookup(pathName); /* Path classDir = classFile.getParent(); if (! classDir.isDirectory()) return null; */ String javaName = name.replace('.', '/') + _sourceExt; Path javaFile = _sourceDir.lookup(javaName); for (int i = 0; i < INNER_CLASS_SEPARATORS.length; i++) { char sep = INNER_CLASS_SEPARATORS[i]; if (name.indexOf(sep) > 0) { String subName = name.substring(0, name.indexOf(sep)); String subJavaName = subName.replace('.', '/') + _sourceExt; Path subJava = _sourceDir.lookup(subJavaName); if (subJava.exists()) { javaFile = subJava; } } } synchronized (this) { if (_requireSource && ! javaFile.exists()) { boolean doRemove = true; if (doRemove) { log.finer(L.l("removing obsolete class `{0}'.", classFile.getPath())); try { classFile.remove(); } catch (IOException e) { log.log(Level.WARNING, e.toString(), e); } return null; } } if (! classFile.canRead() && ! javaFile.canRead()) return null; return new CompilingClassEntry(this, getClassLoader(), name, javaFile, classFile, getCodeSource(classFile)); } } /** * Returns the code source for the directory. */ protected CodeSource getCodeSource(Path path) { return _codeSource; } /** * Checks that the case is okay for the source. */ boolean checkSource(Path sourceDir, String javaName) { try { while (javaName != null && ! javaName.equals("")) { int p = javaName.indexOf('/'); String head; if (p >= 0) { head = javaName.substring(0, p); javaName = javaName.substring(p + 1); } else { head = javaName; javaName = null; } String []names = sourceDir.list(); int i; for (i = 0; i < names.length; i++) { if (names[i].equals(head)) break; } if (i == names.length) return false; sourceDir = sourceDir.lookup(head); } } catch (IOException e) { log.log(Level.FINE, e.toString(), e); return false; } return true; } /** * Compile the Java source. Compile errors are encapsulated in a * ClassNotFound wrapper. * * @param javaSource path to the Java source */ void compileClass(Path javaSource, Path javaClass, String sourcePath, boolean isMake) throws ClassNotFoundException { try { JavaCompiler compiler = JavaCompiler.create(getClassLoader()); compiler.setClassDir(_classDir); compiler.setSourceDir(_sourceDir); if (_encoding != null) compiler.setEncoding(_encoding); compiler.setArgs(_args); compiler.setCompileParent(! isMake); compiler.setSourceExtension(_sourceExt); if (_compiler != null) compiler.setCompiler(_compiler); //LineMap lineMap = new LineMap(javaFile.getNativePath()); // The context path is obvious from the browser url //lineMap.add(name.replace('.', '/') + _sourceExt, 1, 1); // Force this into a relative path so different compilers will work String prefix = _sourceDir.getPath(); String full = javaSource.getPath(); String source; if (full.startsWith(prefix)) { source = full.substring(prefix.length()); if (source.startsWith("/")) source = source.substring(1); } else source = javaSource.getPath(); /* if (javaSource.canRead() && javaClass.exists()) javaClass.remove(); */ compiler.compileIfModified(source, null); } catch (Exception e) { getClassLoader().addDependency(new Depend(javaSource)); log.log(Level.FINEST, e.toString(), e); // Compile errors are wrapped in a special ClassNotFound class // so the server can give a nice error message throw new CompileClassNotFound(e); } } /** * Compile the Java source. Compile errors are encapsulated in a * ClassNotFound wrapper. */ void compileBatch(String []files, boolean isMake) throws ClassNotFoundException { try { JavaCompiler compiler = JavaCompiler.create(getClassLoader()); compiler.setClassDir(_classDir); compiler.setSourceDir(_sourceDir); if (_encoding != null) compiler.setEncoding(_encoding); compiler.setArgs(_args); compiler.setCompileParent(! isMake); compiler.setSourceExtension(_sourceExt); if (_compiler != null) compiler.setCompiler(_compiler); //LineMap lineMap = new LineMap(javaFile.getNativePath()); // The context path is obvious from the browser url //lineMap.add(name.replace('.', '/') + _sourceExt, 1, 1); compiler.compileBatch(files); } catch (Exception e) { getClassLoader().addDependency(AlwaysModified.create()); // Compile errors are wrapped in a special ClassNotFound class // so the server can give a nice error message throw new CompileClassNotFound(e); } } /** * Returns the path for the given name. * * @param name the name of the class */ @Override public Path getPath(String name) { Path path = _classDir.lookup(name); if (path != null && path.exists()) return path; path = _sourceDir.lookup(name); if (path != null && path.exists()) return path; return null; } /** * Adds the classpath we're responsible for to the classpath * * @param head the overriding classpath * @return the new classpath */ @Override protected void buildClassPath(ArrayList<String> pathList) { if (! _classDir.getScheme().equals("file")) return; try { if (! _classDir.isDirectory() && _sourceDir.isDirectory()) { try { _classDir.mkdirs(); } catch (IOException e) { } } if (_classDir.isDirectory()) { String path = _classDir.getNativePath(); if (! pathList.contains(path)) pathList.add(path); } if (! _classDir.equals(_sourceDir)) { String path = _sourceDir.getNativePath(); if (! pathList.contains(path)) pathList.add(path); } } catch (java.security.AccessControlException e) { log.log(Level.WARNING, e.toString(), e); } } protected String prefixClassPath(String tail) { CharBuffer cb = new CharBuffer(); if (! _classDir.isDirectory() && _sourceDir.isDirectory()) { try { _classDir.mkdirs(); } catch (IOException e) { } } if (_classDir.isDirectory()) { if (cb.length() > 0) cb.append(CauchoSystem.getPathSeparatorChar()); cb.append(_classDir.getNativePath()); } if (! _classDir.equals(_sourceDir)) { if (cb.length() > 0) cb.append(CauchoSystem.getPathSeparatorChar()); cb.append(_sourceDir.getNativePath()); } if (cb.length() > 0) cb.append(CauchoSystem.getPathSeparatorChar()); cb.append(tail); return cb.close(); } /* @Override protected void buildSourcePath(StringBuilder head) { buildClassPath(head); } */ public String toString() { if (_classDir == null) return "CompilingLoader[]"; else if (_classDir.equals(_sourceDir)) return "CompilingLoader[src:" + _sourceDir + "]"; else return ("CompilingLoader[src:" + _sourceDir + ",class:" + _classDir + "]"); } }