package org.webpieces.compiler.impl;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.instrument.ClassDefinition;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.AllPermission;
import java.security.CodeSource;
import java.security.Permissions;
import java.security.ProtectionDomain;
import java.security.cert.Certificate;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import org.webpieces.util.logging.Logger;
import org.webpieces.util.logging.LoggerFactory;
import org.webpieces.compiler.api.CompileConfig;
import org.webpieces.util.file.VirtualFile;
/*
* Compile classes that need compiling to load them
*/
public class CompilingClassloader extends ClassLoader implements ClassDefinitionLoader {
private static final Logger log = LoggerFactory.getLogger(CompilingClassloader.class);
/**
* This protection domain applies to all loaded classes.
*/
private final ProtectionDomain protectionDomain;
private final CompileConfig config;
private final BytecodeCache byteCodeCache;
private final CompilerWrapper compiler;
private final CompileMetaMgr appClassMgr;
private final FileStateHashCreator classStateHashCreator;
/**
* Used to track change of the application sources path
*/
private final int pathHash;
private FileLookup fileLookup;
private final int instance;
private static int lastUsedInstanceNumber = 0;
public CompilingClassloader(CompileConfig config, CompilerWrapper compiler, FileLookup fileLookup) {
super(CompilingClassloader.class.getClassLoader());
this.config = config;
this.byteCodeCache = new BytecodeCache(config);
this.compiler = compiler;
this.appClassMgr = compiler.getAppClassMgr();
this.fileLookup = fileLookup;
this.classStateHashCreator = new FileStateHashCreator(config);
VirtualFile pathForCodeSrc = config.getJavaPath().get(0);
// Clean the existing classes
for (CompileClassMeta applicationClass : appClassMgr.all()) {
applicationClass.uncompile();
}
this.pathHash = classStateHashCreator.computePathHash(config.getJavaPath());
try {
CodeSource codeSource = new CodeSource(new URL("file:" + pathForCodeSrc.getAbsolutePath()), (Certificate[]) null);
Permissions permissions = new Permissions();
permissions.add(new AllPermission());
protectionDomain = new ProtectionDomain(codeSource, permissions);
} catch (MalformedURLException e) {
throw new RuntimeException(e);
//throw new UnexpectedException(e);
}
synchronized(CompilingClassloader.class) {
this.instance = lastUsedInstanceNumber;
lastUsedInstanceNumber++;
}
}
/**
* You know ...
*/
@Override
protected synchronized Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
Class<?> c = findLoadedClass(name);
if (c != null) {
return c;
}
// First check if it's an application Class
Class<?> applicationClass = loadApplicationClass(name);
if (applicationClass != null) {
if (resolve) {
resolveClass(applicationClass);
}
return applicationClass;
}
// Delegate to the classic classloader
return super.loadClass(name, resolve);
}
// ~~~~~~~~~~~~~~~~~~~~~~~
public Class<?> loadApplicationClass(String name) {
//We must intern the name so it becomes the SAME EXACT lock in case the
//same name is passed in. ie. name1.intern() == name2.intern() so while
//you can use name.equals(name2), you could also do name1.intern() == name2.intern() as
//they intern returns the same object
synchronized (name.intern()) {
return loadApplicationClassImpl(name);
}
}
public Class<?> loadApplicationClassImpl(String name) {
Class<?> maybeAlreadyLoaded = super.findLoadedClass(name);
if(maybeAlreadyLoaded != null) {
return maybeAlreadyLoaded;
}
long start = System.currentTimeMillis();
CompileClassMeta applicationClass = appClassMgr.getApplicationClass(name);
//For anonymous classes...
if(applicationClass == null) {
VirtualFile file = fileLookup.getJava(name);
applicationClass = appClassMgr.getOrCreateApplicationClass(name, file);
}
//if still null...
if(applicationClass == null) {
//the parent classloader is responsible for this class as it is not on our compile path
return null;
}
if (applicationClass.isDefinable()) {
return applicationClass.javaClass;
}
byte[] bc = byteCodeCache.getBytecode(name, applicationClass.javaSource);
log.trace(()->"Loading class for "+name);
if (bc != null) {
applicationClass.javaByteCode = bc;
applicationClass.javaClass = defineClass(applicationClass.name, applicationClass.javaByteCode, 0, applicationClass.javaByteCode.length, protectionDomain);
resolveClass(applicationClass.javaClass);
if(log.isDebugEnabled()) {
long time = System.currentTimeMillis() - start;
log.trace(()->time+"ms to load class "+name+" from cache");
}
return applicationClass.javaClass;
}
byte[] byteCode = applicationClass.javaByteCode;
if(byteCode == null)
byteCode = applicationClass.compile(compiler, this);
if(byteCode == null) {
throw new IllegalStateException("Bug, should not get here. we could not compile and no exception thrown(should have had upstream fail fast exception");
//previously removed class and returned
// appClassMgr.classes.remove(name);
// return;
}
applicationClass.javaClass = defineClass(applicationClass.name, applicationClass.javaByteCode, 0, applicationClass.javaByteCode.length, protectionDomain);
byteCodeCache.cacheBytecode(applicationClass.javaByteCode, name, applicationClass.javaSource);
resolveClass(applicationClass.javaClass);
if(log.isTraceEnabled()) {
long time = System.currentTimeMillis() - start;
log.trace(()->time+"ms to load class "+name);
}
return applicationClass.javaClass;
}
/**
* Search for the byte code of the given class.
*/
@Override
public byte[] getClassDefinition(String name) {
name = name.replace(".", "/") + ".class";
try (InputStream is = getResourceAsStream(name)) {
if (is == null) {
return null;
}
ByteArrayOutputStream os = new ByteArrayOutputStream();
byte[] buffer = new byte[8192];
int count;
while ((count = is.read(buffer, 0, buffer.length)) > 0) {
os.write(buffer, 0, count);
}
return os.toByteArray();
} catch (Exception e) {
throw new IllegalArgumentException(e);
}
}
/**
* You know ...
*/
@Override
public InputStream getResourceAsStream(String name) {
for (VirtualFile vf : config.getJavaPath()) {
VirtualFile res = vf.child(name);
if (res != null && res.exists()) {
return res.openInputStream();
}
}
return super.getResourceAsStream(name);
}
/**
* You know ...
*/
@Override
public URL getResource(String name) {
for (VirtualFile vf : config.getJavaPath()) {
VirtualFile res = vf.child(name);
if (res != null && res.exists()) {
return res.toURL();
}
}
return super.getResource(name);
}
/**
* You know ...
*/
@Override
public Enumeration<URL> getResources(String name) throws IOException {
List<URL> urls = new ArrayList<URL>();
for (VirtualFile vf : config.getJavaPath()) {
VirtualFile res = vf.child(name);
if (res != null && res.exists()) {
urls.add(res.toURL());
}
}
Enumeration<URL> parent = super.getResources(name);
while (parent.hasMoreElements()) {
URL next = parent.nextElement();
if (!urls.contains(next)) {
urls.add(next);
}
}
final Iterator<URL> it = urls.iterator();
return new Enumeration<URL>() {
@Override
public boolean hasMoreElements() {
return it.hasNext();
}
@Override
public URL nextElement() {
return it.next();
}
};
}
/**
* Detect Java changes
*/
public boolean isNeedToReloadJavaFiles() {
// Now check for file modification
List<CompileClassMeta> modifieds = new ArrayList<CompileClassMeta>();
for (CompileClassMeta applicationClass : appClassMgr.all()) {
if (applicationClass.javaFile.lastModified() > applicationClass.timestamp) {
applicationClass.refresh();
modifieds.add(applicationClass);
}
}
Set<CompileClassMeta> modifiedWithDependencies = new HashSet<CompileClassMeta>();
modifiedWithDependencies.addAll(modifieds);
List<ClassDefinition> newDefinitions = new ArrayList<ClassDefinition>();
for (CompileClassMeta applicationClass : modifiedWithDependencies) {
if (applicationClass.compile(compiler, this) == null) {
appClassMgr.classes.remove(applicationClass.name);
throw new IllegalStateException("In what case can this ever happen in?");
} else {
byteCodeCache.cacheBytecode(applicationClass.javaByteCode, applicationClass.name, applicationClass.javaSource);
//in rare case where outerclass is outside scope of compiling, but inner static class can be recompiled
if(applicationClass.javaClass == null) {
loadApplicationClass(applicationClass.name);
}
newDefinitions.add(new ClassDefinition(applicationClass.javaClass, applicationClass.javaByteCode));
}
}
if (newDefinitions.size() > 0) {
//Cache.clear();
if (HotswapAgent.enabled) {
try {
HotswapAgent.reload(newDefinitions.toArray(new ClassDefinition[newDefinitions.size()]));
} catch (Throwable e) {
return true;
}
} else {
return true;
}
}
// Now check if there is new classes or removed classes
int hash = classStateHashCreator.computePathHash(config.getJavaPath());
if (hash != this.pathHash) {
// Remove class for deleted files !!
for (CompileClassMeta applicationClass : appClassMgr.all()) {
if (!applicationClass.javaFile.exists()) {
appClassMgr.classes.remove(applicationClass.name);
}
if (applicationClass.name.contains("$")) {
appClassMgr.classes.remove(applicationClass.name);
// Ok we have to remove all classes from the same file ...
VirtualFile vf = applicationClass.javaFile;
for (CompileClassMeta ac : appClassMgr.all()) {
if (ac.javaFile.equals(vf)) {
appClassMgr.classes.remove(ac.name);
}
}
}
}
return true;
}
return false;
}
@Override
public String toString() {
return "[CompilingClassLoader " + appClassMgr+", instance="+instance+"]";
}
}