/* * Copyright (C) 2015 Red Hat, Inc. and/or its affiliates. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package org.jboss.errai.codegen.util; import static org.slf4j.LoggerFactory.getLogger; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.PrintWriter; import java.net.URL; import java.util.ArrayList; import java.util.Comparator; import java.util.Enumeration; import java.util.LinkedHashSet; import java.util.List; import java.util.Optional; import java.util.Set; import java.util.jar.JarFile; import java.util.regex.Pattern; import javax.tools.JavaCompiler; import javax.tools.ToolProvider; import org.eclipse.jdt.core.compiler.CompilationProgress; import org.eclipse.jdt.core.compiler.batch.BatchCompiler; import org.jboss.errai.common.metadata.MetaDataScanner; import org.jboss.errai.common.metadata.RebindUtils; import org.slf4j.Logger; /** * @author Mike Brock */ public class ClassChangeUtil { private static final String USE_NATIVE_JAVA_COMPILER = "errai.marshalling.use_native_javac"; private static final String CLASSLOADING_MODE_PROPERTY = "errai.marshalling.classloading.mode"; private static final String classLoadingMode; private static final boolean useNativeJavac = Boolean.getBoolean(USE_NATIVE_JAVA_COMPILER); private static Logger log = getLogger(ClassChangeUtil.class); static { if (System.getProperty(CLASSLOADING_MODE_PROPERTY) != null) { classLoadingMode = System.getProperty(CLASSLOADING_MODE_PROPERTY); } else { classLoadingMode = "thread"; } } private static interface CompilerAdapter { int compile(OutputStream out, OutputStream errors, String outputPath, String toCompile, String classpath); } public static class JDKCompiler implements CompilerAdapter { final JavaCompiler compiler; public JDKCompiler(final JavaCompiler compiler) { this.compiler = compiler; } @Override public int compile(final OutputStream out, final OutputStream errors, final String outputPath, final String toCompile, final String classpath) { return compiler.run(null, out, errors, "-classpath", classpath, "-d", outputPath, toCompile); } } public static class JDTCompiler implements CompilerAdapter { @Override public int compile(final OutputStream out, final OutputStream errors, final String outputPath, final String toCompile, final String classpath) { return BatchCompiler.compile(new String[] { "-classpath", classpath, "-d", outputPath, "-source", "1.6", toCompile }, new PrintWriter(out), new PrintWriter(errors), new CompilationProgress() { @Override public void begin(final int remainingWork) { } @Override public void done() { } @Override public boolean isCanceled() { return false; } @Override public void setTaskName(final String name) { } @Override public void worked(final int workIncrement, final int remainingWork) { } }) ? 0 : -1; } } public static Class<?> compileAndLoad(final File sourceFile, final String fullyQualifiedName) throws IOException { final String packageName = getPackageFromFQCN(fullyQualifiedName); final String className = getNameFromFQCN(fullyQualifiedName); return compileAndLoad(sourceFile, packageName, className); } public static Class<?> compileAndLoad(final File sourceFile, final String packageName, final String className) throws IOException { return compileAndLoad(sourceFile.getParentFile().getAbsolutePath(), packageName, className); } public static Class<?> compileAndLoad(final String sourcePath, final String packageName, final String className) throws IOException { final String tempDirectory = RebindUtils.getTempDirectory(); return compileAndLoad(sourcePath, packageName, className, tempDirectory); } public static Class<?> compileAndLoad(final String sourcePath, final String packageName, final String className, final String outputPath) throws IOException { final String outputLocation = compileClass(sourcePath, packageName, className, outputPath); return loadClassDefinition(outputLocation, packageName, className); } public static String compileClass(final String sourcePath, final String packageName, final String className, final String outputPath) { try { final ByteArrayOutputStream errorOutputStream = new ByteArrayOutputStream(); final JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); final CompilerAdapter adapter; if (compiler == null || !useNativeJavac) { adapter = new JDTCompiler(); } else { adapter = new JDKCompiler(compiler); } final File classOutputDir = new File(outputPath + File.separatorChar + RebindUtils.packageNameToDirName(packageName) + File.separatorChar).getAbsoluteFile(); // delete any marshaller classes already there final Pattern matcher = Pattern.compile("^" + className + "(\\.|$).*class$"); if (classOutputDir.exists()) { final File[] files = classOutputDir.listFiles(); if (files != null) { for (final File file : files) { if (matcher.matcher(file.getName()).matches()) { file.delete(); } } } } final StringBuilder sb = new StringBuilder(4096); final List<URL> configUrls = MetaDataScanner.getConfigUrls(); final List<File> classpathElements = new ArrayList<File>(configUrls.size()); classpathElements.add(new File(outputPath)); log.debug(">>> Searching for all jars using " + MetaDataScanner.ERRAI_CONFIG_STUB_NAME); for (final URL url : configUrls) { final File file = getFileIfExists(url.getFile()); if (file != null) { classpathElements.add(file); } } log.debug("<<< Done searching for all jars using " + MetaDataScanner.ERRAI_CONFIG_STUB_NAME); for (final File file : classpathElements) { sb.append(file.getAbsolutePath()).append(File.pathSeparator); } sb.append(System.getProperty("java.class.path")); sb.append(findAllJarsByManifest()); final String classPath = sb.toString(); /** * Attempt to run the compiler without any classpath specified. */ if (adapter.compile(System.out, errorOutputStream, outputPath, new File(sourcePath + File.separator + className + ".java").getAbsolutePath(), classPath) != 0) { System.err.println("*** FAILED TO COMPILE CLASS ***"); System.err.println("*** Classpath Used: " + classPath); for (final byte b : errorOutputStream.toByteArray()) { System.err.print((char) b); } return null; } return new File(classOutputDir.getAbsolutePath() + File.separatorChar + className + ".class").getAbsolutePath(); } catch (final Exception e) { throw new RuntimeException(e); } } public static Class<?> loadClassDefinition(final String path, final String packageName, final String className) throws IOException { if (path == null) return null; FileInputStream inputStream = new FileInputStream(path); byte[] classDefinition = new byte[inputStream.available()]; final String classBase = path.substring(0, path.length() - ".class".length()); final BootstrapClassloader clsLoader = new BootstrapClassloader(new File(path).getParentFile().getAbsolutePath(), "system".equals(classLoadingMode) ? ClassLoader.getSystemClassLoader() : Thread.currentThread().getContextClassLoader()); final String fqcn; if ("".equals(packageName)) { fqcn = className; } else { fqcn = packageName + "." + className; } boolean success = false; try { final Class<?> loadClass = clsLoader.loadClass(fqcn); success = true; return loadClass; } catch (final Throwable t) { // fall through } finally { if (success) { try { inputStream.close(); } catch (final Throwable ignore) { } } } inputStream.read(classDefinition); final File[] files = new File(path).getParentFile().listFiles(); if (files != null) { for (final File file : files) { if (file.getName().startsWith(className + "$")) { String s = file.getName(); s = s.substring(s.indexOf('$') + 1, s.lastIndexOf('.')); final String nestedClassName = fqcn + "$" + s; Class<?> cls = null; try { cls = clsLoader.loadClass(nestedClassName); } catch (final ClassNotFoundException ignored) { } if (cls != null) continue; final String innerClassBaseName = classBase + "$" + s; final File innerClass = new File(innerClassBaseName + ".class"); if (innerClass.exists()) { try { inputStream = new FileInputStream(innerClass); classDefinition = new byte[inputStream.available()]; inputStream.read(classDefinition); clsLoader.defineClassX(nestedClassName, classDefinition, 0, classDefinition.length); } finally { inputStream.close(); } } else { break; } } } } final Class<?> mainClass = clsLoader .defineClassX(fqcn, classDefinition, 0, classDefinition.length); inputStream.close(); for (int i = 1; i < Integer.MAX_VALUE; i++) { final String nestedClassName = fqcn + "$" + i; Class<?> cls = null; try { cls = clsLoader.loadClass(nestedClassName); } catch (final ClassNotFoundException ignored) { } if (cls != null) continue; final String innerClassBaseName = classBase + "$" + i; final File innerClass = new File(innerClassBaseName + ".class"); if (innerClass.exists()) { try { inputStream = new FileInputStream(innerClass); classDefinition = new byte[inputStream.available()]; inputStream.read(classDefinition); clsLoader.defineClassX(nestedClassName, classDefinition, 0, classDefinition.length); } finally { inputStream.close(); } } else { break; } } return mainClass; } private static class BootstrapClassloader extends ClassLoader { private final String searchPath; private BootstrapClassloader(final String searchPath, final ClassLoader classLoader) { super(classLoader); this.searchPath = searchPath; } public Class<?> defineClassX(final String className, final byte[] b, final int off, final int len) { return super.defineClass(className, b, off, len); } @Override protected Class<?> findClass(final String name) throws ClassNotFoundException { try { return super.findClass(name); } catch (final ClassNotFoundException e) { try { FileInputStream inputStream = null; final byte[] classDefinition; final File innerClass = new File(searchPath + "/" + name.substring(name.lastIndexOf('.') + 1) + ".class"); if (innerClass.exists()) { try { inputStream = new FileInputStream(innerClass); classDefinition = new byte[inputStream.available()]; inputStream.read(classDefinition); return defineClassX(name, classDefinition, 0, classDefinition.length); } finally { if (inputStream != null) inputStream.close(); } } } catch (final IOException e2) { throw new RuntimeException("failed to load class: " + name, e2); } } throw new ClassNotFoundException(name); } } @SuppressWarnings("rawtypes") private static String findAllJarsByManifest() { final StringBuilder cp = new StringBuilder(); try { log.debug(">>> Searching for all jars using " + JarFile.MANIFEST_NAME); final Enumeration[] enumerations = new Enumeration[] { Thread.currentThread().getContextClassLoader().getResources(JarFile.MANIFEST_NAME), ClassLoader.getSystemClassLoader().getResources(JarFile.MANIFEST_NAME) }; for (final Enumeration resEnum : enumerations) { while (resEnum.hasMoreElements()) { try { String path = ((URL) resEnum.nextElement()).getFile(); path = path.substring(0, path.length() - JarFile.MANIFEST_NAME.length() - 1); final File file = getFileIfExists(path); if (file != null) { cp.append(File.pathSeparator).append(file.getAbsolutePath()); } } catch (final Exception e) { log.warn("Ignoring classpath entry with invalid manifest", e); } } } } catch (final IOException e1) { log.warn("Failed to build classpath using manifest discovery. Expect compilation failures...", e1); } finally { log.debug("<<< Done searching for all jars using " + JarFile.MANIFEST_NAME); } return cp.toString(); } public static File getFileIfExists(String path) { final String originalPath = path; if (path.startsWith("file:")) { path = path.substring(5); final int outerElement = path.indexOf('!'); if (outerElement != -1) { path = path.substring(0, outerElement); } } else if (path.startsWith("jar:")) { path = path.substring(4); final int outerElement = path.indexOf('!'); if (outerElement != -1) { path = path.substring(0, outerElement); } } final File file = new File(path); if (file.exists()) { if (log.isDebugEnabled()) { log.debug(" EXISTS: " + originalPath + " -> " + file.getAbsolutePath()); } return file; } if (log.isDebugEnabled()) { log.debug(" !EXISTS: " + originalPath + " -> " + file.getAbsolutePath()); } return null; } private static String getPackageFromFQCN(final String fqcn) { final int index = fqcn.lastIndexOf('.'); if (index == -1) { return ""; } else { return fqcn.substring(0, index); } } private static String getNameFromFQCN(final String fqcn) { final int index = fqcn.lastIndexOf('.'); if (index == -1) { return fqcn; } else { return fqcn.substring(index + 1); } } private static List<String> urlToFile(Enumeration<URL> urls) { final ArrayList<String> files = new ArrayList<String>(); while (urls.hasMoreElements()) { final URL url = urls.nextElement(); if (url.getProtocol().equals("file")) { files.add(url.getFile()); } } return files; } /** * Finds all urls of classes that are not in jars. */ private static Set<String> getClassLocations(final String packageName, final String simpleClassName) throws IOException { final String classResource = packageName.replaceAll("\\.", "/") + "/" + simpleClassName + ".class"; final Set<String> locations = new LinkedHashSet<String>(); // look for the class in every classloader we can think of. For example, current thread // classloading works in Jetty but not JBoss AS 7. locations.addAll(urlToFile(Thread.currentThread().getContextClassLoader().getResources(classResource))); locations.addAll(urlToFile(ClassChangeUtil.class.getClassLoader().getResources(classResource))); locations.addAll(urlToFile(ClassLoader.getSystemResources(classResource))); return locations; } public static Optional<File> getNewest(final Set<String> locations) { return locations.stream() .map(url -> getFileIfExists(url)) .filter(f -> f != null) .max(Comparator.comparingLong(f -> f.lastModified())); } public static Optional<Class<?>> loadClassIfPresent(final String packageName, final String simpleClassName) { final String fullyQualifiedClassName = packageName + "." + simpleClassName; try { log.info("Searching for class: {}", fullyQualifiedClassName); final Set<String> locations = getClassLocations(packageName, simpleClassName); final Optional<File> newest = getNewest(locations); if (locations.size() > 1) { log.warn("*** MULTIPLE VERSIONS OF " + fullyQualifiedClassName + " FOUND IN CLASSPATH: " + "Attempted to guess the newest one based on file dates. But you should clean your output directories."); locations.stream().forEach(loc -> log.warn(" Ambiguous version -> {}", loc)); } if (newest.isPresent()) { log.info("Loading class {} found at {}", fullyQualifiedClassName, newest.get().getAbsolutePath().toString()); return Optional.of(loadClassDefinition(newest.get().getAbsolutePath(), packageName, simpleClassName)); } else { log.info("Could not find URL for {}. Attempting to load with context class loader.", fullyQualifiedClassName); try { // maybe we're in an appserver with a VFS, so try to load anyways. final Class<?> loadedClass = Thread.currentThread().getContextClassLoader().loadClass(fullyQualifiedClassName); log.info("Successfully loaded {} with context class loader.", fullyQualifiedClassName); return Optional.of(loadedClass); } catch (final ClassNotFoundException e) { log.warn("Could not load {} class.", fullyQualifiedClassName); return Optional.empty(); } } } catch (final IOException e) { log.warn("Could not read {} class: " + fullyQualifiedClassName, e); return Optional.empty(); } } public static String generateClassFile(final String packageName, final String simpleClassName, final String sourceDir, final String source, final String outputPath) { final File outputDir = new File(sourceDir + File.separator + RebindUtils.packageNameToDirName(packageName) + File.separator); final File classOutputPath = new File(outputPath); //noinspection ResultOfMethodCallIgnored outputDir.mkdirs(); final File sourceFile = new File(outputDir.getAbsolutePath() + File.separator + simpleClassName + ".java"); RebindUtils.writeStringToFile(sourceFile, source); return compileClass(outputDir.getAbsolutePath(), packageName, simpleClassName, classOutputPath.getAbsolutePath()); } public static Class<?> compileAndLoadFromSource(final String packageName, final String simpleClassName, final String source) { log.info("Compiling and loading {}.{} from source...", packageName, simpleClassName); final File directory = new File(RebindUtils.getTempDirectory() + "/errai.gen/classes/" + packageName.replaceAll("\\.", "/")); final File sourceFile = new File(directory.getAbsolutePath() + File.separator + simpleClassName + ".java"); log.info("Using temporary directory for source and class files: {}", directory.getAbsolutePath()); try { if (directory.exists()) { log.info("Directory {} already exists. Deleting directory and contents (enable debug logging to see deleted files).", directory.getAbsolutePath()); final File[] files = directory.listFiles(); if (files != null) { for (final File file : files) { log.debug("Deleting {}", file.getAbsolutePath()); file.delete(); } } log.debug("Deleting {}", directory.getAbsolutePath()); directory.delete(); } directory.mkdirs(); log.info("Writing source file {}...", sourceFile.getAbsolutePath()); final FileOutputStream outputStream = new FileOutputStream(sourceFile); outputStream.write(source.getBytes("UTF-8")); outputStream.flush(); outputStream.close(); log.info("Compiling {}.{} in source file {}...", packageName, simpleClassName, sourceFile.getAbsolutePath()); final String compiledClassPath = compileClass(directory.getAbsolutePath(), packageName, simpleClassName, directory.getAbsolutePath()); if (compiledClassPath == null) { log.warn("Could not compile {}.{} in source file {}...", packageName, simpleClassName, sourceFile.getAbsolutePath()); return null; } else { log.info("Loading compiled class at {}...", compiledClassPath); return loadClassDefinition(compiledClassPath, packageName, simpleClassName); } } catch (final IOException e) { throw new RuntimeException("failed to generate class ", e); } } }