All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.bbottema.javareflection.util.ExternalClassLoader Maven / Gradle / Ivy

package org.bbottema.javareflection.util;

import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;

import java.io.DataInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

/**
 * A toolkit that can read and compile .java sourcefiles on the fly in runtime. This class loader caches loaded classes for
 * repeated requests. Also, if a .class file already exists for a specific .java sourcefile, the classloader will compare dates and see if
 * the .java needs recompiling. 
*
* A basepath can be specified for the classloader to look in for sourcefiles. */ public final class ExternalClassLoader extends ClassLoader { /** * base path used to locate [packaged] classes in */ @Nullable private String basepath; /** * List of classes. These are non-instances of a class, of which instances can be spawn. */ @NotNull private final Map> classes = new HashMap<>(); /** * The exception that was thrown when the class was actually found but some other error occurred. */ @Nullable @SuppressWarnings("FieldCanBeLocal") private CompileException exception; /** * Constructor which initializes all properties. */ public ExternalClassLoader() { super(ExternalClassLoader.class.getClassLoader()); } /** * Loads a class from the classes cache if available. Delegates to {@link #findClass(String)} otherwise. * * @see ClassLoader#loadClass(String) */ @Override @Nullable public final Class loadClass(final String className) throws ClassNotFoundException { final Class c = classes.get(className); return (c != null) ? c : findClass(className); } /** * Loads a classfile from file in the following order: *
    *
  1. looks for the java source and if available, checks whether it needs to be compiled
  2. *
  3. if no .java or .class file found, try to find it in the VM
  4. *
* If the class ultimately wasn't found a ClassNotFoundException is being thrown. If class was found, but an error * occurred, store exception for later review and return null. This is done so because the overridden method can't throw an * exception other than ClassNotFoundException. * * @param className The path and name to the classfile. * @return The requested class reference. * @throws ClassNotFoundException Thrown by {@link #findSystemClass(String)}. */ @SuppressWarnings("WeakerAccess") @Override @Nullable public final Class findClass(final String className) throws ClassNotFoundException { exception = null; try { // try to load, cache and return the class from compiled .class file checkForFile(basepath, className); // if a classfile is available then load it final Class c = classes.get(className); if (c != null) { return c; } else { throw new ClassNotFoundException(); } } catch (final ClassNotFoundException e) { // - see if class can be found in the VM // - this check must come last, else you won't detect whether a classfile has been recompiled final Class c = findSystemClass(className); classes.put(className, c); return c; } catch (final IOException e) { exception = new CompileException(e.getMessage(), e); return null; } catch (final CompileException e) { exception = e; return null; } } /** * Reads and compiles the sourcefile from filesystem and creates the class inside the VM. * * @param className The path and name to the .java sourcefile. */ private void checkForFile(final String classPath, final String className) throws IOException { // figure paths... final String resource = className.replace("..", "||").replace('.', File.separatorChar).replace("||", ".."); final File javaSource = new File(classPath + File.separatorChar + resource + ".java"); final File javaClass = new File(classPath + File.separatorChar + resource + ".class"); final String absoluteClassPath = javaClass.getAbsolutePath(); // see if there is a javafile and classfile if (javaSource.exists()) { // if classfile available, delete if outdated if (javaClass.exists()) { // determine if the java sourcefile has been modified since last compile // else check if the .class file has already been loaded if (javaSource.lastModified() > javaClass.lastModified()) { if (!javaClass.delete()) { throw new CompileException("runtime compiler: unable to removed outdated .class file"); } classes.remove(className); } else if (classes.get(className) == null) { classes.put(className, loadClass(absoluteClassPath, className)); } } // if no classfile available or became outdated if (!javaClass.exists()) { final int status = 0;// Main.compile(args); // load compiled classfile and cache it or return error that occured during compiling switch (status) { case 0: classes.put(className, loadClass(absoluteClassPath, className)); break; case 1: throw new CompileException("runtime compiler: ERROR"); case 2: throw new CompileException("runtime compiler: CMDERR"); case 3: throw new CompileException("runtime compiler: SYSERR"); case 4: throw new CompileException("runtime compiler: ABNORMAL"); default: throw new CompileException("Compile status: Unknown exit status"); } } } } /** * Reads the classfile from filesystem. * * @param classPath The path and name to the classfile. * @param className The name of the class to use as key for the Class reference value. */ private Class loadClass(final String classPath, final String className) throws IOException { // get data from file final File f = new File(classPath); final int size = (int) f.length(); final byte buff[] = new byte[size]; final DataInputStream dis = new DataInputStream(new FileInputStream(f)); dis.readFully(buff); dis.close(); // convert data into class return defineClass(className, buff, 0, buff.length, null); } /** * Parameterized exception used when Java's runtime compiler fails to compile a Java source file. */ public static class CompileException extends RuntimeException { private static final long serialVersionUID = -7210219718456902667L; /** * @param reason The description of the cause of the exception. */ CompileException(final String reason) { super(reason); } /** * Used to create an exception with a copies stacktrace (using {@link #setStackTrace(StackTraceElement[])}). * * @param reason The description of the cause of the exception. * @param cause A thrown exception that is the cause. */ CompileException(final String reason, final Throwable cause) { super(reason + "\n " + cause.toString()); setStackTrace(cause.getStackTrace()); } } /** * Sets the base path this classloader will look for classes in. * * @param basepath A folder path to look for classes. */ public void setBasepath(@Nullable final String basepath) { this.basepath = basepath; } @Nullable public String getBasepath() { return basepath; } @Nullable public CompileException getException() { return exception; } }




© 2015 - 2024 Weber Informatics LLC | Privacy Policy