Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions restx-classloader/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,13 @@
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<compilerArgument>-proc:none</compilerArgument>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-jar-plugin</artifactId>
Expand Down
21 changes: 21 additions & 0 deletions restx-classloader/src/main/java/restx/classloader/Cold.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package restx.classloader;

import static java.lang.annotation.ElementType.TYPE;


import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
* This annotation permits to declare a class as cold.
*
* Once annotated a class will not be hot-reloaded.
*
* @author apeyrard
*/
@Target(TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface Cold {

}
70 changes: 70 additions & 0 deletions restx-classloader/src/main/java/restx/classloader/ColdClasses.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package restx.classloader;

import com.google.common.base.Charsets;
import com.google.common.base.Splitter;
import com.google.common.collect.ImmutableSet;
import com.google.common.io.Resources;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import java.net.URL;
import java.util.Enumeration;

/**
* Helper methods to manage cold classes.
*
* @author apeyrard
*/
public class ColdClasses {
private static final Logger logger = LoggerFactory.getLogger(ColdClasses.class);

public static final String COLD_CLASSES_FILE_PATH = "META-INF/cold-classes.list";

private ColdClasses() {}

/**
* Extracts the cold classes from a string containing a list of cold classes.
* <p>
* The {@code coldClasses} parameter contains a list of fqcn separated by ',' character.
*
* @param classLoader the classloader to load cold classes
* @param coldClasses all cold classes in a string
* @return a set of cold classes
*/
public static ImmutableSet<Class<?>> extractFromString(ClassLoader classLoader, String coldClasses) {
ImmutableSet.Builder<Class<?>> classes = ImmutableSet.builder();
for (String fqcn : Splitter.on(',').trimResults().split(coldClasses)) {
try {
classes.add(classLoader.loadClass(fqcn));
} catch (ClassNotFoundException e) {
logger.warn("invalid cold class {}: unable to find it from supplied classloader", fqcn);
}
}
return classes.build();
}

/**
* Extracts the cold classes from resources file.
*
* @param classLoader the classloader to load cold classes and resources
* @return the list of cold classes
* @throws java.io.IOException if an I/O error occurs
*/
public static ImmutableSet<Class<?>> extractFromResources(final ClassLoader classLoader) throws IOException {
ImmutableSet.Builder<Class<?>> classes = ImmutableSet.builder();

Enumeration<URL> resources = classLoader.getResources(COLD_CLASSES_FILE_PATH);
while (resources.hasMoreElements()) {
for (String fqcn : Resources.readLines(resources.nextElement(), Charsets.UTF_8)) {
try {
classes.add(classLoader.loadClass(fqcn));
} catch (ClassNotFoundException e) {
logger.warn("invalid cold class {}: unable to find it from supplied classloader", fqcn);
}
}
}

return classes.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
package restx.classloader.processor;

import com.google.common.base.Joiner;
import com.google.common.collect.Ordering;
import com.google.common.collect.Sets;
import com.google.common.io.CharStreams;

import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.util.Set;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedOptions;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import restx.classloader.Cold;
import restx.classloader.ColdClasses;
import restx.common.processor.RestxAbstractProcessor;

/**
* Annotation processor for the cold classes.
*
* @author apeyrard
*/
@SupportedAnnotationTypes({
"restx.classloader.Cold",
})
@SupportedOptions({ "debug" })
public class ColdClassesAnnotationProcessor extends RestxAbstractProcessor {
private final ColdClassesAnnotationProcessor.ColdClassesDeclaration coldClassesDeclaration;

public ColdClassesAnnotationProcessor() {
this.coldClassesDeclaration = new ColdClassesDeclaration();
}

@Override
protected boolean processImpl(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) throws Exception {
coldClassesDeclaration.processing();
if (roundEnv.processingOver()) {
coldClassesDeclaration.generate();
} else {
processColdClasses(roundEnv);
}

return true;
}

private void processColdClasses(RoundEnvironment roundEnv) {
for (Element annotation : roundEnv.getElementsAnnotatedWith(Cold.class)) {
if (!(annotation instanceof TypeElement)) {
error("annotating element " + annotation + " of type " + annotation.getKind().name()
+ " with @Cold is not supported", annotation);
continue;
}
TypeElement typeElem = (TypeElement) annotation;
coldClassesDeclaration.declareColdClass(typeElem.getQualifiedName().toString());
}
}

private class ColdClassesDeclaration extends ResourceDeclaration {
private final Set<String> coldClasses = Sets.newHashSet();

protected ColdClassesDeclaration() {
super(ColdClasses.COLD_CLASSES_FILE_PATH);
}

void declareColdClass(String coldClass) {
coldClasses.add(coldClass);
}

@Override
protected boolean requireGeneration() {
return coldClasses.size() > 0;
}

@Override
protected void clearContent() {
coldClasses.clear();
}

@Override
protected void writeContent(Writer writer) throws IOException {
writer.write(Joiner.on('\n').join(Ordering.natural().sortedCopy(coldClasses)));
writer.write('\n');
}

@Override
protected void readContent(Reader reader) throws IOException {
coldClasses.addAll(CharStreams.readLines(reader));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
restx.classloader.processor.ColdClassesAnnotationProcessor
43 changes: 43 additions & 0 deletions restx-common/src/main/java/restx/common/MoreClasses.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package restx.common;

import com.google.common.collect.Sets;

import java.util.Set;

/**
* Provides static utility methods to deal with {@link Class}
*
* @author apeyrard
*/
public class MoreClasses {
private MoreClasses() {}

/**
* Gets all inherited classes for a specified class (in a flat structure). Its super classes, and its interfaces.
*
* This process is recursive, if class A inherit from B which also inherit from C and D, the result will
* be (B, C, D).
*
* @param clazz the clazz to analyse
* @return the list of inherited classes
*/
public static Set<Class> getInheritedClasses(Class clazz) {
Set<Class> inheritedClasses = Sets.newHashSet();

// add super class, and add recursively their inherited classes
Class superClass = clazz.getSuperclass();
if (superClass != null) {
inheritedClasses.add(superClass);
inheritedClasses.addAll(getInheritedClasses(superClass));
}

// add all interfaces, and recursively add their inherited classes
Class[] interfaces = clazz.getInterfaces();
for (Class anInterface : interfaces) {
inheritedClasses.add(anInterface);
inheritedClasses.addAll(getInheritedClasses(anInterface));
}

return inheritedClasses;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,15 @@
import javax.lang.model.type.TypeMirror;
import javax.lang.model.util.Types;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.JavaFileObject;
import javax.tools.StandardLocation;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintWriter;
import java.io.Reader;
import java.io.StringWriter;
import java.io.Writer;
import java.util.Set;
Expand Down Expand Up @@ -110,4 +116,119 @@ protected void generateJavaClass(String className, Template mustache, ImmutableM
public SourceVersion getSupportedSourceVersion() {
return SourceVersion.latest();
}

/**
* Abstract class to manage resource file generation, it permits to read existing content using the
* {@link #processing()} method, which will call the abstract {@link #readContent(java.io.Reader)} method.
* And the method {@link #generate()} need to be called once we want to generate the resource, during this process
* the {@link #writeContent(java.io.Writer)} method will be called.
*/
protected abstract class ResourceDeclaration {
private final String targetFilePath;
private FileObject fileObject;

/**
* @param targetFilePath path of the resource to write
*/
protected ResourceDeclaration(String targetFilePath) {
this.targetFilePath = targetFilePath;
}

/**
* @return true if the resource need to be generated (for example, it permits to skip empty contents)
*/
protected abstract boolean requireGeneration();

/**
* called once the file has been written
*/
protected abstract void clearContent();

/**
* Writes the resource content.
*
* @param writer the writer to use
* @throws IOException if an I/O error occurs
*/
protected abstract void writeContent(Writer writer) throws IOException;

/**
* Reads the resource content.
*
* @param reader the reader to use
* @throws IOException if an I/O error occurs
*/
protected abstract void readContent(Reader reader) throws IOException;

public void generate() throws IOException {
if (!requireGeneration()) {
return;
}

writeResourceFile(targetFilePath);

clearContent();

fileObject = null;
}

public void processing() throws IOException {
readExistingResourceIfExists(targetFilePath);
}

private void writeResourceFile(String targetFile) throws IOException {
if (fileObject != null
&& fileObject.getClass().getSimpleName().equals("EclipseFileObject")
) {
// eclipse does not allow to do a createResource for a fileobject already obtained via getResource
// but the file object can be used for writing, so it's ok to reuse it in this case

// see source code at:
// https://github.com/eclipse/eclipse.jdt.core/blob/master/org.eclipse.jdt.compiler.apt/src/org/eclipse/jdt/internal/compiler/apt/dispatch/BatchProcessingEnvImpl.java
// https://github.com/eclipse/eclipse.jdt.core/blob/master/org.eclipse.jdt.compiler.tool/src/org/eclipse/jdt/internal/compiler/tool/EclipseFileObject.java
} else {
try {
fileObject = processingEnv.getFiler().createResource(StandardLocation.CLASS_OUTPUT, "", targetFile);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
try (Writer writer = fileObject.openWriter()) {
writeContent(writer);
}
}

private void readExistingResourceIfExists(String targetFile) throws IOException {
try {
if (fileObject == null) {
fileObject = processingEnv.getFiler().getResource(StandardLocation.CLASS_OUTPUT, "", targetFile);
}
try (Reader r = fileObject.openReader(true)) {
readContent(r);
}
} catch (FileNotFoundException ex) {
/*
This is a very strange behaviour of javac during incremantal compilation (at least experienced
with Intellij make process): a FileNotFoundException is raised while the file actually exist.

"Fortunately" the exception message is the path of the file, so we can try to load it using
plain java.io
*/
try {
File file = new File(ex.getMessage());
if (file.exists()) {
try (Reader r = new FileReader(file)) {
readContent(r);
} catch (IOException e) {
// ignore
}
}
} catch (Exception e) {
// ignore
}
} catch (IOException | IllegalArgumentException ex) {
// ignore
}
}
}
}
Loading