Skip to content

Commit

Permalink
smallrye#316: add power annotations @Stereotype and @MixinFor wit…
Browse files Browse the repository at this point in the history
…h power-jandex-maven-plugin

Signed-off-by: Rüdiger zu Dohna <[email protected]>
  • Loading branch information
t1 committed Nov 25, 2020
1 parent 110406e commit 5994fb0
Show file tree
Hide file tree
Showing 15 changed files with 755 additions and 0 deletions.
1 change: 1 addition & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@
<module>server</module>
<module>client</module>
<module>tools</module>
<module>power-annotations</module>
</modules>

<dependencyManagement>
Expand Down
55 changes: 55 additions & 0 deletions power-annotations/README.adoc
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
= Power-Annotations

Power-Annotations is actually independent of GraphQL. It's a generic mechanism for meta annotations. It consists of annotations you can put on your code and (for now only) implementation using a maven plugin to build a Jandex index file.

== Annotations

=== Stereotypes

The simplest use-case for Stereotypes is renaming annotations, e.g. `@Property` instead of `@JsonbProperty` (assuming your JBON-B implementation supported Power Annotations).

[source,java]
----
@Retention(RUNTIME)
@Stereotype
@JsonbProperty
public @interface Property {}
----

It gets much more interesting, when you add more annotations from other (supporting) frameworks to your stereotype, e.g. `@XmlAttribute` from JAX-B. Now you have one annotation instead of two with both having the same semantics.

Properly used, stereotypes are shortcuts describing the role the annotated element has; functionally as well as a documentation.

This is exactly what https://jakarta.ee/specifications/cdi/2.0/cdi-spec-2.0.html#stereotypes[CDI-Stereotypes] do, but not restricted to CDI, i.e. the annotations used by any framework that supports Power Annotations can be used with stereotypes. We have our own `Stereotype` class; but as we don't want to depend on CDI and still not exclude CDI, any annotation named `Stereotype` will work.


// TODO === Resolve From Class
//
//This is a very common pattern: annotations on a class are considered as a fallback for member annotations (i.e. on fields or methods), if
//
//* the member is not annotated with the same type or the annotation is repeatable, and
//* the annotation is annotated to be an _explicitly_ allowed `@Target` for `FIELD`/`METHOD`.


// TODO === Inheritance
//
//When annotating a super class or interface, the annotation is valid also for the sub class or interface. This is also true for annotations on overridden or implemented methods.
//
//In Java reflection, this only works for super classes and only if the annotation is annotated as `@Inherited`. As this generally violates the https://en.wikipedia.org/wiki/Liskov_substitution_principle[LSP], power-annotations always resolves these annotations. We may add a mechanism to _not_ inherit annotations later, if the need actually arises.

=== Mixins

Say you have a class you want to add annotations to, but you can't; e.g., because it's a class from some library or even from the JDK. You can create your own class (or interface) and annotate it as `@MixinFor` the target class. The annotations you put on your mixin class will work as if they were on the target class (if your framework supports Power Annotations).

This also works for annotations: say we're developing an application packed with annotations from JPA, which doesn't support mixins (yet). The application also uses a library that supports mixins but doesn't know about JPA, e.g. a future MP GraphQL. We want all JPA `@Id` annotations to be recognized as synonyms for GraphQL `@Id` annotations. We could create a simple mixin for the JPA annotation:

[source,java]
----
@MixinFor(javax.persistence.Id.class)
@org.eclipse.microprofile.graphql.Id
public class PersistenceIdMixin {}
----

Voila! MP GraphQL would work as if all JPA `@Id` annotations were it's own.

NOTE: Mixins are a very powerful kind of magic: use them with caution and only when strictly necessary. Otherwise, the readers of your code will have a hard time to find out why something behaves as if an annotation was there, but it's clearly not. If you can, use Stereotypes instead.
14 changes: 14 additions & 0 deletions power-annotations/annotations/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-power-annotations-parent</artifactId>
<version>1.0.17-SNAPSHOT</version>
</parent>

<artifactId>power-annotations</artifactId>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.github.t1.annotations;

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

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

/**
* Add annotations to another class <em>that you can't change</em>; if you <em>can</em> change that class,
* use explicit {@link Stereotype}s instead. Using Mixins is very implicit and difficult to trace back;
* it is often better to actually copy the target class, so you can directly add your annotations,
* instead of working with mixins. You have been warned!
*
* If you have a class that you don't control, e.g.:
*
* <pre>
* <code>
* public class ImportedClass {
* ...
* }
* </code>
* </pre>
*
* And you need it to be annotated as <code>@SomeAnnotation</code>. Then you can write a mixin (the name is arbitrary):
*
* <pre>
* <code>
* &#64;MixinFor(ImportedClass.class)
* &#64;SomeAnnotation
* public class ImportedClassMixin {
* ...
* }
* </code>
* </pre>
*
* After the Power Annotations have been resolved, the target class looks as if it had another annotation:
*
* <pre>
* <code>
* &#64;SomeAnnotation
* public class ImportedClass {
* ...
* }
* </code>
* </pre>
*/
@Retention(RUNTIME)
@Target(TYPE)
public @interface MixinFor {
Class<?> value();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.github.t1.annotations;

import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
import static java.lang.annotation.RetentionPolicy.RUNTIME;

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

/**
* Marks an annotation to be a meta annotation, i.e. the annotations on the stereotype (i.e. the annotation that is
* annotated as <code>Stereotype</code>) are logically copied to all targets (i.e. the classes/fields/methods that are
* annotated with the stereotype).
*
* Power Annotations doesn't depend on CDI, so it handles all classes named <code>Stereotype</code> the same.
* You can use this class if you don't have another stereotype class on your classpath,
* but in JEE/MP applications you should have <code>javax.enterprise.inject.Stereotype</code>.
*/
@Retention(RUNTIME)
@Target(ANNOTATION_TYPE)
@Documented
public @interface Stereotype {
}
21 changes: 21 additions & 0 deletions power-annotations/common/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.smallrye</groupId>
<artifactId>smallrye-power-annotations-parent</artifactId>
<version>1.0.17-SNAPSHOT</version>
</parent>

<artifactId>power-annotations-common</artifactId>

<dependencies>
<dependency>
<groupId>org.jboss</groupId>
<artifactId>jandex</artifactId>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
package com.github.t1.powerannotations.common;

import static java.nio.file.FileVisitResult.CONTINUE;
import static java.nio.file.Files.createDirectories;
import static java.nio.file.Files.newInputStream;
import static java.nio.file.Files.newOutputStream;
import static java.nio.file.Files.walkFileTree;
import static org.jboss.jandex.JandexBackdoor.annotations;
import static org.jboss.jandex.JandexBackdoor.classes;
import static org.jboss.jandex.JandexBackdoor.implementors;
import static org.jboss.jandex.JandexBackdoor.newAnnotationInstance;
import static org.jboss.jandex.JandexBackdoor.subclasses;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.nio.file.FileVisitResult;
import java.nio.file.Path;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;

import org.jboss.jandex.AnnotationInstance;
import org.jboss.jandex.AnnotationTarget;
import org.jboss.jandex.AnnotationValue;
import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.DotName;
import org.jboss.jandex.FieldInfo;
import org.jboss.jandex.Index;
import org.jboss.jandex.IndexWriter;
import org.jboss.jandex.Indexer;
import org.jboss.jandex.JandexBackdoor;
import org.jboss.jandex.MethodInfo;
import org.jboss.jandex.Type;

public class Jandex {

public static Jandex scan(Path path) {
return new Jandex(path, scanIndex(path));
}

private static Index scanIndex(Path path) {
final Indexer indexer = new Indexer();
try {
walkFileTree(path, new SimpleFileVisitor<Path>() {
@Override
public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException {
if (file.toString().endsWith(".class")) {
try (InputStream inputStream = newInputStream(file)) {
indexer.index(inputStream);
}
}
return CONTINUE;
}
});
return indexer.complete();
} catch (IOException e) {
throw new RuntimeException("failed to index", e);
}
}

private final Path path;
final Index index;

private final Map<DotName, List<AnnotationInstance>> annotations;
private final Map<DotName, List<ClassInfo>> subclasses;
private final Map<DotName, List<ClassInfo>> implementors;
private final Map<DotName, ClassInfo> classes;

public Jandex(Path path, Index index) {
this.path = path;
this.index = index;

this.annotations = annotations(index);
this.subclasses = subclasses(index);
this.implementors = implementors(index);
this.classes = classes(index);
}

public Set<DotName> allAnnotationNames() {
return annotations.keySet();
}

public void copyClassAnnotation(AnnotationInstance original, DotName className) {
ClassInfo classInfo = classes.get(className);
AnnotationInstance copy = copyAnnotationInstance(original, classInfo);
add(copy, annotations(classInfo));
}

public void copyFieldAnnotation(AnnotationInstance original, DotName className, String fieldName) {
ClassInfo classInfo = classes.get(className);
FieldInfo field = classInfo.field(fieldName);
AnnotationInstance annotationInstance = copyAnnotationInstance(original, field);
JandexBackdoor.add(annotationInstance, field);
add(annotationInstance, annotations(classInfo));
}

public void copyMethodAnnotation(AnnotationInstance original, DotName className, String methodName, Type... parameters) {
ClassInfo classInfo = classes.get(className);
MethodInfo method = classInfo.method(methodName, parameters);
AnnotationInstance annotationInstance = copyAnnotationInstance(original, method);
JandexBackdoor.add(annotationInstance, method);
add(annotationInstance, annotations(classInfo));
}

private AnnotationInstance copyAnnotationInstance(AnnotationInstance original, AnnotationTarget annotationTarget) {
return createAnnotationInstance(original.name(), annotationTarget, original.values().toArray(new AnnotationValue[0]));
}

private AnnotationInstance createAnnotationInstance(DotName annotationName, AnnotationTarget target,
AnnotationValue... values) {
AnnotationInstance annotation = newAnnotationInstance(annotationName, target, values);
add(annotation, annotations);
return annotation;
}

private void add(AnnotationInstance instance, Map<DotName, List<AnnotationInstance>> map) {
if (!map.containsKey(instance.name()))
map.put(instance.name(), new ArrayList<>());
map.get(instance.name()).add(instance);
}

public void write() {
Path filePath = path.resolve("META-INF/jandex.idx");
try {
createDirectories(filePath.getParent());
OutputStream outputStream = newOutputStream(filePath);
new IndexWriter(outputStream).write(index);
outputStream.close();
} catch (IOException e) {
throw new RuntimeException("can't write jandex to " + filePath, e);
}
}

public void print() {
new JandexPrinter(index).run();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package com.github.t1.powerannotations.common;

import static java.nio.file.Files.newInputStream;

import java.io.BufferedInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Path;
import java.util.function.Consumer;

import org.jboss.jandex.ClassInfo;
import org.jboss.jandex.Index;
import org.jboss.jandex.IndexReader;
import org.jboss.jandex.IndexView;
import org.jboss.jandex.MethodInfo;

public class JandexPrinter {

private final IndexView index;

public JandexPrinter(Path indexFile) {
this(load(indexFile));
}

public JandexPrinter(IndexView index) {
this.index = index;
}

private static IndexView load(Path indexFile) {
System.out.println("load from " + indexFile);
try (InputStream inputStream = new BufferedInputStream(newInputStream(indexFile))) {
return new IndexReader(inputStream).read();
} catch (IOException e) {
throw new RuntimeException("can't load Jandex index file", e);
}
}

public void run() {
System.out.println("------------------------------------------------------------");
((Index) index).printAnnotations();
System.out.println("------------------------------------------------------------");
((Index) index).printSubclasses();
System.out.println("------------------------------------------------------------");
index.getKnownClasses().forEach(new Consumer<ClassInfo>() {
@Override
public void accept(ClassInfo classInfo) {
if (!classInfo.name().toString().startsWith("test."))
return;
System.out.println(classInfo.name() + ":");
classInfo.methods().forEach(new Consumer<MethodInfo>() {
@Override
public void accept(MethodInfo method) {
System.out.println(" " + method.name() + " [" + method.defaultValue() + "]");
}
});
}
});
System.out.println("------------------------------------------------------------");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.github.t1.powerannotations.common;

public interface Logger {
void info(String message);
}
Loading

0 comments on commit 5994fb0

Please sign in to comment.