diff --git a/power-annotations/README.adoc b/power-annotations/README.adoc
index 563d703e2..7af45bba3 100644
--- a/power-annotations/README.adoc
+++ b/power-annotations/README.adoc
@@ -1,6 +1,12 @@
= 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.
+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 (these are meant to become a separate standard) and the following implementations:
+
+1. A maven plugin to build a Jandex index file (and resolve all power annotations). This replaces the `jandex-maven-plugin`. It currently has fewer options, but as we only want to build what people actually need, give us a call if you need more!
+
+2. A classpath scanner to build a Jandex index at runtime (and resolve all power annotations).
+
+There's also a TCK to test, if an implementation adheres to the standard defined by the annotations. It uses an Utils API, so the TCK can also verify implementations not using Jandex. This Annotation Utils API may be convenient for your usage as well, as, e.g., it also resolves repeated annotations.
== Annotations
@@ -39,7 +45,7 @@ This is exactly what https://jakarta.ee/specifications/cdi/2.0/cdi-spec-2.0.html
=== 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).
+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 or even annotation) 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:
diff --git a/power-annotations/annotations/pom.xml b/power-annotations/annotations/pom.xml
index 267ff1f9c..ce731e5f5 100644
--- a/power-annotations/annotations/pom.xml
+++ b/power-annotations/annotations/pom.xml
@@ -9,4 +9,6 @@
power-annotations
+ Power Annotations Annotations
+ The Annotations resolved by Power Annotations
diff --git a/power-annotations/common/pom.xml b/power-annotations/common/pom.xml
index aa9c2e141..d6ab3020c 100644
--- a/power-annotations/common/pom.xml
+++ b/power-annotations/common/pom.xml
@@ -8,7 +8,9 @@
1.0.20-SNAPSHOT
- power-annotations-common
+ power-annotations-jandex-common
+ Power Annotations Jandex Common
+ Common Jandex specific Power Annotations code used by build plugins and the runtime scanner.
diff --git a/power-annotations/common/src/main/java/com/github/t1/powerannotations/common/Jandex.java b/power-annotations/common/src/main/java/com/github/t1/powerannotations/common/Jandex.java
index c8e787887..0d341c398 100644
--- a/power-annotations/common/src/main/java/com/github/t1/powerannotations/common/Jandex.java
+++ b/power-annotations/common/src/main/java/com/github/t1/powerannotations/common/Jandex.java
@@ -85,13 +85,13 @@ public Set allAnnotationNames() {
}
public void copyClassAnnotation(AnnotationInstance original, DotName className) {
- ClassInfo classInfo = classes.get(className);
+ ClassInfo classInfo = getClassInfo(className);
AnnotationInstance copy = copyAnnotationInstance(original, classInfo);
add(copy, annotations(classInfo));
}
public void copyFieldAnnotation(AnnotationInstance original, DotName className, String fieldName) {
- ClassInfo classInfo = classes.get(className);
+ ClassInfo classInfo = getClassInfo(className);
FieldInfo field = classInfo.field(fieldName);
AnnotationInstance annotationInstance = copyAnnotationInstance(original, field);
JandexBackdoor.add(annotationInstance, field);
@@ -99,13 +99,20 @@ public void copyFieldAnnotation(AnnotationInstance original, DotName className,
}
public void copyMethodAnnotation(AnnotationInstance original, DotName className, String methodName, Type... parameters) {
- ClassInfo classInfo = classes.get(className);
+ ClassInfo classInfo = getClassInfo(className);
MethodInfo method = classInfo.method(methodName, parameters);
AnnotationInstance annotationInstance = copyAnnotationInstance(original, method);
JandexBackdoor.add(annotationInstance, method);
add(annotationInstance, annotations(classInfo));
}
+ private ClassInfo getClassInfo(DotName className) {
+ ClassInfo classInfo = classes.get(className);
+ if (classInfo == null)
+ throw new RuntimeException("class not in index: " + className);
+ return classInfo;
+ }
+
private AnnotationInstance copyAnnotationInstance(AnnotationInstance original, AnnotationTarget annotationTarget) {
return createAnnotationInstance(original.name(), annotationTarget, original.values().toArray(new AnnotationValue[0]));
}
diff --git a/power-annotations/maven-plugin/pom.xml b/power-annotations/maven-plugin/pom.xml
index c11bf0b72..7e5519941 100644
--- a/power-annotations/maven-plugin/pom.xml
+++ b/power-annotations/maven-plugin/pom.xml
@@ -10,6 +10,8 @@
power-jandex-maven-plugin
maven-plugin
+ Power Annotations Jandex Maven Plugin
+ Replaces the `jandex-maven-plugin` to generate a Jandex index file, but with the Power Annotations resolved.
3.5
@@ -45,7 +47,7 @@
io.smallrye
- power-annotations-common
+ power-annotations-jandex-common
${project.version}
diff --git a/power-annotations/pom.xml b/power-annotations/pom.xml
index bb173e24d..562bebf96 100644
--- a/power-annotations/pom.xml
+++ b/power-annotations/pom.xml
@@ -7,15 +7,42 @@
smallrye-graphql-parent
1.0.20-SNAPSHOT
-
+
smallrye-power-annotations-parent
pom
-
- Power Annotations
+ Power Annotations Parent POM
+ Power-Annotations is a generic mechanism for meta annotations, most notably stereotypes and mixins.
annotations
+ utils
+ tck
common
maven-plugin
+ scanner
+
+
+ install
+
+
+ org.apache.maven.plugins
+ maven-compiler-plugin
+
+
+ 8
+ true
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+
+ *Behavior
+
+
+
+
+
diff --git a/power-annotations/scanner/pom.xml b/power-annotations/scanner/pom.xml
new file mode 100644
index 000000000..f2e1519fc
--- /dev/null
+++ b/power-annotations/scanner/pom.xml
@@ -0,0 +1,43 @@
+
+
+ 4.0.0
+
+
+ io.smallrye
+ smallrye-power-annotations-parent
+ 1.0.18-SNAPSHOT
+
+
+ power-annotations-scanner
+ Power Annotations Jandex Runtime Scanner
+ Build a Jandex index at runtime from the classpath and resolve power annotations
+
+
+
+ org.jboss
+ jandex
+ 2.2.2.Final
+
+
+ io.smallrye
+ power-annotations-jandex-common
+ ${project.version}
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.7.0
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.18.1
+ test
+
+
+
diff --git a/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/IndexBuilder.java b/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/IndexBuilder.java
new file mode 100644
index 000000000..6bf37c16b
--- /dev/null
+++ b/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/IndexBuilder.java
@@ -0,0 +1,53 @@
+package com.github.t1.powerannotations.scanner;
+
+import static java.util.logging.Level.FINE;
+import static java.util.stream.Collectors.joining;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Optional;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import org.jboss.jandex.IndexReader;
+import org.jboss.jandex.IndexView;
+
+public class IndexBuilder {
+ public static IndexView loadOrScan() {
+ IndexView jandex = load().orElseGet(Scanner::scan);
+ if (LOG.isLoggable(LEVEL)) {
+ LOG.log(LEVEL, "------------------------------------------------------------");
+ jandex.getKnownClasses()
+ .forEach(classInfo -> LOG.log(LEVEL, classInfo.name() + " :: " + classInfo.classAnnotations().stream()
+ .filter(instance -> !instance.name().toString().equals("kotlin.Metadata")) // contains binary
+ .map(Object::toString).collect(joining(", "))));
+ LOG.log(LEVEL, "------------------------------------------------------------");
+ }
+ return jandex;
+ }
+
+ @SuppressWarnings("UnusedReturnValue")
+ private static Optional load() {
+ try (InputStream inputStream = getClassLoader().getResourceAsStream("META-INF/jandex.idx")) {
+ return Optional.ofNullable(inputStream).map(IndexBuilder::load);
+ } catch (RuntimeException | IOException e) {
+ throw new RuntimeException("can't read index file", e);
+ }
+ }
+
+ private static IndexView load(InputStream inputStream) {
+ try {
+ return new IndexReader(inputStream).read();
+ } catch (RuntimeException | IOException e) {
+ throw new RuntimeException("can't read Jandex input stream", e);
+ }
+ }
+
+ private static ClassLoader getClassLoader() {
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ return (classLoader == null) ? ClassLoader.getSystemClassLoader() : classLoader;
+ }
+
+ private static final Logger LOG = Logger.getLogger(IndexBuilder.class.getName());
+ private static final Level LEVEL = FINE;
+}
diff --git a/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/IndexerConfig.java b/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/IndexerConfig.java
new file mode 100644
index 000000000..72a890d67
--- /dev/null
+++ b/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/IndexerConfig.java
@@ -0,0 +1,54 @@
+package com.github.t1.powerannotations.scanner;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Properties;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+import java.util.stream.Stream;
+
+class IndexerConfig {
+ private final List exclude = new ArrayList<>();
+
+ IndexerConfig() {
+ this("META-INF/jandex.properties");
+ }
+
+ IndexerConfig(String resource) {
+ try (InputStream inputStream = ClassLoader.getSystemClassLoader().getResourceAsStream(resource)) {
+ if (inputStream == null)
+ return;
+ Properties properties = new Properties();
+ properties.load(inputStream);
+ loadExcludeConfig(properties);
+ } catch (IOException e) {
+ throw new RuntimeException("can't load " + resource, e);
+ }
+ }
+
+ private void loadExcludeConfig(Properties properties) {
+ String excludeString = properties.getProperty("exclude", null);
+ if (excludeString != null) {
+ try {
+ Stream.of(excludeString.split("\\s+"))
+ .map(this::gavToRegex)
+ .forEach(this.exclude::add);
+ } catch (Exception e) {
+ throw new RuntimeException("can't parse exclude config", e);
+ }
+ }
+ }
+
+ private String gavToRegex(String groupArtifact) {
+ Matcher matcher = Pattern.compile("(?[^:]+):(?[^:]+)").matcher(groupArtifact);
+ if (!matcher.matches())
+ throw new RuntimeException("expect `group:artifact` but found `" + groupArtifact + "`");
+ return ".*/" + matcher.group("group") + "/.*/" + matcher.group("artifact") + "-.*\\.jar";
+ }
+
+ public Stream excludes() {
+ return exclude.stream();
+ }
+}
diff --git a/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/Scanner.java b/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/Scanner.java
new file mode 100644
index 000000000..5657064b0
--- /dev/null
+++ b/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/Scanner.java
@@ -0,0 +1,133 @@
+package com.github.t1.powerannotations.scanner;
+
+import static java.nio.charset.StandardCharsets.UTF_8;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.logging.Logger;
+import java.util.stream.Stream;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipInputStream;
+
+import org.jboss.jandex.Index;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.Indexer;
+
+public class Scanner {
+
+ public static IndexView scan() {
+ return new Scanner().scanClassPath();
+ }
+
+ private final Indexer indexer = new Indexer();
+ private final IndexerConfig config = new IndexerConfig();
+ private int archivesIndexed;
+ private int classesIndexed;
+
+ private IndexView scanClassPath() {
+ long t = System.currentTimeMillis();
+ urls()
+ .distinct()
+ .filter(this::include)
+ .forEach(this::index);
+ Index index = indexer.complete();
+ LOG.info("scanned " + archivesIndexed + " archives with " + classesIndexed + " classes " +
+ "in " + (System.currentTimeMillis() - t) + "ms");
+ return index;
+ }
+
+ private Stream urls() {
+ ClassLoader classLoader = ClassLoader.getSystemClassLoader();
+ if (classLoader instanceof URLClassLoader) {
+ return Stream.of(((URLClassLoader) classLoader).getURLs());
+ } else {
+ return classPath().map(Scanner::toUrl);
+ }
+ }
+
+ private Stream classPath() {
+ return Stream.of(System.getProperty("java.class.path").split(System.getProperty("path.separator")));
+ }
+
+ private static URL toUrl(String url) {
+ try {
+ return Paths.get(url).toUri().toURL();
+ } catch (MalformedURLException e) {
+ throw new RuntimeException("invalid classpath url " + url, e);
+ }
+ }
+
+ private boolean include(URL url) {
+ String urlString = url.toString();
+ return config.excludes().noneMatch(urlString::matches);
+ }
+
+ private void index(URL url) {
+ try {
+ long t0 = System.currentTimeMillis();
+ int classesIndexedBefore = classesIndexed;
+ if (url.toString().endsWith(".jar") || url.toString().endsWith(".war"))
+ indexArchive(url.openStream());
+ else
+ indexFolder(url);
+ LOG.info("indexed " + (classesIndexed - classesIndexedBefore) + " classes in " + url
+ + " in " + (System.currentTimeMillis() - t0) + " ms");
+ } catch (IOException e) {
+ throw new RuntimeException("can't index " + url, e);
+ }
+ }
+
+ private void indexArchive(InputStream inputStream) throws IOException {
+ archivesIndexed++;
+ ZipInputStream zipInputStream = new ZipInputStream(inputStream, UTF_8);
+ while (true) {
+ ZipEntry entry = zipInputStream.getNextEntry();
+ if (entry == null)
+ break;
+ String entryName = entry.getName();
+ indexFile(entryName, zipInputStream);
+ }
+ }
+
+ private void indexFile(String fileName, InputStream inputStream) throws IOException {
+ if (fileName.endsWith(".class")) {
+ classesIndexed++;
+ indexer.index(inputStream);
+ } else if (fileName.endsWith(".war")) {
+ // necessary because of the Thorntail arquillian adapter
+ indexArchive(inputStream);
+ }
+ }
+
+ private void indexFolder(URL url) throws IOException {
+ try {
+ Path folderPath = Paths.get(url.toURI());
+ if (Files.isDirectory(folderPath)) {
+ try (Stream walk = Files.walk(folderPath)) {
+ walk.filter(Files::isRegularFile)
+ .forEach(this::indexFile);
+ }
+ }
+ } catch (URISyntaxException e) {
+ throw new RuntimeException("invalid folder url " + url, e);
+ }
+ }
+
+ private void indexFile(Path path) {
+ try {
+ String entryName = path.getFileName().toString();
+ indexFile(entryName, Files.newInputStream(path));
+ } catch (IOException e) {
+ throw new RuntimeException("can't index path " + path, e);
+ }
+ }
+
+ private static final Logger LOG = Logger.getLogger(Scanner.class.getName());
+}
diff --git a/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/Utils.java b/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/Utils.java
new file mode 100644
index 000000000..4e3d213f1
--- /dev/null
+++ b/power-annotations/scanner/src/main/java/com/github/t1/powerannotations/scanner/Utils.java
@@ -0,0 +1,109 @@
+package com.github.t1.powerannotations.scanner;
+
+import static java.util.Collections.emptySet;
+
+import java.lang.reflect.Array;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.TreeMap;
+import java.util.function.BiConsumer;
+import java.util.function.BinaryOperator;
+import java.util.function.Function;
+import java.util.function.Supplier;
+import java.util.stream.Collector;
+import java.util.stream.Stream;
+
+import org.jboss.jandex.DotName;
+
+public class Utils {
+
+ static DotName toDotName(Class> type) {
+ return toDotName(type.getName());
+ }
+
+ static DotName toDotName(String typeName) {
+ return DotName.createSimple(typeName);
+ }
+
+ static Collector, TreeMap> toTreeMap(
+ Function super T, ? extends K> keyMapper,
+ Function super T, ? extends U> valueMapper) {
+ return new Collector, TreeMap>() {
+ @Override
+ public Supplier> supplier() {
+ return TreeMap::new;
+ }
+
+ @Override
+ public BiConsumer, T> accumulator() {
+ return (map, element) -> map.put(keyMapper.apply(element), valueMapper.apply(element));
+ }
+
+ @Override
+ public BinaryOperator> combiner() {
+ return (left, right) -> {
+ left.putAll(right);
+ return left;
+ };
+ }
+
+ @Override
+ public Function, TreeMap> finisher() {
+ return Function.identity();
+ }
+
+ @Override
+ public Set characteristics() {
+ return emptySet();
+ }
+ };
+ }
+
+ public static Collector, T[]> toArray(Class componentType) {
+ return new Collector, T[]>() {
+ @Override
+ public Supplier> supplier() {
+ return ArrayList::new;
+ }
+
+ @Override
+ public BiConsumer, T> accumulator() {
+ return List::add;
+ }
+
+ @Override
+ public BinaryOperator> combiner() {
+ return (a, b) -> {
+ a.addAll(b);
+ return a;
+ };
+ }
+
+ @Override
+ public Function, T[]> finisher() {
+ return list -> {
+ @SuppressWarnings("unchecked")
+ T[] array = (T[]) Array.newInstance(componentType, 0);
+ return list.toArray(array);
+ };
+ }
+
+ @Override
+ public Set characteristics() {
+ return emptySet();
+ }
+ };
+ }
+
+ /** {@link Stream#ofNullable(Object)} is JDK 9+ */
+ static Stream streamOfNullable(T value) {
+ return (value == null) ? Stream.empty() : Stream.of(value);
+ }
+
+ /** Like JDK 9 Optional::stream
*/
+ public static Stream toStream(Optional optional) {
+ return optional.map(Stream::of).orElseGet(Stream::empty);
+ }
+}
diff --git a/power-annotations/scanner/src/test/java/com/github/t1/powerannotations/scanner/IndexerConfigBehavior.java b/power-annotations/scanner/src/test/java/com/github/t1/powerannotations/scanner/IndexerConfigBehavior.java
new file mode 100644
index 000000000..b111834ee
--- /dev/null
+++ b/power-annotations/scanner/src/test/java/com/github/t1/powerannotations/scanner/IndexerConfigBehavior.java
@@ -0,0 +1,54 @@
+package com.github.t1.powerannotations.scanner;
+
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.assertj.core.api.BDDAssertions.then;
+
+import org.junit.jupiter.api.Test;
+
+class IndexerConfigBehavior {
+ @Test
+ void shouldIgnoreUnknownResource() {
+ IndexerConfig unknown = new IndexerConfig("unknown");
+
+ then(unknown.excludes()).isEmpty();
+ }
+
+ @Test
+ void shouldLoadEmptyResource() {
+ IndexerConfig unknown = new IndexerConfig("META-INF/empty.properties");
+
+ then(unknown.excludes()).isEmpty();
+ }
+
+ @Test
+ void shouldLoadOtherPropertiesResource() {
+ IndexerConfig unknown = new IndexerConfig("META-INF/other.properties");
+
+ then(unknown.excludes()).isEmpty();
+ }
+
+ @Test
+ void shouldLoadOneExcludeConfig() {
+ IndexerConfig unknown = new IndexerConfig("META-INF/one-exclude.properties");
+
+ then(unknown.excludes()).containsExactly(".*/foo/.*/bar-.*\\.jar");
+ }
+
+ @Test
+ void shouldFailToLoadInvalidExcludeConfig() {
+ Throwable throwable = catchThrowable(() -> new IndexerConfig("META-INF/invalid-exclude.properties"));
+
+ then(throwable)
+ .hasMessage("can't parse exclude config")
+ .hasRootCauseMessage("expect `group:artifact` but found `invalid`");
+ }
+
+ @Test
+ void shouldLoadMultiExcludeConfig() {
+ IndexerConfig unknown = new IndexerConfig("META-INF/multi-exclude.properties");
+
+ then(unknown.excludes()).containsExactly(
+ ".*/foo/.*/bar-.*\\.jar",
+ ".*/baz/.*/bee-.*\\.jar");
+ }
+}
diff --git a/power-annotations/scanner/src/test/resources/META-INF/empty.properties b/power-annotations/scanner/src/test/resources/META-INF/empty.properties
new file mode 100644
index 000000000..e69de29bb
diff --git a/power-annotations/scanner/src/test/resources/META-INF/invalid-exclude.properties b/power-annotations/scanner/src/test/resources/META-INF/invalid-exclude.properties
new file mode 100644
index 000000000..b654114c1
--- /dev/null
+++ b/power-annotations/scanner/src/test/resources/META-INF/invalid-exclude.properties
@@ -0,0 +1 @@
+exclude=invalid
diff --git a/power-annotations/scanner/src/test/resources/META-INF/multi-exclude.properties b/power-annotations/scanner/src/test/resources/META-INF/multi-exclude.properties
new file mode 100644
index 000000000..287f2d33b
--- /dev/null
+++ b/power-annotations/scanner/src/test/resources/META-INF/multi-exclude.properties
@@ -0,0 +1,2 @@
+exclude=foo:bar \
+ baz:bee
diff --git a/power-annotations/scanner/src/test/resources/META-INF/one-exclude.properties b/power-annotations/scanner/src/test/resources/META-INF/one-exclude.properties
new file mode 100644
index 000000000..9f2e4ab7b
--- /dev/null
+++ b/power-annotations/scanner/src/test/resources/META-INF/one-exclude.properties
@@ -0,0 +1 @@
+exclude=foo:bar
diff --git a/power-annotations/scanner/src/test/resources/META-INF/other.properties b/power-annotations/scanner/src/test/resources/META-INF/other.properties
new file mode 100644
index 000000000..0fb7e9ef0
--- /dev/null
+++ b/power-annotations/scanner/src/test/resources/META-INF/other.properties
@@ -0,0 +1,2 @@
+# suppress inspection "UnusedProperty" for whole file
+something.else = true
diff --git a/power-annotations/tck/pom.xml b/power-annotations/tck/pom.xml
new file mode 100644
index 000000000..d1e902d51
--- /dev/null
+++ b/power-annotations/tck/pom.xml
@@ -0,0 +1,97 @@
+
+
+ 4.0.0
+
+
+ io.smallrye
+ smallrye-power-annotations-parent
+ 1.0.18-SNAPSHOT
+
+
+ power-annotations-tck
+ Power Annotations TCK
+ Test Compatibility Kit for Power Annotations, i.e. verifies that a Power Annotations implementation complies to the standard.
+
+
+
+ io.smallrye
+ power-annotations
+ ${project.version}
+
+
+
+ org.junit.jupiter
+ junit-jupiter
+ 5.7.0
+ test
+
+
+ org.assertj
+ assertj-core
+ 3.18.1
+ test
+
+
+ org.jboss
+ jandex
+ 2.2.2.Final
+ test
+
+
+ io.smallrye
+ smallrye-power-annotations-utils-api
+ ${project.version}
+ test
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-surefire-plugin
+
+ !RepeatableAnnotationsTestSuite & !InheritedAnnotationsTestSuite & !TypeToMemberAnnotationsTestSuite
+
+
+
+
+
+
+ utils-jandex
+
+ true
+
+
+
+ io.smallrye
+ smallrye-power-annotations-utils-jandex
+ ${project.version}
+ test
+
+
+
+
+ power-jandex-maven-plugin
+
+
+
+ io.smallrye
+ power-jandex-maven-plugin
+ ${project.version}
+
+
+
+ power-jandex
+
+
+
+
+
+
+
+
+
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/AnnotationValueTypeClasses.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/AnnotationValueTypeClasses.java
new file mode 100644
index 000000000..d447e41d4
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/AnnotationValueTypeClasses.java
@@ -0,0 +1,158 @@
+package com.github.t1.annotations.tck;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static java.lang.annotation.RetentionPolicy.SOURCE;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+public class AnnotationValueTypeClasses {
+ @Retention(RUNTIME)
+ public @interface DifferentValueTypesAnnotation {
+ boolean booleanValue() default false;
+
+ byte byteValue() default 0;
+
+ char charValue() default 0;
+
+ short shortValue() default 0;
+
+ int intValue() default 0;
+
+ long longValue() default 0;
+
+ float floatValue() default 0;
+
+ double doubleValue() default 0;
+
+ String stringValue() default "";
+
+ RetentionPolicy enumValue() default SOURCE;
+
+ Class> classValue() default Void.class;
+
+ SomeAnnotation annotationValue() default @SomeAnnotation("");
+
+ boolean[] booleanArrayValue() default false;
+
+ byte[] byteArrayValue() default 0;
+
+ char[] charArrayValue() default 0;
+
+ short[] shortArrayValue() default 0;
+
+ int[] intArrayValue() default 0;
+
+ long[] longArrayValue() default 0;
+
+ double[] doubleArrayValue() default 0;
+
+ float[] floatArrayValue() default 0;
+
+ String[] stringArrayValue() default "";
+
+ RetentionPolicy[] enumArrayValue() default SOURCE;
+
+ Class>[] classArrayValue() default Void.class;
+
+ SomeAnnotation[] annotationArrayValue() default @SomeAnnotation("");
+ }
+
+ @DifferentValueTypesAnnotation(booleanValue = true)
+ public static class AnnotatedWithBooleanValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(byteValue = 1)
+ public static class AnnotatedWithByteValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(charValue = 'a')
+ public static class AnnotatedWithCharValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(shortValue = 1234)
+ public static class AnnotatedWithShortValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(intValue = 42)
+ public static class AnnotatedWithIntValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(longValue = 44L)
+ public static class AnnotatedWithLongValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(floatValue = 1.2F)
+ public static class AnnotatedWithFloatValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(doubleValue = 12.34D)
+ public static class AnnotatedWithDoubleValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(stringValue = "foo")
+ public static class AnnotatedWithStringValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(enumValue = RUNTIME)
+ public static class AnnotatedWithEnumValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(classValue = String.class)
+ public static class AnnotatedWithClassValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(annotationValue = @SomeAnnotation("annotation-value"))
+ public static class AnnotatedWithAnnotationValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(booleanArrayValue = { true, false })
+ public static class AnnotatedWithBooleanArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(byteArrayValue = { 1, 2 })
+ public static class AnnotatedWithByteArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(charArrayValue = { 'a', 'b' })
+ public static class AnnotatedWithCharArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(shortArrayValue = { 1234, 1235 })
+ public static class AnnotatedWithShortArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(intArrayValue = { 42, 43 })
+ public static class AnnotatedWithIntArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(longArrayValue = { 44L, 45L })
+ public static class AnnotatedWithLongArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(floatArrayValue = { 1.2F, 1.3F })
+ public static class AnnotatedWithFloatArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(doubleArrayValue = { 12.34D, 12.35D })
+ public static class AnnotatedWithDoubleArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(stringArrayValue = { "foo", "bar" })
+ public static class AnnotatedWithStringArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(enumArrayValue = { RUNTIME, CLASS })
+ public static class AnnotatedWithEnumArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(classArrayValue = { String.class, Integer.class })
+ public static class AnnotatedWithClassArrayValueClass {
+ }
+
+ @DifferentValueTypesAnnotation(annotationArrayValue = { @SomeAnnotation("annotation-value1"),
+ @SomeAnnotation("annotation-value2") })
+ public static class AnnotatedWithAnnotationArrayValueClass {
+ }
+}
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/CombinedAnnotationClasses.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/CombinedAnnotationClasses.java
new file mode 100644
index 000000000..35659fbdb
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/CombinedAnnotationClasses.java
@@ -0,0 +1,37 @@
+package com.github.t1.annotations.tck;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import com.github.t1.annotations.Stereotype;
+
+public class CombinedAnnotationClasses {
+ @Stereotype
+ @SomeAnnotation("from-stereotype")
+ @Retention(RUNTIME)
+ public @interface SomeStereotype {
+ }
+
+ @SomeStereotype
+ public interface SomeStereotypedInterface {
+ @SuppressWarnings("unused")
+ void foo();
+ }
+
+ @SomeStereotype
+ public static class SomeStereotypedClass {
+ @SuppressWarnings("unused")
+ public void foo() {
+ }
+ }
+
+ @SomeAnnotation("from-sub-interface")
+ public interface SomeInheritingInterface extends SomeInheritedInterface {
+ }
+
+ public interface SomeInheritedInterface {
+ @SuppressWarnings("unused")
+ void foo();
+ }
+}
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/ContainingTypeClasses.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/ContainingTypeClasses.java
new file mode 100644
index 000000000..906d3ef2f
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/ContainingTypeClasses.java
@@ -0,0 +1,81 @@
+package com.github.t1.annotations.tck;
+
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+public class ContainingTypeClasses {
+
+ @Retention(RUNTIME)
+ public @interface SomeAnnotationWithoutTargetAnnotation {
+ }
+
+ @Retention(RUNTIME)
+ @Target(TYPE)
+ public @interface SomeAnnotationWithOnlyTypeTargetAnnotation {
+ }
+
+ @SomeAnnotation("class-annotation")
+ @SomeAnnotationWithoutTargetAnnotation
+ @SomeAnnotationWithOnlyTypeTargetAnnotation
+ public static class ClassWithField {
+ @SuppressWarnings("unused")
+ String someField;
+ }
+
+ @RepeatableAnnotation(1)
+ @RepeatableAnnotation(2)
+ public static class ClassWithRepeatedAnnotationsForField {
+ @SuppressWarnings("unused")
+ String someField;
+ }
+
+ @RepeatableAnnotation(2)
+ public static class ClassWithRepeatableAnnotationOnClassAndField {
+ @RepeatableAnnotation(1)
+ @SuppressWarnings("unused")
+ String someField;
+ }
+
+ @SomeAnnotation("class-annotation")
+ public static class ClassWithAnnotationsOnClassAndField {
+ @SuppressWarnings("unused")
+ @RepeatableAnnotation(1)
+ String someField;
+ }
+
+ @SomeAnnotation("class-annotation")
+ @SomeAnnotationWithoutTargetAnnotation
+ @SomeAnnotationWithOnlyTypeTargetAnnotation
+ public static class ClassWithMethod {
+ @SuppressWarnings("unused")
+ void someMethod() {
+ }
+ }
+
+ @RepeatableAnnotation(1)
+ @RepeatableAnnotation(2)
+ public static class ClassWithRepeatedAnnotationsForMethod {
+ @SuppressWarnings("unused")
+ void someMethod() {
+ }
+ }
+
+ @RepeatableAnnotation(2)
+ public static class ClassWithRepeatableAnnotationOnClassAndMethod {
+ @RepeatableAnnotation(1)
+ @SuppressWarnings("unused")
+ void someMethod() {
+ }
+ }
+
+ @SomeAnnotation("class-annotation")
+ public static class ClassWithAnnotationsOnClassAndMethod {
+ @SuppressWarnings("unused")
+ @RepeatableAnnotation(1)
+ void someMethod() {
+ }
+ }
+}
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/DirectAnnotationClasses.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/DirectAnnotationClasses.java
new file mode 100644
index 000000000..c857a8dd3
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/DirectAnnotationClasses.java
@@ -0,0 +1,55 @@
+package com.github.t1.annotations.tck;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import com.github.t1.annotations.tck.MixinClasses.AnotherAnnotation;
+
+public class DirectAnnotationClasses {
+ @Retention(RUNTIME)
+ public @interface SomeAnnotationWithDefaultValue {
+ String valueWithDefault() default "default-value";
+ }
+
+ public static class SomeUnannotatedClass {
+ }
+
+ @SomeAnnotation("class-annotation")
+ public static class SomeAnnotatedClass {
+ }
+
+ @SomeAnnotation("interface-annotation")
+ @SomeAnnotationWithDefaultValue
+ public interface SomeAnnotatedInterface {
+ }
+
+ public static class SomeClassWithUnannotatedField {
+ @SuppressWarnings("unused")
+ String foo;
+ }
+
+ public static class SomeClassWithAnnotatedField {
+ @SuppressWarnings("unused")
+ @SomeAnnotation("field-annotation")
+ private String foo;
+ }
+
+ public static class SomeClassWithUnannotatedMethod {
+ @SuppressWarnings("unused")
+ void foo(String x) {
+ }
+ }
+
+ public static class SomeClassWithAnnotatedMethod {
+ @SuppressWarnings("unused")
+ @SomeAnnotation("method-annotation")
+ void foo(String x) {
+ }
+ }
+
+ public interface SomeInterfaceWithAnnotatedMethod {
+ @SomeAnnotation("method-annotation")
+ void foo(@AnotherAnnotation String x);
+ }
+}
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/InheritedAnnotationClasses.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/InheritedAnnotationClasses.java
new file mode 100644
index 000000000..dd2e5aa3f
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/InheritedAnnotationClasses.java
@@ -0,0 +1,52 @@
+package com.github.t1.annotations.tck;
+
+public class InheritedAnnotationClasses {
+
+ @SomeAnnotation("1")
+ @RepeatableAnnotation(1)
+ public interface InheritingInterface extends Base, SideBase {
+ }
+
+ @SomeAnnotation("2")
+ @RepeatableAnnotation(2)
+ public static class InheritingClass extends Super implements Base, SideBase {
+ }
+
+ @SomeAnnotation("3")
+ @RepeatableAnnotation(3)
+ public static class Super {
+ @SomeAnnotation("4")
+ @RepeatableAnnotation(4)
+ public String field;
+
+ @SomeAnnotation("5")
+ @RepeatableAnnotation(5)
+ public String method() {
+ return null;
+ }
+ }
+
+ @SomeAnnotation("6")
+ @RepeatableAnnotation(6)
+ public interface Base extends SuperBase {
+ @SomeAnnotation("7")
+ @RepeatableAnnotation(7)
+ @Override
+ String method();
+ }
+
+ @SomeAnnotation("8")
+ @RepeatableAnnotation(8)
+ public interface SideBase {
+ }
+
+ @SomeAnnotation("9")
+ @RepeatableAnnotation(9)
+ public interface SuperBase {
+ @SomeAnnotation("10")
+ @RepeatableAnnotation(10)
+ default String method() {
+ return null;
+ }
+ }
+}
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/MixinClasses.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/MixinClasses.java
new file mode 100644
index 000000000..844dc2f93
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/MixinClasses.java
@@ -0,0 +1,275 @@
+package com.github.t1.annotations.tck;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import com.github.t1.annotations.MixinFor;
+import com.github.t1.annotations.tck.MixinClasses.TypeAnnotationMixinClasses.SomeAnnotationWithoutValue;
+
+public class MixinClasses {
+ @Retention(RUNTIME)
+ public @interface AnotherAnnotation {
+ }
+
+ public static class TypeAnnotationMixinClasses {
+ @Retention(RUNTIME)
+ public @interface SomeAnnotationWithoutValue {
+ }
+
+ @SomeAnnotationWithoutValue
+ @SomeAnnotation("to-be-replaced")
+ @RepeatableAnnotation(2)
+ public static class SomeClassWithVariousAnnotations {
+ }
+
+ @MixinFor(SomeClassWithVariousAnnotations.class)
+ @AnotherAnnotation
+ @SomeAnnotation("replacing")
+ @RepeatableAnnotation(1)
+ public static class MixinForSomeClassWithVariousAnnotations {
+ }
+
+ @SomeAnnotationTargetedByMixin
+ @RepeatableAnnotation(1)
+ public static class SomeClassWithAnnotationTargetedByMixin {
+ }
+
+ @Retention(RUNTIME)
+ @RepeatableAnnotation(3)
+ public @interface SomeAnnotationTargetedByMixin {
+ }
+
+ @MixinFor(SomeAnnotationTargetedByMixin.class)
+ @SomeAnnotation("annotation-mixin")
+ @RepeatableAnnotation(2)
+ public static class MixinForAnnotation {
+ }
+
+ @SomeAnnotationTargetedByMixin
+ @SomeAnnotation("original")
+ public static class OriginalAnnotatedTarget {
+ }
+
+ public static class TargetClassWithTwoMixins {
+ }
+
+ @MixinFor(TargetClassWithTwoMixins.class)
+ @SomeAnnotation("one")
+ static class MixinForTargetClassWithTwoMixins1 {
+ }
+
+ @MixinFor(TargetClassWithTwoMixins.class)
+ @RepeatableAnnotation(2)
+ static class MixinForTargetClassWithTwoMixins2 {
+ }
+
+ public static class TargetClassWithTwoNonRepeatableMixins {
+ }
+
+ @MixinFor(TargetClassWithTwoNonRepeatableMixins.class)
+ @SomeAnnotation("one")
+ static class MixinForTargetClassWithTwoNonRepeatableMixins1 {
+ }
+
+ @MixinFor(TargetClassWithTwoNonRepeatableMixins.class)
+ @SomeAnnotation("one")
+ static class MixinForTargetClassWithTwoNonRepeatableMixins2 {
+ }
+
+ public static class TargetClassWithTwoRepeatableMixins {
+ }
+
+ @MixinFor(TargetClassWithTwoRepeatableMixins.class)
+ @RepeatableAnnotation(1)
+ static class MixinForTargetClassWithTwoRepeatableMixins1 {
+ }
+
+ @MixinFor(TargetClassWithTwoRepeatableMixins.class)
+ @RepeatableAnnotation(2)
+ static class MixinForTargetClassWithTwoRepeatableMixins2 {
+ }
+ }
+
+ public static class FieldAnnotationMixinClasses {
+ public static class SomeClassWithFieldWithVariousAnnotations {
+ @SuppressWarnings("unused")
+ @SomeAnnotationWithoutValue
+ @SomeAnnotation("to-be-replaced")
+ @RepeatableAnnotation(2)
+ String foo;
+
+ @SuppressWarnings("unused")
+ String bar;
+ }
+
+ @MixinFor(SomeClassWithFieldWithVariousAnnotations.class)
+ public static class MixinForSomeClassWithFieldWithVariousAnnotations {
+ @SuppressWarnings("unused")
+ @AnotherAnnotation
+ @SomeAnnotation("replacing")
+ @RepeatableAnnotation(1)
+ String foo;
+ }
+
+ public static class TargetFieldClassWithTwoMixins {
+ @SuppressWarnings("unused")
+ String foo;
+ }
+
+ @MixinFor(TargetFieldClassWithTwoMixins.class)
+ static class MixinForTargetFieldClassWithTwoMixins1 {
+ @SomeAnnotation("one")
+ @SuppressWarnings("unused")
+ String foo;
+ }
+
+ @MixinFor(TargetFieldClassWithTwoMixins.class)
+ static class MixinForTargetFieldClassWithTwoMixins2 {
+ @RepeatableAnnotation(2)
+ @SuppressWarnings("unused")
+ String foo;
+ }
+
+ public static class TargetFieldClassWithTwoNonRepeatableMixins {
+ @SuppressWarnings("unused")
+ String foo;
+ }
+
+ @MixinFor(TargetFieldClassWithTwoNonRepeatableMixins.class)
+ static class MixinForTargetFieldClassWithTwoNonRepeatableMixins1 {
+ @SomeAnnotation("one")
+ @SuppressWarnings("unused")
+ String foo;
+ }
+
+ @MixinFor(TargetFieldClassWithTwoNonRepeatableMixins.class)
+ static class MixinForTargetFieldClassWithTwoNonRepeatableMixins2 {
+ @SomeAnnotation("one")
+ @SuppressWarnings("unused")
+ String foo;
+ }
+
+ public static class TargetFieldClassWithTwoRepeatableMixins {
+ @SuppressWarnings("unused")
+ String foo;
+ }
+
+ @MixinFor(TargetFieldClassWithTwoRepeatableMixins.class)
+ static class MixinForTargetFieldClassWithTwoRepeatableMixins1 {
+ @RepeatableAnnotation(1)
+ @SuppressWarnings("unused")
+ String foo;
+ }
+
+ @MixinFor(TargetFieldClassWithTwoRepeatableMixins.class)
+ static class MixinForTargetFieldClassWithTwoRepeatableMixins2 {
+ @RepeatableAnnotation(2)
+ @SuppressWarnings("unused")
+ String foo;
+ }
+ }
+
+ public static class MethodAnnotationMixinClasses {
+ public static class SomeClassWithMethodWithVariousAnnotations {
+ @SuppressWarnings("unused")
+ @SomeAnnotationWithoutValue
+ @SomeAnnotation("to-be-replaced")
+ @RepeatableAnnotation(2)
+ String foo() {
+ return "foo";
+ }
+
+ @SuppressWarnings("unused")
+ String bar() {
+ return "bar";
+ }
+ }
+
+ @MixinFor(SomeClassWithMethodWithVariousAnnotations.class)
+ public static class MixinForSomeClassWithMethodWithVariousAnnotations {
+ @SuppressWarnings("unused")
+ @AnotherAnnotation
+ @SomeAnnotation("replacing")
+ @RepeatableAnnotation(1)
+ String foo() {
+ return "foo";
+ }
+ }
+
+ public static class TargetMethodClassWithTwoMixins {
+ @SuppressWarnings("unused")
+ String foo() {
+ return "foo";
+ }
+ }
+
+ @MixinFor(TargetMethodClassWithTwoMixins.class)
+ static class MixinForTargetMethodClassWithTwoMixins1 {
+ @SomeAnnotation("one")
+ @SuppressWarnings("unused")
+ String foo() {
+ return "foo";
+ }
+ }
+
+ @MixinFor(TargetMethodClassWithTwoMixins.class)
+ static class MixinForTargetMethodClassWithTwoMixins2 {
+ @RepeatableAnnotation(2)
+ @SuppressWarnings("unused")
+ String foo() {
+ return "foo";
+ }
+ }
+
+ public static class TargetMethodClassWithTwoNonRepeatableMixins {
+ @SuppressWarnings("unused")
+ String foo() {
+ return "foo";
+ }
+ }
+
+ @MixinFor(TargetMethodClassWithTwoNonRepeatableMixins.class)
+ static class MixinForTargetMethodClassWithTwoNonRepeatableMixins1 {
+ @SomeAnnotation("one")
+ @SuppressWarnings("unused")
+ String foo() {
+ return "foo";
+ }
+ }
+
+ @MixinFor(TargetMethodClassWithTwoNonRepeatableMixins.class)
+ static class MixinTargetMethodClassWithTwoNonRepeatableMixins2 {
+ @SomeAnnotation("one")
+ @SuppressWarnings("unused")
+ String foo() {
+ return "foo";
+ }
+ }
+
+ public static class TargetMethodClassWithTwoRepeatableMixins {
+ @SuppressWarnings("unused")
+ String foo() {
+ return "foo";
+ }
+ }
+
+ @MixinFor(TargetMethodClassWithTwoRepeatableMixins.class)
+ static class MixinForTargetMethodClassWithTwoRepeatableMixins1 {
+ @RepeatableAnnotation(1)
+ @SuppressWarnings("unused")
+ String foo() {
+ return "foo";
+ }
+ }
+
+ @MixinFor(TargetMethodClassWithTwoRepeatableMixins.class)
+ static class MixinForTargetMethodClassWithTwoRepeatableMixins2 {
+ @RepeatableAnnotation(2)
+ @SuppressWarnings("unused")
+ String foo() {
+ return "foo";
+ }
+ }
+ }
+}
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/RepeatableAnnotation.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/RepeatableAnnotation.java
new file mode 100644
index 000000000..b78453193
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/RepeatableAnnotation.java
@@ -0,0 +1,26 @@
+package com.github.t1.annotations.tck;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Repeatable;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import com.github.t1.annotations.tck.RepeatableAnnotation.RepeatableAnnotations;
+
+@Retention(RUNTIME)
+@Target({ TYPE, FIELD, METHOD })
+@Repeatable(RepeatableAnnotations.class)
+public @interface RepeatableAnnotation {
+ int value();
+
+ @Target({ TYPE, FIELD, METHOD })
+ @Retention(RUNTIME)
+ @interface RepeatableAnnotations {
+ @SuppressWarnings("unused")
+ RepeatableAnnotation[] value();
+ }
+}
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/RepeatableAnnotationClasses.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/RepeatableAnnotationClasses.java
new file mode 100644
index 000000000..c6c67f492
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/RepeatableAnnotationClasses.java
@@ -0,0 +1,12 @@
+package com.github.t1.annotations.tck;
+
+public class RepeatableAnnotationClasses {
+ @RepeatableAnnotation(1)
+ public static class UnrepeatedAnnotationClass {
+ }
+
+ @RepeatableAnnotation(1)
+ @RepeatableAnnotation(2)
+ public static class RepeatedAnnotationClass {
+ }
+}
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/SomeAnnotation.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/SomeAnnotation.java
new file mode 100644
index 000000000..3c47b99d5
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/SomeAnnotation.java
@@ -0,0 +1,15 @@
+package com.github.t1.annotations.tck;
+
+import static java.lang.annotation.ElementType.FIELD;
+import static java.lang.annotation.ElementType.METHOD;
+import static java.lang.annotation.ElementType.TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+@Retention(RUNTIME)
+@Target({ TYPE, FIELD, METHOD })
+public @interface SomeAnnotation {
+ String value();
+}
diff --git a/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/StereotypeClasses.java b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/StereotypeClasses.java
new file mode 100644
index 000000000..9c9e9758b
--- /dev/null
+++ b/power-annotations/tck/src/main/java/com/github/t1/annotations/tck/StereotypeClasses.java
@@ -0,0 +1,100 @@
+package com.github.t1.annotations.tck;
+
+import static java.lang.annotation.ElementType.ANNOTATION_TYPE;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+import com.github.t1.annotations.Stereotype;
+
+public class StereotypeClasses {
+ @Retention(RUNTIME)
+ @Target(ANNOTATION_TYPE) // i.e. not on TYPE
+ public @interface SomeMetaAnnotation {
+ }
+
+ @Stereotype
+ @Retention(RUNTIME)
+ @SomeAnnotation("some-stereotype")
+ @RepeatableAnnotation(1)
+ @RepeatableAnnotation(2)
+ @SomeMetaAnnotation
+ public @interface SomeStereotype {
+ }
+
+ @Stereotype
+ @Retention(RUNTIME)
+ @SomeAnnotation("another-stereotype")
+ @RepeatableAnnotation(3)
+ @RepeatableAnnotation(4)
+ public @interface AnotherStereotype {
+ }
+
+ @Stereotype
+ @Retention(RUNTIME)
+ @SomeStereotype
+ public @interface SomeIndirectedStereotype {
+ }
+
+ @Stereotype
+ @Retention(RUNTIME)
+ @SomeStereotype
+ public @interface SomeTardyIndirectedStereotype {
+ }
+
+ @Stereotype
+ @Retention(RUNTIME)
+ @SomeIndirectedStereotype
+ public @interface SomeDoubleIndirectedStereotype {
+ }
+
+ @SomeStereotype
+ @RepeatableAnnotation(5)
+ public static class StereotypedClass {
+ }
+
+ @SomeStereotype
+ @SomeAnnotation("on-class")
+ public static class StereotypedClassWithSomeAnnotation {
+ }
+
+ @SomeIndirectedStereotype
+ public static class IndirectlyStereotypedClass {
+ }
+
+ @SomeTardyIndirectedStereotype
+ public static class TardyIndirectlyStereotypedClass {
+ }
+
+ @SomeDoubleIndirectedStereotype
+ public static class DoubleIndirectlyStereotypedClass {
+ }
+
+ @SomeStereotype
+ @AnotherStereotype
+ @RepeatableAnnotation(6)
+ public static class DoubleStereotypedClass {
+ }
+
+ @SuppressWarnings("unused")
+ public static class ClassWithStereotypedField {
+ @SomeStereotype
+ @RepeatableAnnotation(7)
+ String foo;
+ boolean bar;
+ }
+
+ @SuppressWarnings("unused")
+ public static class ClassWithStereotypedMethod {
+ @SomeStereotype
+ @RepeatableAnnotation(7)
+ String foo() {
+ return "foo";
+ }
+
+ String bar() {
+ return "bar";
+ }
+ }
+}
diff --git a/power-annotations/tck/src/test/java/test/AnnotationValueTypeBehavior.java b/power-annotations/tck/src/test/java/test/AnnotationValueTypeBehavior.java
new file mode 100644
index 000000000..21278c442
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/AnnotationValueTypeBehavior.java
@@ -0,0 +1,224 @@
+package test;
+
+import static java.lang.annotation.RetentionPolicy.CLASS;
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.github.t1.annotations.Annotations;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithAnnotationArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithAnnotationValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithBooleanArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithBooleanValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithByteArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithByteValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithCharArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithCharValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithClassArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithClassValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithDoubleArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithDoubleValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithEnumArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithEnumValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithFloatArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithFloatValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithIntArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithIntValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithLongArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithLongValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithShortArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithShortValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithStringArrayValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.AnnotatedWithStringValueClass;
+import com.github.t1.annotations.tck.AnnotationValueTypeClasses.DifferentValueTypesAnnotation;
+import com.github.t1.annotations.tck.SomeAnnotation;
+
+@Nested
+class AnnotationValueTypeBehavior {
+
+ @Test
+ void shouldGetBooleanAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithBooleanValueClass.class);
+
+ then(annotation.booleanValue()).isTrue();
+ }
+
+ @Test
+ void shouldGetByteAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithByteValueClass.class);
+
+ then(annotation.byteValue()).isEqualTo((byte) 1);
+ }
+
+ @Test
+ void shouldGetCharAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithCharValueClass.class);
+
+ then(annotation.charValue()).isEqualTo('a');
+ }
+
+ @Test
+ void shouldGetShortAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithShortValueClass.class);
+
+ then(annotation.shortValue()).isEqualTo((short) 1234);
+ }
+
+ @Test
+ void shouldGetIntAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithIntValueClass.class);
+
+ then(annotation.intValue()).isEqualTo(42);
+ }
+
+ @Test
+ void shouldGetLongAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithLongValueClass.class);
+
+ then(annotation.longValue()).isEqualTo(44L);
+ }
+
+ @Test
+ void shouldGetFloatAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithFloatValueClass.class);
+
+ then(annotation.floatValue()).isEqualTo(1.2F);
+ }
+
+ @Test
+ void shouldGetDoubleAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithDoubleValueClass.class);
+
+ then(annotation.doubleValue()).isEqualTo(12.34);
+ }
+
+ @Test
+ void shouldGetStringAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithStringValueClass.class);
+
+ then(annotation.stringValue()).isEqualTo("foo");
+ }
+
+ @Test
+ void shouldGetEnumAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithEnumValueClass.class);
+
+ then(annotation.enumValue()).isEqualTo(RUNTIME);
+ }
+
+ @Test
+ void shouldGetClassAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithClassValueClass.class);
+
+ then(annotation.classValue()).isEqualTo(String.class);
+ }
+
+ @Test
+ void shouldGetAnnotationAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithAnnotationValueClass.class);
+
+ then(annotation.annotationValue().value()).isEqualTo("annotation-value");
+ }
+
+ @Test
+ void shouldGetBooleanArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithBooleanArrayValueClass.class);
+
+ then(annotation.booleanArrayValue()).contains(true, false);
+ }
+
+ @Test
+ void shouldGetByteArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithByteArrayValueClass.class);
+
+ then(annotation.byteArrayValue()).containsExactly((byte) 1, (byte) 2);
+ }
+
+ @Test
+ void shouldGetCharArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithCharArrayValueClass.class);
+
+ then(annotation.charArrayValue()).containsExactly('a', 'b');
+ }
+
+ @Test
+ void shouldGetShortArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithShortArrayValueClass.class);
+
+ then(annotation.shortArrayValue()).containsExactly((short) 1234, (short) 1235);
+ }
+
+ @Test
+ void shouldGetIntArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithIntArrayValueClass.class);
+
+ then(annotation.intArrayValue()).containsExactly(42, 43);
+ }
+
+ @Test
+ void shouldGetLongArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithLongArrayValueClass.class);
+
+ then(annotation.longArrayValue()).containsExactly(44L, 45L);
+ }
+
+ @Test
+ void shouldGetFloatArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithFloatArrayValueClass.class);
+
+ then(annotation.floatArrayValue()).containsExactly(1.2F, 1.3F);
+ }
+
+ @Test
+ void shouldGetDoubleArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithDoubleArrayValueClass.class);
+
+ then(annotation.doubleArrayValue()).containsExactly(12.34, 12.35);
+ }
+
+ @Test
+ void shouldGetStringArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithStringArrayValueClass.class);
+
+ then(annotation.stringArrayValue()).containsExactly("foo", "bar");
+ }
+
+ @Test
+ void shouldGetEnumArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithEnumArrayValueClass.class);
+
+ then(annotation.enumArrayValue()).containsExactly(RUNTIME, CLASS);
+ }
+
+ @Test
+ void shouldGetClassArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(AnnotatedWithClassArrayValueClass.class);
+
+ then(annotation.classArrayValue()).containsExactly(String.class, Integer.class);
+ }
+
+ @Test
+ void shouldGetAnnotationArrayAnnotationValue() {
+ DifferentValueTypesAnnotation annotation = getDifferentValueTypesAnnotation(
+ AnnotatedWithAnnotationArrayValueClass.class);
+
+ then(Stream.of(annotation.annotationArrayValue()).map(SomeAnnotation::value))
+ .containsExactly("annotation-value1", "annotation-value2");
+ }
+
+ private DifferentValueTypesAnnotation getDifferentValueTypesAnnotation(Class> type) {
+ Annotations annotations = Annotations.on(type);
+
+ Optional annotation = annotations.get(DifferentValueTypesAnnotation.class);
+
+ assert annotation.isPresent();
+ DifferentValueTypesAnnotation someAnnotation = annotation.get();
+ then(someAnnotation.annotationType()).isEqualTo(DifferentValueTypesAnnotation.class);
+ return someAnnotation;
+ }
+}
diff --git a/power-annotations/tck/src/test/java/test/CombinedBehavior.java b/power-annotations/tck/src/test/java/test/CombinedBehavior.java
new file mode 100644
index 000000000..84e4becef
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/CombinedBehavior.java
@@ -0,0 +1,47 @@
+package test;
+
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.lang.annotation.Annotation;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.t1.annotations.Annotations;
+import com.github.t1.annotations.tck.CombinedAnnotationClasses.SomeInheritingInterface;
+import com.github.t1.annotations.tck.CombinedAnnotationClasses.SomeStereotypedClass;
+import com.github.t1.annotations.tck.CombinedAnnotationClasses.SomeStereotypedInterface;
+import com.github.t1.annotations.tck.SomeAnnotation;
+
+@TypeToMemberAnnotationsTestSuite
+public class CombinedBehavior {
+ @Test
+ void shouldResolveInterfaceStereotypesBeforeTypeToMember() {
+ Annotations fooAnnotations = Annotations.onMethod(SomeStereotypedInterface.class, "foo");
+
+ Stream all = fooAnnotations.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"from-stereotype\")");
+ }
+
+ @Test
+ void shouldResolveClassStereotypesBeforeTypeToMember() {
+ Annotations fooAnnotations = Annotations.onMethod(SomeStereotypedClass.class, "foo");
+
+ Stream all = fooAnnotations.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"from-stereotype\")");
+ }
+
+ @Test
+ void shouldResolveInterfaceInheritedBeforeTypeToMember() {
+ Annotations fooAnnotations = Annotations.onMethod(SomeInheritingInterface.class, "foo");
+
+ Stream all = fooAnnotations.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"from-sub-interface\")");
+ }
+}
diff --git a/power-annotations/tck/src/test/java/test/ContainingTypeBehavior.java b/power-annotations/tck/src/test/java/test/ContainingTypeBehavior.java
new file mode 100644
index 000000000..7810b9891
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/ContainingTypeBehavior.java
@@ -0,0 +1,159 @@
+package test;
+
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.lang.annotation.Annotation;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.github.t1.annotations.Annotations;
+import com.github.t1.annotations.tck.ContainingTypeClasses.ClassWithAnnotationsOnClassAndField;
+import com.github.t1.annotations.tck.ContainingTypeClasses.ClassWithAnnotationsOnClassAndMethod;
+import com.github.t1.annotations.tck.ContainingTypeClasses.ClassWithField;
+import com.github.t1.annotations.tck.ContainingTypeClasses.ClassWithMethod;
+import com.github.t1.annotations.tck.ContainingTypeClasses.ClassWithRepeatableAnnotationOnClassAndField;
+import com.github.t1.annotations.tck.ContainingTypeClasses.ClassWithRepeatableAnnotationOnClassAndMethod;
+import com.github.t1.annotations.tck.ContainingTypeClasses.ClassWithRepeatedAnnotationsForField;
+import com.github.t1.annotations.tck.ContainingTypeClasses.ClassWithRepeatedAnnotationsForMethod;
+import com.github.t1.annotations.tck.ContainingTypeClasses.SomeAnnotationWithOnlyTypeTargetAnnotation;
+import com.github.t1.annotations.tck.ContainingTypeClasses.SomeAnnotationWithoutTargetAnnotation;
+import com.github.t1.annotations.tck.RepeatableAnnotation;
+import com.github.t1.annotations.tck.SomeAnnotation;
+
+@TypeToMemberAnnotationsTestSuite
+public class ContainingTypeBehavior {
+ @Nested
+ class FieldAnnotations {
+ Annotations fieldAnnotations = Annotations.onField(ClassWithField.class, "someField");
+
+ @Test
+ void shouldGetFieldAnnotationFromClass() {
+ Optional annotation = fieldAnnotations.get(SomeAnnotation.class);
+
+ assert annotation.isPresent();
+ then(annotation.get().value()).isEqualTo("class-annotation");
+ }
+
+ @Test
+ void shouldNotGetFieldAnnotationWithoutTargetAnnotationFromClass() {
+ Optional annotation = fieldAnnotations
+ .get(SomeAnnotationWithoutTargetAnnotation.class);
+
+ then(annotation).isEmpty();
+ }
+
+ @Test
+ void shouldNotGetFieldAnnotationWithOnlyTypeTargetAnnotationFromClass() {
+ Optional annotation = fieldAnnotations
+ .get(SomeAnnotationWithOnlyTypeTargetAnnotation.class);
+
+ then(annotation).isEmpty();
+ }
+
+ @Test
+ void shouldNotGetAllFieldAnnotationFromClass() {
+ Optional annotation = fieldAnnotations.get(SomeAnnotation.class);
+
+ assert annotation.isPresent();
+ then(annotation.get().value()).isEqualTo("class-annotation");
+ }
+
+ @Test
+ void shouldGetRepeatableFieldAnnotationFromClass() {
+ Annotations annotations = Annotations.onField(ClassWithRepeatedAnnotationsForField.class, "someField");
+
+ Stream annotation = annotations.all(RepeatableAnnotation.class);
+
+ then(annotation.map(RepeatableAnnotation::value)).containsExactly(1, 2);
+ }
+
+ @Test
+ void shouldGetMoreRepeatableFieldAnnotationsFromClass() {
+ Annotations annotations = Annotations.onField(ClassWithRepeatableAnnotationOnClassAndField.class, "someField");
+
+ Stream annotation = annotations.all(RepeatableAnnotation.class);
+
+ then(annotation.map(RepeatableAnnotation::value)).containsExactly(1, 2);
+ }
+
+ @Test
+ void shouldOnlyGetAllFieldAnnotationAndNotFromClass() {
+ Annotations annotations = Annotations.onField(ClassWithAnnotationsOnClassAndField.class, "someField");
+
+ Stream list = annotations.all();
+
+ then(list.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + SomeAnnotation.class.getName() + "(value = \"class-annotation\")");
+ }
+ }
+
+ @Nested
+ class MethodAnnotations {
+ Annotations methodAnnotations = Annotations.onMethod(ClassWithMethod.class, "someMethod");
+
+ @Test
+ void shouldGetMethodAnnotationFromClass() {
+ Optional annotation = methodAnnotations.get(SomeAnnotation.class);
+
+ assert annotation.isPresent();
+ then(annotation.get().value()).isEqualTo("class-annotation");
+ }
+
+ @Test
+ void shouldNotGetMethodAnnotationWithoutTargetAnnotationFromClass() {
+ Optional annotation = methodAnnotations
+ .get(SomeAnnotationWithoutTargetAnnotation.class);
+
+ then(annotation).isEmpty();
+ }
+
+ @Test
+ void shouldNotGetMethodAnnotationWithOnlyTypeTargetAnnotationFromClass() {
+ Optional annotation = methodAnnotations
+ .get(SomeAnnotationWithOnlyTypeTargetAnnotation.class);
+
+ then(annotation).isEmpty();
+ }
+
+ @Test
+ void shouldNotGetAllMethodAnnotationFromClass() {
+ Optional annotation = methodAnnotations.get(SomeAnnotation.class);
+
+ assert annotation.isPresent();
+ then(annotation.get().value()).isEqualTo("class-annotation");
+ }
+
+ @Test
+ void shouldGetRepeatableMethodAnnotationFromClass() {
+ Annotations annotations = Annotations.onMethod(ClassWithRepeatedAnnotationsForMethod.class, "someMethod");
+
+ Stream annotation = annotations.all(RepeatableAnnotation.class);
+
+ then(annotation.map(RepeatableAnnotation::value)).containsExactly(1, 2);
+ }
+
+ @Test
+ void shouldGetMoreRepeatableMethodAnnotationsFromClass() {
+ Annotations annotations = Annotations.onMethod(ClassWithRepeatableAnnotationOnClassAndMethod.class, "someMethod");
+
+ Stream annotation = annotations.all(RepeatableAnnotation.class);
+
+ then(annotation.map(RepeatableAnnotation::value)).containsExactly(1, 2);
+ }
+
+ @Test
+ void shouldGetAllMethodAndClassAnnotations() {
+ Annotations annotations = Annotations.onMethod(ClassWithAnnotationsOnClassAndMethod.class, "someMethod");
+
+ Stream list = annotations.all();
+
+ then(list.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + SomeAnnotation.class.getName() + "(value = \"class-annotation\")");
+ }
+ }
+}
diff --git a/power-annotations/tck/src/test/java/test/DirectAnnotationsBehavior.java b/power-annotations/tck/src/test/java/test/DirectAnnotationsBehavior.java
new file mode 100644
index 000000000..0b1383765
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/DirectAnnotationsBehavior.java
@@ -0,0 +1,206 @@
+package test;
+
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Method;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.github.t1.annotations.Annotations;
+import com.github.t1.annotations.tck.DirectAnnotationClasses.SomeAnnotatedClass;
+import com.github.t1.annotations.tck.DirectAnnotationClasses.SomeAnnotatedInterface;
+import com.github.t1.annotations.tck.DirectAnnotationClasses.SomeAnnotationWithDefaultValue;
+import com.github.t1.annotations.tck.DirectAnnotationClasses.SomeClassWithAnnotatedField;
+import com.github.t1.annotations.tck.DirectAnnotationClasses.SomeClassWithAnnotatedMethod;
+import com.github.t1.annotations.tck.DirectAnnotationClasses.SomeClassWithUnannotatedField;
+import com.github.t1.annotations.tck.DirectAnnotationClasses.SomeClassWithUnannotatedMethod;
+import com.github.t1.annotations.tck.DirectAnnotationClasses.SomeInterfaceWithAnnotatedMethod;
+import com.github.t1.annotations.tck.DirectAnnotationClasses.SomeUnannotatedClass;
+import com.github.t1.annotations.tck.MixinClasses.AnotherAnnotation;
+import com.github.t1.annotations.tck.SomeAnnotation;
+
+public class DirectAnnotationsBehavior {
+
+ @Nested
+ class ClassAnnotations {
+ @Test
+ void shouldGetNoClassAnnotation() {
+ Annotations annotations = Annotations.on(SomeUnannotatedClass.class);
+
+ thenEmpty(annotations);
+ }
+
+ @Test
+ void shouldGetClassAnnotation() {
+ Annotations annotations = Annotations.on(SomeAnnotatedClass.class);
+
+ Optional annotation = annotations.get(SomeAnnotation.class);
+
+ thenIsSomeAnnotation(annotation, "class-annotation");
+ }
+
+ @Test
+ void shouldGetInterfaceAnnotation() {
+ Annotations annotations = Annotations.on(SomeAnnotatedInterface.class);
+
+ Optional annotation = annotations.get(SomeAnnotation.class);
+
+ thenIsSomeAnnotation(annotation, "interface-annotation");
+ }
+
+ @Test
+ void shouldGetDefaultValueOfClassAnnotation() {
+ Annotations annotations = Annotations.on(SomeAnnotatedInterface.class);
+
+ Optional annotation = annotations.get(SomeAnnotationWithDefaultValue.class);
+
+ assert annotation.isPresent();
+ SomeAnnotationWithDefaultValue someAnnotation = annotation.get();
+ then(someAnnotation.annotationType()).isEqualTo(SomeAnnotationWithDefaultValue.class);
+ then(someAnnotation.valueWithDefault()).isEqualTo("default-value");
+ then(someAnnotation).isNotSameAs(SomeAnnotatedInterface.class.getAnnotation(SomeAnnotation.class));
+ }
+
+ @Test
+ void shouldGetAllTypeAnnotations() {
+ Annotations annotations = Annotations.on(SomeAnnotatedInterface.class);
+
+ Stream list = annotations.all();
+
+ then(list.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotationWithDefaultValue.class.getName(),
+ "@" + SomeAnnotation.class.getName() + "(value = \"interface-annotation\")");
+ }
+ }
+
+ @Nested
+ class FieldAnnotations {
+ @Test
+ void shouldGetNoFieldAnnotation() {
+ Annotations annotations = Annotations.onField(SomeClassWithUnannotatedField.class, "foo");
+
+ thenEmpty(annotations);
+ }
+
+ @Test
+ void shouldFailToGetUnknownFieldAnnotation() {
+ Throwable throwable = catchThrowable(() -> Annotations.onField(SomeClassWithAnnotatedField.class, "bar"));
+
+ then(throwable)
+ .isInstanceOf(RuntimeException.class)
+ .hasMessage("no field 'bar' in " + SomeClassWithAnnotatedField.class); // implementation detail?
+ }
+
+ @Test
+ void shouldGetFieldAnnotation() {
+ Annotations annotations = Annotations.onField(SomeClassWithAnnotatedField.class, "foo");
+
+ Optional annotation = annotations.get(SomeAnnotation.class);
+
+ thenIsSomeAnnotation(annotation, "field-annotation");
+ }
+ }
+
+ @Nested
+ class MethodAnnotations {
+ @Test
+ void shouldGetNoMethodAnnotation() {
+ Annotations annotations = Annotations.onMethod(SomeClassWithUnannotatedMethod.class, "foo", String.class);
+
+ thenEmpty(annotations);
+ }
+
+ @Test
+ void shouldGetMethodAnnotation() {
+ Annotations annotations = Annotations.onMethod(SomeClassWithAnnotatedMethod.class, "foo", String.class);
+
+ Optional annotation = annotations.get(SomeAnnotation.class);
+
+ thenIsSomeAnnotation(annotation, "method-annotation");
+ }
+
+ @Test
+ void shouldGetInterfaceMethodAnnotation() {
+ Annotations annotations = Annotations.onMethod(SomeInterfaceWithAnnotatedMethod.class, "foo", String.class);
+
+ Optional annotation = annotations.get(SomeAnnotation.class);
+
+ thenIsSomeAnnotation(annotation, "method-annotation");
+ }
+
+ @Test
+ void shouldGetAllMethodAnnotations() throws NoSuchMethodException {
+ Annotations annotations = Annotations.onMethod(SomeInterfaceWithAnnotatedMethod.class, "foo", String.class);
+
+ Stream all = annotations.all();
+
+ // the parameter annotation must be there, but not represented as method annotation
+ then(fooMethod().getParameterAnnotations()[0][0].toString())
+ .startsWith("@" + AnotherAnnotation.class.getName()); // `since` and `forRemoval` are JDK 9+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"method-annotation\")");
+ }
+
+ private Method fooMethod() throws NoSuchMethodException {
+ return SomeInterfaceWithAnnotatedMethod.class.getDeclaredMethod("foo", String.class);
+ }
+
+ @Test
+ void shouldFailToGetAnnotationsFromUnknownMethodName() {
+ Throwable throwable = catchThrowable(
+ () -> Annotations.onMethod(SomeClassWithAnnotatedMethod.class, "bar", String.class));
+
+ then(throwable)
+ .isInstanceOf(RuntimeException.class)
+ .hasMessage("no method bar(java.lang.String) in " + SomeClassWithAnnotatedMethod.class); // implementation detail?
+ }
+
+ @Test
+ void shouldFailToGetAnnotationsFromMethodWithTooManyArguments() {
+ Throwable throwable = catchThrowable(() -> Annotations.onMethod(SomeClassWithAnnotatedMethod.class, "foo"));
+
+ then(throwable)
+ .isInstanceOf(RuntimeException.class)
+ .hasMessage("no method foo() in " + SomeClassWithAnnotatedMethod.class); // implementation detail?
+ }
+
+ @Test
+ void shouldFailToGetAnnotationsFromMethodWithTooFewArguments() {
+ Throwable throwable = catchThrowable(
+ () -> Annotations.onMethod(SomeClassWithAnnotatedMethod.class, "foo", String.class, int.class));
+
+ then(throwable)
+ .isInstanceOf(RuntimeException.class)
+ .hasMessage("no method foo(java.lang.String, int) in " + SomeClassWithAnnotatedMethod.class); // implementation detail?
+ }
+
+ @Test
+ void shouldFailToGetAnnotationsFromMethodWithWrongArgumentType() {
+ Throwable throwable = catchThrowable(
+ () -> Annotations.onMethod(SomeClassWithAnnotatedMethod.class, "foo", int.class));
+
+ then(throwable)
+ .isInstanceOf(RuntimeException.class)
+ .hasMessage("no method foo(int) in " + SomeClassWithAnnotatedMethod.class); // implementation detail?
+ }
+ }
+
+ void thenEmpty(Annotations annotations) {
+ then(annotations.all()).isEmpty();
+ then(annotations.get(SomeAnnotation.class)).isEmpty();
+ }
+
+ void thenIsSomeAnnotation(
+ @SuppressWarnings("OptionalUsedAsFieldOrParameterType") Optional annotation,
+ String expectedValue) {
+ assert annotation.isPresent();
+ SomeAnnotation someAnnotation = annotation.get();
+ then(someAnnotation.annotationType()).isEqualTo(SomeAnnotation.class);
+ then(someAnnotation.value()).isEqualTo(expectedValue);
+ }
+}
diff --git a/power-annotations/tck/src/test/java/test/InheritedAnnotationsTestSuite.java b/power-annotations/tck/src/test/java/test/InheritedAnnotationsTestSuite.java
new file mode 100644
index 000000000..e77a3c187
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/InheritedAnnotationsTestSuite.java
@@ -0,0 +1,12 @@
+package test;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import org.junit.jupiter.api.Tag;
+
+@Tag("InheritedAnnotationsTestSuite")
+@Retention(RUNTIME)
+public @interface InheritedAnnotationsTestSuite {
+}
diff --git a/power-annotations/tck/src/test/java/test/InheritedBehavior.java b/power-annotations/tck/src/test/java/test/InheritedBehavior.java
new file mode 100644
index 000000000..654074dcf
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/InheritedBehavior.java
@@ -0,0 +1,95 @@
+package test;
+
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.lang.annotation.Annotation;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.t1.annotations.Annotations;
+import com.github.t1.annotations.tck.InheritedAnnotationClasses.InheritingClass;
+import com.github.t1.annotations.tck.InheritedAnnotationClasses.InheritingInterface;
+import com.github.t1.annotations.tck.RepeatableAnnotation;
+import com.github.t1.annotations.tck.SomeAnnotation;
+
+@InheritedAnnotationsTestSuite
+public class InheritedBehavior {
+
+ @Test
+ void shouldGetAllOnInterface() {
+ Annotations annotations = Annotations.on(InheritingInterface.class);
+
+ Stream all = annotations.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"1\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 6)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 8)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 9)");
+ }
+
+ @Test
+ void shouldGetAllOnInterfaceMethod() {
+ Annotations annotations = Annotations.onMethod(InheritingInterface.class, "method");
+
+ Stream all = annotations.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"10\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 6)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 8)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 9)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 10)");
+ }
+
+ @Test
+ void shouldGetAllOnClass() {
+ Annotations annotations = Annotations.on(InheritingClass.class);
+
+ Stream all = annotations.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"2\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 3)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 6)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 8)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 9)");
+ }
+
+ @Test
+ void shouldGetAllOnField() {
+ Annotations annotations = Annotations.onField(InheritingClass.class, "field");
+
+ Stream all = annotations.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"4\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 3)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 4)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 6)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 8)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 9)");
+ }
+
+ @Test
+ void shouldGetAllOnClassMethod() {
+ Annotations annotations = Annotations.onMethod(InheritingClass.class, "method");
+
+ Stream all = annotations.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"5\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 3)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 5)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 6)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 8)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 10)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 9)");
+ }
+}
diff --git a/power-annotations/tck/src/test/java/test/MixinBehavior.java b/power-annotations/tck/src/test/java/test/MixinBehavior.java
new file mode 100644
index 000000000..c270e108c
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/MixinBehavior.java
@@ -0,0 +1,371 @@
+package test;
+
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.lang.annotation.Annotation;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.github.t1.annotations.AmbiguousAnnotationResolutionException;
+import com.github.t1.annotations.Annotations;
+import com.github.t1.annotations.tck.MixinClasses.AnotherAnnotation;
+import com.github.t1.annotations.tck.MixinClasses.FieldAnnotationMixinClasses.SomeClassWithFieldWithVariousAnnotations;
+import com.github.t1.annotations.tck.MixinClasses.FieldAnnotationMixinClasses.TargetFieldClassWithTwoMixins;
+import com.github.t1.annotations.tck.MixinClasses.FieldAnnotationMixinClasses.TargetFieldClassWithTwoNonRepeatableMixins;
+import com.github.t1.annotations.tck.MixinClasses.FieldAnnotationMixinClasses.TargetFieldClassWithTwoRepeatableMixins;
+import com.github.t1.annotations.tck.MixinClasses.MethodAnnotationMixinClasses.SomeClassWithMethodWithVariousAnnotations;
+import com.github.t1.annotations.tck.MixinClasses.MethodAnnotationMixinClasses.TargetMethodClassWithTwoMixins;
+import com.github.t1.annotations.tck.MixinClasses.MethodAnnotationMixinClasses.TargetMethodClassWithTwoNonRepeatableMixins;
+import com.github.t1.annotations.tck.MixinClasses.MethodAnnotationMixinClasses.TargetMethodClassWithTwoRepeatableMixins;
+import com.github.t1.annotations.tck.MixinClasses.TypeAnnotationMixinClasses.OriginalAnnotatedTarget;
+import com.github.t1.annotations.tck.MixinClasses.TypeAnnotationMixinClasses.SomeAnnotationTargetedByMixin;
+import com.github.t1.annotations.tck.MixinClasses.TypeAnnotationMixinClasses.SomeAnnotationWithoutValue;
+import com.github.t1.annotations.tck.MixinClasses.TypeAnnotationMixinClasses.SomeClassWithAnnotationTargetedByMixin;
+import com.github.t1.annotations.tck.MixinClasses.TypeAnnotationMixinClasses.SomeClassWithVariousAnnotations;
+import com.github.t1.annotations.tck.MixinClasses.TypeAnnotationMixinClasses.TargetClassWithTwoMixins;
+import com.github.t1.annotations.tck.MixinClasses.TypeAnnotationMixinClasses.TargetClassWithTwoNonRepeatableMixins;
+import com.github.t1.annotations.tck.MixinClasses.TypeAnnotationMixinClasses.TargetClassWithTwoRepeatableMixins;
+import com.github.t1.annotations.tck.RepeatableAnnotation;
+import com.github.t1.annotations.tck.SomeAnnotation;
+
+public class MixinBehavior {
+
+ @Nested
+ class ClassAnnotations {
+ Annotations annotations = Annotations.on(SomeClassWithVariousAnnotations.class);
+
+ @Test
+ void shouldGetTargetClassAnnotation() {
+ Optional annotation = annotations.get(SomeAnnotationWithoutValue.class);
+
+ then(annotation).isPresent();
+ }
+
+ @Test
+ void shouldGetMixinClassAnnotation() {
+ Optional annotation = annotations.get(AnotherAnnotation.class);
+
+ then(annotation).isPresent();
+ }
+
+ @Test
+ void shouldGetReplacedClassAnnotation() {
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("replacing");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldFailToGetRepeatedClassAnnotation() {
+ Throwable throwable = catchThrowable(() -> annotations.get(RepeatableAnnotation.class));
+
+ then(throwable).isInstanceOf(AmbiguousAnnotationResolutionException.class);
+ }
+
+ @Test
+ void shouldGetAllClassAnnotations() {
+ Stream list = annotations.all();
+
+ then(list.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + SomeAnnotation.class.getName() + "(value = \"replacing\")",
+ "@" + AnotherAnnotation.class.getName(),
+ "@" + SomeAnnotationWithoutValue.class.getName(),
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllRepeatableClassAnnotations() {
+ Stream list = annotations.all(RepeatableAnnotation.class);
+
+ then(list.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ Annotations annotationsFromAnnotationTargetedByMixin = Annotations.on(SomeClassWithAnnotationTargetedByMixin.class);
+
+ @Test
+ void shouldGetMixedInAnnotation() {
+ Optional someAnnotation = annotationsFromAnnotationTargetedByMixin.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("annotation-mixin");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllNonRepeatableMixedInAnnotations() {
+ Stream someAnnotation = annotationsFromAnnotationTargetedByMixin.all(SomeAnnotation.class);
+
+ then(someAnnotation.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"annotation-mixin\")");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllRepeatableMixedInAnnotations() {
+ Stream repeatableAnnotations = annotationsFromAnnotationTargetedByMixin
+ .all(RepeatableAnnotation.class);
+
+ then(repeatableAnnotations.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ void shouldGetAllMixedInAnnotation() {
+ Stream all = annotationsFromAnnotationTargetedByMixin.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotationTargetedByMixin.class.getName(),
+ "@" + SomeAnnotation.class.getName() + "(value = \"annotation-mixin\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ void shouldOverwriteAnnotationWithAnnotationMixedIn() {
+ Annotations annotations = Annotations.on(OriginalAnnotatedTarget.class);
+
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("annotation-mixin");
+ }
+
+ @Test
+ void shouldGetClassAnnotationFromMultipleMixins() {
+ Annotations annotations = Annotations.on(TargetClassWithTwoMixins.class);
+
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("one");
+ }
+
+ @Test
+ void shouldGetOneOfDuplicateNonRepeatableClassAnnotationFromMultipleMixins() {
+ Annotations annotations = Annotations.on(TargetClassWithTwoNonRepeatableMixins.class);
+
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("one");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldFailToGetDuplicateRepeatableClassAnnotationFromMultipleMixins() {
+ Annotations annotations = Annotations.on(TargetClassWithTwoRepeatableMixins.class);
+
+ Throwable throwable = catchThrowable(() -> annotations.get(RepeatableAnnotation.class));
+
+ then(throwable).isInstanceOf(AmbiguousAnnotationResolutionException.class);
+ }
+ }
+
+ @Nested
+ class FieldAnnotations {
+ Annotations annotations = Annotations.onField(SomeClassWithFieldWithVariousAnnotations.class, "foo");
+
+ @Test
+ void shouldSkipUndefinedMixinFieldAnnotation() {
+ Annotations annotations = Annotations.onField(SomeClassWithFieldWithVariousAnnotations.class, "bar");
+
+ Optional someAnnotationWithoutValue = annotations.get(SomeAnnotationWithoutValue.class);
+
+ then(someAnnotationWithoutValue).isNotPresent();
+ }
+
+ @Test
+ void shouldGetTargetFieldAnnotation() {
+ Optional someAnnotationWithoutValue = annotations.get(SomeAnnotationWithoutValue.class);
+
+ then(someAnnotationWithoutValue).isPresent();
+ }
+
+ @Test
+ void shouldGetMixinFieldAnnotation() {
+ Optional anotherAnnotation = annotations.get(AnotherAnnotation.class);
+
+ then(anotherAnnotation).isPresent();
+ }
+
+ @Test
+ void shouldGetReplacedFieldAnnotation() {
+ Optional annotation = annotations.get(SomeAnnotation.class);
+
+ assert annotation.isPresent();
+ then(annotation.get().value()).isEqualTo("replacing");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldFailToGetRepeatableFieldAnnotation() {
+ Throwable throwable = catchThrowable(() -> annotations.get(RepeatableAnnotation.class));
+
+ then(throwable).isInstanceOf(AmbiguousAnnotationResolutionException.class);
+ }
+
+ @Test
+ void shouldGetAllFieldAnnotations() {
+ Stream list = annotations.all();
+
+ then(list.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + AnotherAnnotation.class.getName(),
+ "@" + SomeAnnotationWithoutValue.class.getName(),
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + SomeAnnotation.class.getName() + "(value = \"replacing\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllRepeatableFieldAnnotations() {
+ Stream list = annotations.all(RepeatableAnnotation.class);
+
+ then(list.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ void shouldGetOneOfDuplicateFieldAnnotationsFromMultipleMixins() {
+ Annotations annotations = Annotations.onField(TargetFieldClassWithTwoMixins.class, "foo");
+
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("one");
+ }
+
+ @Test
+ void shouldGetOneRepeatableFieldAnnotationFromMultipleMixins() {
+ Annotations annotations = Annotations.onField(TargetFieldClassWithTwoNonRepeatableMixins.class, "foo");
+
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("one");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldFailToGetDuplicateRepeatableFieldAnnotationFromMultipleMixins() {
+ Annotations annotations = Annotations.onField(TargetFieldClassWithTwoRepeatableMixins.class, "foo");
+
+ Throwable throwable = catchThrowable(() -> annotations.get(RepeatableAnnotation.class));
+
+ then(throwable).isInstanceOf(AmbiguousAnnotationResolutionException.class);
+ }
+
+ // TODO test unknown field mixin
+ }
+
+ @Nested
+ class MethodAnnotations {
+ Annotations annotations = Annotations.onMethod(SomeClassWithMethodWithVariousAnnotations.class, "foo");
+
+ @Test
+ void shouldSkipUndefinedMixinMethodAnnotation() {
+ Annotations annotations = Annotations.onMethod(SomeClassWithMethodWithVariousAnnotations.class, "bar");
+
+ Optional someAnnotationWithoutValue = annotations.get(SomeAnnotationWithoutValue.class);
+
+ then(someAnnotationWithoutValue).isNotPresent();
+ }
+
+ @Test
+ void shouldGetTargetMethodAnnotation() {
+ Optional someAnnotationWithoutValue = annotations.get(SomeAnnotationWithoutValue.class);
+
+ then(someAnnotationWithoutValue).isPresent();
+ }
+
+ @Test
+ void shouldGetMixinMethodAnnotation() {
+ Optional anotherAnnotation = annotations.get(AnotherAnnotation.class);
+
+ then(anotherAnnotation).isPresent();
+ }
+
+ @Test
+ void shouldGetReplacedMethodAnnotation() {
+ Optional annotation = annotations.get(SomeAnnotation.class);
+
+ assert annotation.isPresent();
+ then(annotation.get().value()).isEqualTo("replacing");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldFailToGetRepeatableMethodAnnotation() {
+ Throwable throwable = catchThrowable(() -> annotations.get(RepeatableAnnotation.class));
+
+ then(throwable).isInstanceOf(AmbiguousAnnotationResolutionException.class);
+ }
+
+ @Test
+ void shouldGetAllMethodAnnotations() {
+ Stream list = annotations.all();
+
+ then(list.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + AnotherAnnotation.class.getName(),
+ "@" + SomeAnnotationWithoutValue.class.getName(),
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + SomeAnnotation.class.getName() + "(value = \"replacing\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllRepeatableMethodAnnotations() {
+ Stream list = annotations.all(RepeatableAnnotation.class);
+
+ then(list.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ void shouldGetMethodAnnotationFromMultipleMixins() {
+ Annotations annotations = Annotations.onMethod(TargetMethodClassWithTwoMixins.class, "foo");
+
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("one");
+ }
+
+ @Test
+ void shouldGetOneOfDuplicateNonRepeatableMethodAnnotationFromMultipleMixins() {
+ Annotations annotations = Annotations.onMethod(TargetMethodClassWithTwoNonRepeatableMixins.class, "foo");
+
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("one");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldFailToGetDuplicateRepeatableMethodAnnotationFromMultipleMixins() {
+ Annotations annotations = Annotations.onMethod(TargetMethodClassWithTwoRepeatableMixins.class, "foo");
+
+ Throwable throwable = catchThrowable(() -> annotations.get(RepeatableAnnotation.class));
+
+ then(throwable).isInstanceOf(AmbiguousAnnotationResolutionException.class);
+ }
+
+ // TODO constructor mixins
+ // TODO test unknown method mixin (name or args)
+ }
+}
diff --git a/power-annotations/tck/src/test/java/test/RepeatableAnnotationBehavior.java b/power-annotations/tck/src/test/java/test/RepeatableAnnotationBehavior.java
new file mode 100644
index 000000000..0e1594260
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/RepeatableAnnotationBehavior.java
@@ -0,0 +1,58 @@
+package test;
+
+import static org.assertj.core.api.Assertions.catchThrowable;
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.lang.annotation.Annotation;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Test;
+
+import com.github.t1.annotations.AmbiguousAnnotationResolutionException;
+import com.github.t1.annotations.Annotations;
+import com.github.t1.annotations.tck.RepeatableAnnotation;
+import com.github.t1.annotations.tck.RepeatableAnnotationClasses.RepeatedAnnotationClass;
+import com.github.t1.annotations.tck.RepeatableAnnotationClasses.UnrepeatedAnnotationClass;
+
+@RepeatableAnnotationsTestSuite
+public class RepeatableAnnotationBehavior {
+
+ @Test
+ void shouldGetSingleRepeatedAnnotation() {
+ Annotations annotations = Annotations.on(UnrepeatedAnnotationClass.class);
+
+ Optional annotation = annotations.get(RepeatableAnnotation.class);
+
+ assert annotation.isPresent();
+ then(annotation.get().value()).isEqualTo(1);
+ }
+
+ Annotations repeatedAnnotations = Annotations.on(RepeatedAnnotationClass.class);
+
+ @Test
+ void shouldFailToGetRepeatingAnnotation() {
+ Throwable throwable = catchThrowable(() -> repeatedAnnotations.get(RepeatableAnnotation.class));
+
+ then(throwable)
+ .isInstanceOf(AmbiguousAnnotationResolutionException.class)
+ // TODO message detail about the target .hasMessageContaining(SomeClass.class.getName())
+ .hasMessageContaining(RepeatableAnnotation.class.getName());
+ }
+
+ @Test
+ void shouldGetAll() {
+ Stream all = repeatedAnnotations.all();
+
+ then(all.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ void shouldGetTypedAll() {
+ Stream all = repeatedAnnotations.all(RepeatableAnnotation.class);
+
+ then(all.map(RepeatableAnnotation::value)).containsExactlyInAnyOrder(1, 2);
+ }
+}
diff --git a/power-annotations/tck/src/test/java/test/RepeatableAnnotationsTestSuite.java b/power-annotations/tck/src/test/java/test/RepeatableAnnotationsTestSuite.java
new file mode 100644
index 000000000..8a0e746ae
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/RepeatableAnnotationsTestSuite.java
@@ -0,0 +1,12 @@
+package test;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import org.junit.jupiter.api.Tag;
+
+@Tag("RepeatableAnnotationsTestSuite")
+@Retention(RUNTIME)
+public @interface RepeatableAnnotationsTestSuite {
+}
diff --git a/power-annotations/tck/src/test/java/test/StereotypeBehavior.java b/power-annotations/tck/src/test/java/test/StereotypeBehavior.java
new file mode 100644
index 000000000..4692ae6f4
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/StereotypeBehavior.java
@@ -0,0 +1,256 @@
+package test;
+
+import static java.util.stream.Collectors.toList;
+import static org.assertj.core.api.BDDAssertions.then;
+
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Objects;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.junit.jupiter.api.Nested;
+import org.junit.jupiter.api.Test;
+
+import com.github.t1.annotations.Annotations;
+import com.github.t1.annotations.tck.RepeatableAnnotation;
+import com.github.t1.annotations.tck.SomeAnnotation;
+import com.github.t1.annotations.tck.StereotypeClasses.AnotherStereotype;
+import com.github.t1.annotations.tck.StereotypeClasses.ClassWithStereotypedField;
+import com.github.t1.annotations.tck.StereotypeClasses.ClassWithStereotypedMethod;
+import com.github.t1.annotations.tck.StereotypeClasses.DoubleIndirectlyStereotypedClass;
+import com.github.t1.annotations.tck.StereotypeClasses.DoubleStereotypedClass;
+import com.github.t1.annotations.tck.StereotypeClasses.IndirectlyStereotypedClass;
+import com.github.t1.annotations.tck.StereotypeClasses.SomeDoubleIndirectedStereotype;
+import com.github.t1.annotations.tck.StereotypeClasses.SomeIndirectedStereotype;
+import com.github.t1.annotations.tck.StereotypeClasses.SomeStereotype;
+import com.github.t1.annotations.tck.StereotypeClasses.SomeTardyIndirectedStereotype;
+import com.github.t1.annotations.tck.StereotypeClasses.StereotypedClass;
+import com.github.t1.annotations.tck.StereotypeClasses.StereotypedClassWithSomeAnnotation;
+import com.github.t1.annotations.tck.StereotypeClasses.TardyIndirectlyStereotypedClass;
+
+public class StereotypeBehavior {
+
+ @Nested
+ class StereotypedClasses {
+ Annotations annotations = Annotations.on(StereotypedClass.class);
+
+ @Test
+ void shouldGetAnnotationFromClassStereotype() {
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("some-stereotype");
+ }
+
+ @Test
+ void shouldGetAllAnnotationsFromClassStereotype() {
+ Stream someAnnotation = annotations.all();
+
+ then(someAnnotation.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 5)",
+ "@" + SomeStereotype.class.getName(),
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllNonRepeatableAnnotationsFromClassStereotype() {
+ Stream someAnnotation = annotations.all(SomeAnnotation.class);
+
+ then(someAnnotation.map(Objects::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllRepeatableAnnotationFromClassStereotype() {
+ Stream someAnnotation = annotations.all(RepeatableAnnotation.class);
+
+ then(someAnnotation.map(Objects::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 5)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ void shouldGetAllFromIndirectClassStereotype() {
+ Annotations annotations = Annotations.on(IndirectlyStereotypedClass.class);
+
+ Stream all = annotations.all();
+
+ then(all.map(Objects::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)",
+ "@" + SomeStereotype.class.getName(),
+ "@" + SomeIndirectedStereotype.class.getName());
+ }
+
+ @Test
+ void shouldGetAllFromIndirectClassStereotypeResolvedAlphabeticallyAfterSomeStereotype() {
+ Annotations annotations = Annotations.on(TardyIndirectlyStereotypedClass.class);
+
+ Stream all = annotations.all();
+
+ then(all.map(Objects::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)",
+ "@" + SomeStereotype.class.getName(),
+ "@" + SomeTardyIndirectedStereotype.class.getName());
+ }
+
+ @Test
+ void shouldGetAllFromDoubleIndirectClassStereotype() {
+ Annotations annotations = Annotations.on(DoubleIndirectlyStereotypedClass.class);
+
+ Stream all = annotations.all();
+
+ then(all.map(Objects::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)",
+ "@" + SomeStereotype.class.getName(),
+ "@" + SomeIndirectedStereotype.class.getName(),
+ "@" + SomeDoubleIndirectedStereotype.class.getName());
+ }
+
+ @Test
+ void shouldGetClassAnnotationAmbiguousWithStereotype() {
+ Annotations annotations = Annotations.on(StereotypedClassWithSomeAnnotation.class);
+
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("on-class");
+ }
+ }
+
+ @Nested
+ class DoubleStereotypedClasses {
+ Annotations annotations = Annotations.on(DoubleStereotypedClass.class);
+
+ @Test
+ void shouldGetFirstOfAmbiguousAnnotationFromTwoStereotypes() {
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isIn( // both are allowed:
+ "some-stereotype",
+ "another-stereotype");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllNonRepeatableAnnotationsFromTwoStereotypes() {
+ Stream someAnnotations = annotations.all(SomeAnnotation.class);
+
+ then(someAnnotations.map(Objects::toString)).containsAnyOf( // both are allowed:
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")",
+ "@" + SomeAnnotation.class.getName() + "(value = \"another-stereotype\")");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllRepeatableAnnotationsFromTwoStereotypes() {
+ Stream repeatableAnnotations = annotations.all(RepeatableAnnotation.class);
+
+ then(repeatableAnnotations.map(Objects::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 6)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 3)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 4)");
+ }
+
+ @Test
+ void shouldGetAllAnnotationsFromTwoStereotypes() {
+ Stream all = annotations.all();
+
+ List list = all.map(Objects::toString).collect(toList());
+ then(list).contains(
+ "@" + SomeStereotype.class.getName(),
+ "@" + AnotherStereotype.class.getName(),
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 3)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 4)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 6)");
+ then(list).containsAnyOf( // both are allowed:
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")",
+ "@" + SomeAnnotation.class.getName() + "(value = \"another-stereotype\")");
+ then(list).hasSize(8);
+ }
+ }
+
+ @Nested
+ class StereotypedFields {
+ Annotations annotations = Annotations.onField(ClassWithStereotypedField.class, "foo");
+
+ @Test
+ void shouldGetAnnotationFromFieldStereotype() {
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("some-stereotype");
+ }
+
+ @Test
+ void shouldGetAllAnnotationsFromFieldStereotype() {
+ Stream someAnnotation = annotations.all();
+
+ then(someAnnotation.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 7)",
+ "@" + SomeStereotype.class.getName(),
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllAnnotationNonRepeatableTypedFromFieldStereotype() {
+ Stream someAnnotation = annotations.all(SomeAnnotation.class);
+
+ then(someAnnotation.map(Objects::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")");
+ }
+ }
+
+ @Nested
+ class StereotypedMethods {
+ Annotations annotations = Annotations.onMethod(ClassWithStereotypedMethod.class, "foo");
+
+ @Test
+ void shouldGetAnnotationFromMethodStereotype() {
+ Optional someAnnotation = annotations.get(SomeAnnotation.class);
+
+ assert someAnnotation.isPresent();
+ then(someAnnotation.get().value()).isEqualTo("some-stereotype");
+ }
+
+ @Test
+ void shouldGetAllAnnotationsFromMethodStereotype() {
+ Stream someAnnotation = annotations.all();
+
+ then(someAnnotation.map(Object::toString)).containsExactlyInAnyOrder(
+ "@" + RepeatableAnnotation.class.getName() + "(value = 7)",
+ "@" + SomeStereotype.class.getName(),
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 1)",
+ "@" + RepeatableAnnotation.class.getName() + "(value = 2)");
+ }
+
+ @Test
+ @RepeatableAnnotationsTestSuite
+ void shouldGetAllAnnotationNonRepeatableTypedFromMethodStereotype() {
+ Stream someAnnotation = annotations.all(SomeAnnotation.class);
+
+ then(someAnnotation.map(Objects::toString)).containsExactlyInAnyOrder(
+ "@" + SomeAnnotation.class.getName() + "(value = \"some-stereotype\")");
+ }
+ }
+}
diff --git a/power-annotations/tck/src/test/java/test/TypeToMemberAnnotationsTestSuite.java b/power-annotations/tck/src/test/java/test/TypeToMemberAnnotationsTestSuite.java
new file mode 100644
index 000000000..9968b67de
--- /dev/null
+++ b/power-annotations/tck/src/test/java/test/TypeToMemberAnnotationsTestSuite.java
@@ -0,0 +1,12 @@
+package test;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.Retention;
+
+import org.junit.jupiter.api.Tag;
+
+@Tag("TypeToMemberAnnotationsTestSuite")
+@Retention(RUNTIME)
+public @interface TypeToMemberAnnotationsTestSuite {
+}
diff --git a/power-annotations/utils/api/pom.xml b/power-annotations/utils/api/pom.xml
new file mode 100644
index 000000000..84b1c470a
--- /dev/null
+++ b/power-annotations/utils/api/pom.xml
@@ -0,0 +1,16 @@
+
+
+ 4.0.0
+
+ io.smallrye
+ smallrye-power-annotations-utils-parent
+ 1.0.18-SNAPSHOT
+
+
+ smallrye-power-annotations-utils-api
+ Power Annotations Utils API
+ The Utils API for working with annotations. Used by the TCK.
+
diff --git a/power-annotations/utils/api/src/main/java/com/github/t1/annotations/AmbiguousAnnotationResolutionException.java b/power-annotations/utils/api/src/main/java/com/github/t1/annotations/AmbiguousAnnotationResolutionException.java
new file mode 100644
index 000000000..0a1e4c595
--- /dev/null
+++ b/power-annotations/utils/api/src/main/java/com/github/t1/annotations/AmbiguousAnnotationResolutionException.java
@@ -0,0 +1,27 @@
+package com.github.t1.annotations;
+
+/**
+ * @see Annotations#get(Class)
+ */
+@SuppressWarnings("unused")
+public class AmbiguousAnnotationResolutionException extends RuntimeException {
+ public AmbiguousAnnotationResolutionException() {
+ }
+
+ public AmbiguousAnnotationResolutionException(String message) {
+ super(message);
+ }
+
+ public AmbiguousAnnotationResolutionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public AmbiguousAnnotationResolutionException(Throwable cause) {
+ super(cause);
+ }
+
+ public AmbiguousAnnotationResolutionException(String message, Throwable cause, boolean enableSuppression,
+ boolean writableStackTrace) {
+ super(message, cause, enableSuppression, writableStackTrace);
+ }
+}
diff --git a/power-annotations/utils/api/src/main/java/com/github/t1/annotations/Annotations.java b/power-annotations/utils/api/src/main/java/com/github/t1/annotations/Annotations.java
new file mode 100644
index 000000000..7d8460357
--- /dev/null
+++ b/power-annotations/utils/api/src/main/java/com/github/t1/annotations/Annotations.java
@@ -0,0 +1,63 @@
+package com.github.t1.annotations;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Field;
+import java.lang.reflect.Method;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+public interface Annotations {
+ static Annotations on(Class> type) {
+ return AnnotationsLoader.INSTANCE.onType(type);
+ }
+
+ static Annotations on(Field field) {
+ return onField(field.getDeclaringClass(), field.getName());
+ }
+
+ static Annotations onField(Class> type, String fieldName) {
+ return AnnotationsLoader.INSTANCE.onField(type, fieldName);
+ }
+
+ static Annotations on(Method method) {
+ return onMethod(method.getDeclaringClass(), method.getName(), method.getParameterTypes());
+ }
+
+ static Annotations onMethod(Class> type, String methodName, Class>... argTypes) {
+ return AnnotationsLoader.INSTANCE.onMethod(type, methodName, argTypes);
+ }
+
+ /**
+ * Get all {@link Annotation} instances.
+ * If the annotation type is {@link java.lang.annotation.Repeatable}, the same type
+ * can show up several times, eventually with different properties.
+ */
+ Stream all();
+
+ /**
+ * Get the 'strongest' {@link Annotation} instance of this type.
+ * Multiple annotations may be applicable, e.g. from several mixins or stereotypes.
+ * The annotation will be picked in this order:
+ *
+ * - mixin
+ * - target
+ * - target stereotypes
+ * - containing class
+ * - containing class stereotypes
+ * - containing package (TODO not yet implemented)
+ * - containing package stereotypes (TODO not yet implemented)
+ *
+ * If this order not sufficiently defined, e.g. because there are multiple repeatable annotations,
+ * or because there are multiple mixins or stereotypes with the same annotation,
+ * an {@link AmbiguousAnnotationResolutionException} is thrown. I.e. when an annotation is changed
+ * to be repeatable, all frameworks using this annotation will have to use {@link #all(Class)} instead
+ * (of course), but the semantics for the client code changes, which may break existing code.
+ */
+ Optional get(Class type);
+
+ /**
+ * Get all (eventually {@link java.lang.annotation.Repeatable repeatable})
+ * {@link Annotation} instances of this type
.
+ */
+ Stream all(Class type);
+}
diff --git a/power-annotations/utils/api/src/main/java/com/github/t1/annotations/AnnotationsLoader.java b/power-annotations/utils/api/src/main/java/com/github/t1/annotations/AnnotationsLoader.java
new file mode 100644
index 000000000..4e4b2f3be
--- /dev/null
+++ b/power-annotations/utils/api/src/main/java/com/github/t1/annotations/AnnotationsLoader.java
@@ -0,0 +1,29 @@
+package com.github.t1.annotations;
+
+import java.util.Iterator;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+
+/**
+ * SPI + Singleton for loading the implementation
+ */
+public abstract class AnnotationsLoader {
+ static final AnnotationsLoader INSTANCE = load();
+
+ private static AnnotationsLoader load() {
+ ServiceLoader loader = ServiceLoader.load(AnnotationsLoader.class);
+ Iterator iterator = loader.iterator();
+ if (!iterator.hasNext())
+ throw new ServiceConfigurationError("no " + AnnotationsLoader.class.getName() + " in classpath");
+ AnnotationsLoader graphQlClientBuilder = iterator.next();
+ if (iterator.hasNext())
+ throw new ServiceConfigurationError("more than one " + AnnotationsLoader.class.getName() + " in classpath");
+ return graphQlClientBuilder;
+ }
+
+ public abstract Annotations onType(Class> type);
+
+ public abstract Annotations onField(Class> type, String fieldName);
+
+ public abstract Annotations onMethod(Class> type, String methodName, Class>... argTypes);
+}
diff --git a/power-annotations/utils/jandex/pom.xml b/power-annotations/utils/jandex/pom.xml
new file mode 100644
index 000000000..2233716a6
--- /dev/null
+++ b/power-annotations/utils/jandex/pom.xml
@@ -0,0 +1,30 @@
+
+
+ 4.0.0
+
+
+ io.smallrye
+ smallrye-power-annotations-utils-parent
+ 1.0.18-SNAPSHOT
+
+
+ smallrye-power-annotations-utils-jandex
+ Power Annotations Utils Impl for Jandex
+ Implementation of the utils for working with annotations base od Jandex.
+
+
+
+ io.smallrye
+ smallrye-power-annotations-utils-api
+ ${project.version}
+
+
+ io.smallrye
+ power-annotations-scanner
+ ${project.version}
+
+
+
diff --git a/power-annotations/utils/jandex/src/main/java/com/github/t1/powerannotations/utils/jandex/AnnotationProxy.java b/power-annotations/utils/jandex/src/main/java/com/github/t1/powerannotations/utils/jandex/AnnotationProxy.java
new file mode 100644
index 000000000..58821e820
--- /dev/null
+++ b/power-annotations/utils/jandex/src/main/java/com/github/t1/powerannotations/utils/jandex/AnnotationProxy.java
@@ -0,0 +1,95 @@
+package com.github.t1.powerannotations.utils.jandex;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Array;
+import java.lang.reflect.Method;
+import java.lang.reflect.Proxy;
+import java.util.function.Function;
+
+import org.jboss.jandex.AnnotationInstance;
+
+/**
+ * {@link #build() Builds} a {@link Proxy dynamic proxy} that delegates to three
+ * function objects for the implementation.
+ */
+class AnnotationProxy {
+ static Annotation proxy(AnnotationInstance annotationInstance) {
+ return new AnnotationProxy(
+ annotationInstance.name().toString(),
+ annotationInstance.toString(false),
+ name -> annotationInstance.value(name).value())
+ .build();
+ }
+
+ private final String typeName;
+ private final String toString;
+ private final Function property;
+
+ private AnnotationProxy(String typeName, String toString, Function property) {
+ this.typeName = typeName;
+ this.toString = toString;
+ this.property = property;
+ }
+
+ private Annotation build() {
+ Class>[] interfaces = new Class[] { getAnnotationType(), Annotation.class };
+ return (Annotation) Proxy.newProxyInstance(getClassLoader(), interfaces, this::invoke);
+ }
+
+ private Class> getAnnotationType() {
+ return loadClass(typeName);
+ }
+
+ private static Class> loadClass(String typeName) {
+ try {
+ return getClassLoader().loadClass(typeName);
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException("can't load annotation type " + typeName, e);
+ }
+ }
+
+ private static ClassLoader getClassLoader() {
+ ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
+ return (classLoader == null) ? ClassLoader.getSystemClassLoader() : classLoader;
+ }
+
+ Object invoke(Object proxy, Method method, Object... args) {
+ String name = method.getName();
+ if (method.getParameterCount() == 1 && "equals".equals(name))
+ return toString.equals(args[0].toString());
+ // no other methods on annotations can have arguments (except for one `wait`)
+ assert method.getParameterCount() == 0;
+ assert args == null || args.length == 0;
+ if ("hashCode".equals(name))
+ return toString.hashCode();
+ if ("annotationType".equals(name))
+ return getAnnotationType();
+ if ("toString".equals(name))
+ return toString;
+
+ Object value = property.apply(name);
+
+ return toType(value, method.getReturnType());
+ }
+
+ private Object toType(Object value, Class> returnType) {
+ // TODO implement
+ // if (returnType.isAnnotation())
+ // return proxy(AnnotationInstance.from(value));
+ // if (returnType.isEnum())
+ // return enumValue(returnType, (String) value);
+ // if (returnType.equals(Class.class))
+ // return toClass(value);
+ if (returnType.isArray())
+ return toArray(returnType.getComponentType(), (Object[]) value);
+ return value;
+ }
+
+ private Object toArray(Class> componentType, Object[] values) {
+ Object array = Array.newInstance(componentType, values.length);
+ // TODO implement
+ // for (int i = 0; i < values.length; i++)
+ // Array.set(array, i, toType(AnnotationValue.of(values[i]), componentType));
+ return array;
+ }
+}
diff --git a/power-annotations/utils/jandex/src/main/java/com/github/t1/powerannotations/utils/jandex/JandexAnnotationsLoader.java b/power-annotations/utils/jandex/src/main/java/com/github/t1/powerannotations/utils/jandex/JandexAnnotationsLoader.java
new file mode 100644
index 000000000..9485476e5
--- /dev/null
+++ b/power-annotations/utils/jandex/src/main/java/com/github/t1/powerannotations/utils/jandex/JandexAnnotationsLoader.java
@@ -0,0 +1,101 @@
+package com.github.t1.powerannotations.utils.jandex;
+
+import java.lang.annotation.Annotation;
+import java.util.Optional;
+import java.util.stream.Stream;
+
+import org.jboss.jandex.AnnotationInstance;
+import org.jboss.jandex.ClassInfo;
+import org.jboss.jandex.DotName;
+import org.jboss.jandex.FieldInfo;
+import org.jboss.jandex.IndexView;
+import org.jboss.jandex.MethodInfo;
+
+import com.github.t1.annotations.Annotations;
+import com.github.t1.annotations.AnnotationsLoader;
+import com.github.t1.powerannotations.scanner.IndexBuilder;
+
+public class JandexAnnotationsLoader extends AnnotationsLoader {
+ private static final IndexView jandex = IndexBuilder.loadOrScan();
+
+ @Override
+ public Annotations onType(Class> type) {
+ ClassInfo classInfo = jandex.getClassByName(toDotName(type));
+
+ return new Annotations() {
+ @Override
+ public Stream all() {
+ return classInfo.classAnnotations().stream().map(JandexAnnotationsLoader::proxy);
+ }
+
+ @Override
+ public Optional get(Class type) {
+ return Optional.ofNullable(classInfo.classAnnotation(toDotName(type))).map(JandexAnnotationsLoader::proxy);
+ }
+
+ @Override
+ public Stream all(Class type) {
+ throw new UnsupportedOperationException(); // TODO resolve repeated annotations
+ }
+ };
+ }
+
+ @Override
+ public Annotations onField(Class> type, String fieldName) {
+ ClassInfo classInfo = jandex.getClassByName(toDotName(type));
+ FieldInfo fieldInfo = classInfo.field(fieldName);
+
+ // Looks like a code duplicate, but ClassInfo, FieldInfo, and MethodInfo don't have a common interface
+ //noinspection DuplicatedCode
+ return new Annotations() {
+ @Override
+ public Stream all() {
+ return fieldInfo.annotations().stream().map(JandexAnnotationsLoader::proxy);
+ }
+
+ @Override
+ public Optional get(Class type) {
+ return Optional.ofNullable(fieldInfo.annotation(toDotName(type))).map(JandexAnnotationsLoader::proxy);
+ }
+
+ @Override
+ public Stream all(Class type) {
+ throw new UnsupportedOperationException(); // TODO resolve repeated annotations
+ }
+ };
+ }
+
+ @Override
+ public Annotations onMethod(Class> type, String methodName, Class>... argTypes) {
+ ClassInfo classInfo = jandex.getClassByName(toDotName(type));
+ MethodInfo methodInfo = classInfo.method(methodName);// TODO arg types
+
+ // Looks like a code duplicate, but ClassInfo, FieldInfo, and MethodInfo don't have a common interface
+ //noinspection DuplicatedCode
+ return new Annotations() {
+ @Override
+ public Stream all() {
+ return methodInfo.annotations().stream().map(JandexAnnotationsLoader::proxy);
+ }
+
+ @Override
+ public Optional get(Class type) {
+ return Optional.ofNullable(methodInfo.annotation(toDotName(type))).map(JandexAnnotationsLoader::proxy);
+ }
+
+ @Override
+ public Stream all(Class type) {
+ throw new UnsupportedOperationException(); // TODO resolve repeated annotations
+ }
+ };
+ }
+
+ private static T proxy(AnnotationInstance annotationInstance) {
+ //noinspection unchecked
+ return (T) AnnotationProxy.proxy(annotationInstance);
+ }
+
+ private static DotName toDotName(Class> annotationType) {
+ return DotName.createSimple(annotationType.getName());
+ }
+}
diff --git a/power-annotations/utils/jandex/src/main/resources/META-INF/jandex.properties b/power-annotations/utils/jandex/src/main/resources/META-INF/jandex.properties
new file mode 100644
index 000000000..4786f2862
--- /dev/null
+++ b/power-annotations/utils/jandex/src/main/resources/META-INF/jandex.properties
@@ -0,0 +1,16 @@
+exclude=\
+ net.bytebuddy:byte-buddy-agent \
+ net.bytebuddy:byte-buddy \
+ org.apiguardian:apiguardian-api \
+ org.assertj:assertj-core \
+ org.jboss:jandex \
+ org.junit.jupiter:junit-jupiter-api \
+ org.junit.jupiter:junit-jupiter-engine \
+ org.junit.jupiter:junit-jupiter-params \
+ org.junit.jupiter:junit-jupiter \
+ org.junit.platform:junit-platform-commons \
+ org.junit.platform:junit-platform-engine \
+ org.mockito:mockito-core \
+ org.mockito:mockito-junit-jupiter \
+ org.objenesis:objenesis \
+ org.opentest4j:opentest4j
diff --git a/power-annotations/utils/jandex/src/main/resources/META-INF/services/com.github.t1.annotations.AnnotationsLoader b/power-annotations/utils/jandex/src/main/resources/META-INF/services/com.github.t1.annotations.AnnotationsLoader
new file mode 100644
index 000000000..c47a8f632
--- /dev/null
+++ b/power-annotations/utils/jandex/src/main/resources/META-INF/services/com.github.t1.annotations.AnnotationsLoader
@@ -0,0 +1 @@
+com.github.t1.powerannotations.utils.jandex.JandexAnnotationsLoader
diff --git a/power-annotations/utils/pom.xml b/power-annotations/utils/pom.xml
new file mode 100644
index 000000000..7d3377b67
--- /dev/null
+++ b/power-annotations/utils/pom.xml
@@ -0,0 +1,20 @@
+
+
+ 4.0.0
+
+
+ io.smallrye
+ smallrye-power-annotations-parent
+ 1.0.18-SNAPSHOT
+
+
+ smallrye-power-annotations-utils-parent
+ pom
+ Power Annotations Utils Parent POM
+ Utils for working with annotations, i.e. currently an abstraction of the Jandex API. Used by the TCK.
+
+
+ api
+ jandex
+
+