Skip to content

Commit

Permalink
Remove runtime classpath scanning (#911)
Browse files Browse the repository at this point in the history
* Remove runtime classpath scanning

Operations, custom XStream types,and publishable classes are stored in registry files in the META-INF directory of the core project JAR.

* Use annotation processor to generate class lists
  • Loading branch information
SamCarlberg authored Mar 27, 2019
1 parent a2c3264 commit 142b1de
Show file tree
Hide file tree
Showing 57 changed files with 506 additions and 160 deletions.
16 changes: 16 additions & 0 deletions annotation/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# GRIP Annotation Processor

This subproject contains an annotation processor used to generate manifest files in the core project,
used by the GRIP runtime to discover operations, publishable data types, and aliases for XStream
serialization for save files.

The annotation processor generates these files:

| Annotation | File |
|---|---|
| `@Description` | `/META-INF/operations` |
| `@PublishableObject` | `/META-INF/publishables` |
| `@XStreamAlias` | `/META-INF/xstream-aliases` |

Each file contains a list of the names of the classes annotated with the corresponding annotation,
which is then read by the `MetaInfReader` class in the GRIP core module.
12 changes: 12 additions & 0 deletions annotation/annotation.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
plugins {
`java-library`
}

repositories {
mavenCentral()
}

dependencies {
compileOnly(group = "com.google.auto.service", name = "auto-service", version = "1.0-rc4")
annotationProcessor(group = "com.google.auto.service", name = "auto-service", version = "1.0-rc4")
}
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
package edu.wpi.grip.core;
package edu.wpi.grip.annotation.operation;

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

import static edu.wpi.grip.core.OperationDescription.Category;
import static edu.wpi.grip.core.OperationDescription.Category.MISCELLANEOUS;
import static edu.wpi.grip.annotation.operation.OperationCategory.MISCELLANEOUS;


/**
* Annotates an {@link Operation} subclass to describe it. This annotation gets transformed into a
* {@link OperationDescription}. All operation classes with this annotation will be automatically
* Annotates an {@code Operation} subclass to describe it. This annotation gets transformed into a
* {@code OperationDescription}. All operation classes with this annotation will be automatically
* discovered and added to the palette at startup.
*/
@Target(ElementType.TYPE)
Expand All @@ -30,9 +30,9 @@

/**
* The category the operation belongs to. Defaults to
* {@link OperationDescription.Category#MISCELLANEOUS MISCELLANEOUS}.
* {@link OperationCategory#MISCELLANEOUS MISCELLANEOUS}.
*/
Category category() default MISCELLANEOUS;
OperationCategory category() default MISCELLANEOUS;

/**
* All known aliases of the operation. If the name of the operation changes, the previous name
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package edu.wpi.grip.annotation.operation;

/**
* The categories that entries can be in.
*/
public enum OperationCategory {
IMAGE_PROCESSING,
FEATURE_DETECTION,
NETWORK,
LOGICAL,
OPENCV,
MISCELLANEOUS,
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package edu.wpi.grip.annotation.operation;

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

/**
* Marks a type as being publishable by a network operation.
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface PublishableObject {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package edu.wpi.grip.annotation.processor;

import edu.wpi.grip.annotation.operation.Description;
import edu.wpi.grip.annotation.operation.PublishableObject;

import com.google.auto.service.AutoService;
import com.google.common.collect.ImmutableMap;

import java.io.IOException;
import java.io.Writer;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

import javax.annotation.processing.AbstractProcessor;
import javax.annotation.processing.Filer;
import javax.annotation.processing.Processor;
import javax.annotation.processing.RoundEnvironment;
import javax.annotation.processing.SupportedAnnotationTypes;
import javax.annotation.processing.SupportedSourceVersion;
import javax.lang.model.SourceVersion;
import javax.lang.model.element.Element;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.DeclaredType;
import javax.lang.model.type.TypeKind;
import javax.lang.model.util.SimpleTypeVisitor8;
import javax.tools.Diagnostic;
import javax.tools.FileObject;
import javax.tools.StandardLocation;

/**
* Processes elements with the GRIP annotations and generates class list files for them.
*/
@SupportedAnnotationTypes({
"edu.wpi.grip.annotation.*",
"com.thoughtworks.xstream.annotations.XStreamAlias"
})
@SupportedSourceVersion(SourceVersion.RELEASE_8)
@AutoService(Processor.class)
public class ClassListProcessor extends AbstractProcessor {

public static final String OPERATIONS_FILE_NAME = "operations";
public static final String PUBLISHABLES_FILE_NAME = "publishables";
public static final String XSTREAM_ALIASES_FILE_NAME = "xstream-aliases";

private final Map<String, String> fileNames = ImmutableMap.of(
Description.class.getName(), OPERATIONS_FILE_NAME,
PublishableObject.class.getName(), PUBLISHABLES_FILE_NAME,
"com.thoughtworks.xstream.annotations.XStreamAlias", XSTREAM_ALIASES_FILE_NAME
);

@Override
public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
for (TypeElement annotation : annotations) {
String fileName = fileNames.get(annotation.asType().toString());
if (fileName != null) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.NOTE, "Processing " + annotation);
createFile(fileName, classesAnnotatedWith(annotation, roundEnv));
}
}
return false;
}

private Iterable<String> classesAnnotatedWith(TypeElement element, RoundEnvironment roundEnv) {
return roundEnv.getElementsAnnotatedWith(element)
.stream()
.map(Element::asType)
.map(t -> t.accept(TypeNameExtractor.INSTANCE, null))
.collect(Collectors.toList());
}

private void createFile(String fileName, Iterable<String> classNames) {
Filer filer = processingEnv.getFiler();

String resource = "META-INF/" + fileName;

try {
FileObject fileObject = filer.createResource(StandardLocation.CLASS_OUTPUT, "", resource);
try (Writer writer = fileObject.openWriter()) {
for (String className : classNames) {
writer.write(className);
writer.write('\n');
}
}
} catch (IOException e) {
processingEnv.getMessager().printMessage(Diagnostic.Kind.ERROR,
"Unable to create resource file " + resource + ": " + e.getMessage());
}
}

private static final class TypeNameExtractor extends SimpleTypeVisitor8<String, Void> {

static final TypeNameExtractor INSTANCE = new TypeNameExtractor();

@Override
public String visitDeclared(DeclaredType t, Void o) {
String typeName = t.toString();
if (typeName.contains("<")) {
typeName = typeName.substring(0, typeName.indexOf('<'));
}
if (t.getEnclosingType().getKind() != TypeKind.NONE) {
// Inner class, replace '.' with '$'
int lastDot = typeName.lastIndexOf('.');
String first = typeName.substring(0, lastDot);
String second = typeName.substring(lastDot + 1);
typeName = first + "$" + second;
}
return typeName;
}
}
}
2 changes: 2 additions & 0 deletions core/core.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ val os = osdetector.classifier.replace("osx", "macosx").replace("x86_32", "x86")
val arch = osdetector.arch.replace("x86_64", "x64")

dependencies {
api(project(":annotation"))
annotationProcessor(project(":annotation"))
api(group = "com.google.code.findbugs", name = "jsr305", version = "3.0.1")
api(group = "org.bytedeco", name = "javacv", version = "1.1")
api(group = "org.bytedeco.javacpp-presets", name = "opencv", version = "3.0.0-1.1")
Expand Down
35 changes: 13 additions & 22 deletions core/src/main/java/edu/wpi/grip/core/OperationDescription.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package edu.wpi.grip.core;

import edu.wpi.grip.annotation.operation.Description;
import edu.wpi.grip.annotation.operation.OperationCategory;
import edu.wpi.grip.core.util.Icon;

import com.google.common.base.MoreObjects;
Expand All @@ -23,7 +25,7 @@ public class OperationDescription {

private final String name;
private final String summary;
private final Category category;
private final OperationCategory category;
private final Icon icon;
private final ImmutableSet<String> aliases;

Expand Down Expand Up @@ -59,25 +61,26 @@ public static OperationDescription from(Class<? extends Operation> clazz) {
*/
private OperationDescription(String name,
String summary,
Category category,
OperationCategory category,
Icon icon,
Set<String> aliases) {
this.name = checkNotNull(name, "Name cannot be null");
this.summary = checkNotNull(summary, "Summary cannot be null");
this.category = checkNotNull(category, "Category cannot be null");
this.category = checkNotNull(category, "OperationCategory cannot be null");
this.icon = icon; // This is allowed to be null
this.aliases = ImmutableSet.copyOf(checkNotNull(aliases, "Aliases cannot be null"));
}

/**
* Creates a new {@link Builder} instance to create a new {@code OperationDescription} object. The
* created descriptor has a default category of {@link Category#MISCELLANEOUS MISCELLANEOUS} and
* no icon; use the {@link Builder#category(Category) .category()} and {@link Builder#icon(Icon)
* .icon()} methods to override the default values.
* created descriptor has a default category of
* {@link OperationCategory#MISCELLANEOUS MISCELLANEOUS} and no icon; use the
* {@link Builder#category(OperationCategory) .category()} and {@link Builder#icon(Icon) .icon()}
* methods to override the default values.
*/
public static Builder builder() {
return new Builder()
.category(Category.MISCELLANEOUS)
.category(OperationCategory.MISCELLANEOUS)
.icon(null);
}

Expand All @@ -98,7 +101,7 @@ public String summary() {
/**
* @return What category the operation falls under. This is used to organize them in the GUI.
*/
public Category category() {
public OperationCategory category() {
return category;
}

Expand Down Expand Up @@ -155,25 +158,13 @@ public String toString() {
.toString();
}

/**
* The categories that entries can be in.
*/
public enum Category {
IMAGE_PROCESSING,
FEATURE_DETECTION,
NETWORK,
LOGICAL,
OPENCV,
MISCELLANEOUS,
}

/**
* Builder class for {@code OperationDescription}.
*/
public static final class Builder {
private String name;
private String summary = "PLEASE PROVIDE A DESCRIPTION TO THE OPERATION DESCRIPTION!";
private Category category;
private OperationCategory category;
private Icon icon;
private ImmutableSet<String> aliases = ImmutableSet.of(); // default to empty Set to
// avoid NPE if not assigned
Expand Down Expand Up @@ -203,7 +194,7 @@ public Builder summary(String summary) {
/**
* Sets the category.
*/
public Builder category(Category category) {
public Builder category(OperationCategory category) {
this.category = checkNotNull(category);
return this;
}
Expand Down
31 changes: 8 additions & 23 deletions core/src/main/java/edu/wpi/grip/core/operations/Operations.java
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package edu.wpi.grip.core.operations;

import edu.wpi.grip.core.Description;
import edu.wpi.grip.annotation.operation.Description;
import edu.wpi.grip.annotation.processor.ClassListProcessor;
import edu.wpi.grip.core.FileManager;
import edu.wpi.grip.core.Operation;
import edu.wpi.grip.core.OperationDescription;
Expand All @@ -17,11 +18,11 @@
import edu.wpi.grip.core.operations.network.ros.ROSNetworkPublisherFactory;
import edu.wpi.grip.core.operations.network.ros.ROSPublishOperation;
import edu.wpi.grip.core.sockets.InputSocket;
import edu.wpi.grip.core.util.MetaInfReader;

import com.google.common.annotations.VisibleForTesting;
import com.google.common.collect.ImmutableList;
import com.google.common.eventbus.EventBus;
import com.google.common.reflect.ClassPath;
import com.google.inject.Inject;
import com.google.inject.Injector;
import com.google.inject.Singleton;
Expand Down Expand Up @@ -56,7 +57,7 @@ public class Operations {
private final ROSNetworkPublisherFactory rosManager; //NOPMD
private final ImmutableList<OperationMetaData> operations;

private List<Class<Publishable>> publishableTypes = null;
private List<Class<? extends Publishable>> publishableTypes = null;

/**
* Creates a new Operations instance. This should only be used in tests.
Expand Down Expand Up @@ -109,16 +110,9 @@ private OperationDescription descriptionFor(Class<? extends Operation> clazz) {
return OperationDescription.from(clazz.getAnnotation(Description.class));
}

@SuppressWarnings("unchecked")
private List<OperationMetaData> createBasicOperations() {
try {
ClassPath cp = ClassPath.from(getClass().getClassLoader());
return cp.getAllClasses().stream()
.filter(ci -> ci.getName().startsWith("edu.wpi.grip.core.operations"))
.map(ClassPath.ClassInfo::load)
.filter(Operation.class::isAssignableFrom)
.map(c -> (Class<? extends Operation>) c)
.filter(c -> c.isAnnotationPresent(Description.class))
return MetaInfReader.<Operation>readClasses(ClassListProcessor.OPERATIONS_FILE_NAME)
.map(c -> new OperationMetaData(descriptionFor(c), () -> injector.getInstance(c)))
.collect(Collectors.toList());
} catch (IOException e) {
Expand All @@ -131,21 +125,12 @@ private List<OperationMetaData> createBasicOperations() {
* Finds all subclasses of {@link Publishable} in {@code edu.wpi.grip.core.operation}.
*/
@SuppressWarnings("unchecked")
private List<Class<Publishable>> findPublishables() {
private List<Class<? extends Publishable>> findPublishables() {
if (publishableTypes == null) {
// Only need to search once
try {
ClassPath cp = ClassPath.from(getClass().getClassLoader());
publishableTypes = cp.getAllClasses().stream()
// only look in our namespace (don't want to wade through tens of thousands of classes)
.filter(ci -> ci.getName().startsWith("edu.wpi.grip.core.operation"))
.map(ClassPath.ClassInfo::load)
.filter(Publishable.class::isAssignableFrom)
// only accept concrete top-level subclasses
.filter(c -> !c.isAnonymousClass() && !c.isInterface() && !c.isLocalClass()
&& !c.isMemberClass())
.filter(c -> Modifier.isPublic(c.getModifiers()))
.map(c -> (Class<Publishable>) c)
String fileName = ClassListProcessor.PUBLISHABLES_FILE_NAME;
publishableTypes = MetaInfReader.<Publishable>readClasses(fileName)
.collect(Collectors.toList());
} catch (IOException e) {
logger.log(Level.WARNING, "Could not find the publishable types.", e);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.wpi.grip.core.operations;

import edu.wpi.grip.annotation.operation.OperationCategory;
import edu.wpi.grip.core.Operation;
import edu.wpi.grip.core.OperationDescription;
import edu.wpi.grip.core.sockets.InputSocket;
Expand Down Expand Up @@ -81,7 +82,7 @@ public static OperationDescription descriptionFor(PythonScriptFile pythonScriptF
.name(pythonScriptFile.name())
.summary(pythonScriptFile.summary())
.icon(Icon.iconStream("python"))
.category(OperationDescription.Category.MISCELLANEOUS)
.category(OperationCategory.MISCELLANEOUS)
.build();
}

Expand Down
Loading

0 comments on commit 142b1de

Please sign in to comment.