From 772a528eec140ea2b56a341b04adfb6fd1a108cf Mon Sep 17 00:00:00 2001 From: Ladislav Thon Date: Mon, 11 Dec 2023 13:42:00 +0100 Subject: [PATCH] add StackedIndex A stacked index is a composition of multiple indices with overlay semantics. Overlaying is done on class granularity. When the composition doesn't contain duplicate classes (multiple classes with the same name), a `StackedIndex` behaves just like a `CompositeIndex`, but when duplicates appear, the behavior of `StackedIndex` is actually well defined. This commit also adds a test for index navigation methods from `IndexView`, as those have not been tested at all. Some tests reveal inconsistencies in how `CompositeIndex` behaves in presence of duplicates, which is not nice, but this commit intentionally doesn't modify `CompositeIndex` behavior. --- .../java/org/jboss/jandex/StackedIndex.java | 348 ++++++++++++++++++ .../jandex/test/IndexNavigationTest.java | 321 ++++++++++++++++ .../jboss/jandex/test/StackedIndexTest.java | 146 ++++++++ .../org/jboss/jandex/test/util/IOUtil.java | 55 +++ 4 files changed, 870 insertions(+) create mode 100644 core/src/main/java/org/jboss/jandex/StackedIndex.java create mode 100644 core/src/test/java/org/jboss/jandex/test/IndexNavigationTest.java create mode 100644 core/src/test/java/org/jboss/jandex/test/StackedIndexTest.java create mode 100644 core/src/test/java/org/jboss/jandex/test/util/IOUtil.java diff --git a/core/src/main/java/org/jboss/jandex/StackedIndex.java b/core/src/main/java/org/jboss/jandex/StackedIndex.java new file mode 100644 index 00000000..cc110657 --- /dev/null +++ b/core/src/main/java/org/jboss/jandex/StackedIndex.java @@ -0,0 +1,348 @@ +package org.jboss.jandex; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Objects; +import java.util.Queue; +import java.util.Set; + +/** + * A stack of {@linkplain IndexView indexes} with overlay semantics. Overlaying is done on class + * granularity. That is, if a class is present in multiple indexes on the stack, only the top-most + * occurrence is considered; the other variants of the class present below on the stack are ignored. + * + * @since 3.2.0 + */ +public final class StackedIndex implements IndexView { + // note that the top-most index comes first (reverse order compared to the `create()` methods) + private final IndexView[] stack; + + private StackedIndex(IndexView[] stack) { + this.stack = stack; + } + + /** + * Creates a stacked index from given {@code indexes}. The first element of the list is the bottom-most index + * on the stack, while the last element of the list is the top-most index on the stack. + * + * @param indexes indexes in stack order; must not be {@code null} and must not contain {@code null} elements + * @return stacked index, never {@code null} + */ + public static StackedIndex create(List indexes) { + return create(indexes.toArray(new IndexView[0])); + } + + /** + * Creates a stacked index from given {@code indexes}. The first element of the array is the bottom-most index + * on the stack, while the last element of the array is the top-most index on the stack. + * + * @param indexes indexes in stack order; must not be {@code null} and must not contain {@code null} elements + * @return stacked index, never {@code null} + */ + public static StackedIndex create(IndexView... indexes) { + Objects.requireNonNull(indexes); + + List stack = new ArrayList<>(); + for (int i = indexes.length - 1; i >= 0; i--) { + IndexView index = indexes[i]; + Objects.requireNonNull(index); + if (index instanceof StackedIndex) { + Collections.addAll(stack, ((StackedIndex) index).stack); + } else { + stack.add(index); + } + } + return new StackedIndex(stack.toArray(new IndexView[0])); + } + + /** + * Creates a new stacked index where the given {@code index} is on top of the stack + * and the rest of the stack is equivalent to this stacked index. + * + * @param index the index to become a new top of the stack + * @return a new stacked index that results from pushing {@code index} on top of this stacked index + */ + public StackedIndex pushIndex(IndexView index) { + IndexView[] newStack; + if (index instanceof StackedIndex) { + IndexView[] indexes = ((StackedIndex) index).stack; + newStack = new IndexView[indexes.length + stack.length]; + System.arraycopy(indexes, 0, newStack, 0, indexes.length); + System.arraycopy(stack, 0, newStack, indexes.length, stack.length); + } else { + newStack = new IndexView[1 + stack.length]; + newStack[0] = index; + System.arraycopy(stack, 0, newStack, 1, stack.length); + } + return new StackedIndex(newStack); + } + + // --- + + @Override + public Collection getKnownClasses() { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (IndexView idx : stack) { + for (ClassInfo clazz : idx.getKnownClasses()) { + if (seen.add(clazz.name())) { + result.add(clazz); + } + } + } + return Collections.unmodifiableList(result); + } + + @Override + public ClassInfo getClassByName(DotName className) { + for (IndexView index : stack) { + ClassInfo result = index.getClassByName(className); + if (result != null) { + return result; + } + } + return null; + } + + @Override + public Collection getKnownDirectSubclasses(DotName className) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (IndexView idx : stack) { + for (ClassInfo clazz : idx.getKnownDirectSubclasses(className)) { + if (seen.add(clazz.name())) { + result.add(clazz); + } + } + } + return Collections.unmodifiableList(result); + } + + @Override + public Collection getAllKnownSubclasses(DotName className) { + List result = new ArrayList<>(); + + Queue worklist = new ArrayDeque<>(); + Set seen = new HashSet<>(); + + worklist.add(className); + while (!worklist.isEmpty()) { + DotName cls = worklist.remove(); + for (IndexView index : stack) { + for (ClassInfo directSubclass : index.getKnownDirectSubclasses(cls)) { + worklist.add(directSubclass.name()); + if (seen.add(directSubclass.name())) { + result.add(directSubclass); + } + } + } + } + + return Collections.unmodifiableList(result); + } + + @Override + public Collection getKnownDirectSubinterfaces(DotName interfaceName) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (IndexView idx : stack) { + for (ClassInfo clazz : idx.getKnownDirectSubinterfaces(interfaceName)) { + if (seen.add(clazz.name())) { + result.add(clazz); + } + } + } + return Collections.unmodifiableList(result); + } + + @Override + public Collection getAllKnownSubinterfaces(DotName interfaceName) { + List result = new ArrayList<>(); + + Queue worklist = new ArrayDeque<>(); + Set seen = new HashSet<>(); + + worklist.add(interfaceName); + while (!worklist.isEmpty()) { + DotName iface = worklist.remove(); + for (IndexView index : stack) { + for (ClassInfo directSubinterface : index.getKnownDirectSubinterfaces(iface)) { + worklist.add(directSubinterface.name()); + if (seen.add(directSubinterface.name())) { + result.add(directSubinterface); + } + } + } + } + + return Collections.unmodifiableList(result); + } + + @Override + public Collection getKnownDirectImplementors(DotName interfaceName) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (IndexView idx : stack) { + for (ClassInfo clazz : idx.getKnownDirectImplementors(interfaceName)) { + if (seen.add(clazz.name())) { + result.add(clazz); + } + } + } + return Collections.unmodifiableList(result); + } + + @Override + public Collection getAllKnownImplementors(DotName interfaceName) { + List result = new ArrayList<>(); + + Queue worklist = new ArrayDeque<>(); + Set seen = new HashSet<>(); + + worklist.add(interfaceName); + while (!worklist.isEmpty()) { + DotName iface = worklist.remove(); + for (IndexView index : stack) { + for (ClassInfo directImplementor : index.getKnownDirectImplementors(iface)) { + if (directImplementor.isInterface()) { + worklist.add(directImplementor.name()); + } else if (seen.add(directImplementor.name())) { + result.add(directImplementor); + } + } + } + } + + return Collections.unmodifiableList(result); + } + + @Override + public Collection getAnnotations(DotName annotationName) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + Set seenInThisIteration = new HashSet<>(); + for (IndexView idx : stack) { + for (AnnotationInstance annotation : idx.getAnnotations(annotationName)) { + DotName inClass = nameOfDeclaringClass(annotation.target()); + if (inClass == null) { + continue; + } + if (!seen.contains(inClass)) { + result.add(annotation); + seenInThisIteration.add(inClass); + } + } + seen.addAll(seenInThisIteration); + seenInThisIteration.clear(); + } + return Collections.unmodifiableList(result); + } + + @Override + public Collection getAnnotationsWithRepeatable(DotName annotationName, IndexView index) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + Set seenInThisIteration = new HashSet<>(); + for (IndexView idx : stack) { + for (AnnotationInstance annotation : idx.getAnnotationsWithRepeatable(annotationName, index)) { + DotName inClass = nameOfDeclaringClass(annotation.target()); + if (inClass == null) { + continue; + } + if (!seen.contains(inClass)) { + result.add(annotation); + seenInThisIteration.add(inClass); + } + } + seen.addAll(seenInThisIteration); + seenInThisIteration.clear(); + } + return Collections.unmodifiableList(result); + } + + private static DotName nameOfDeclaringClass(AnnotationTarget target) { + if (target == null) { + return null; + } else if (target.kind() == AnnotationTarget.Kind.CLASS) { + return target.asClass().name(); + } else if (target.kind() == AnnotationTarget.Kind.METHOD) { + return target.asMethod().declaringClass().name(); + } else if (target.kind() == AnnotationTarget.Kind.METHOD_PARAMETER) { + return target.asMethodParameter().method().declaringClass().name(); + } else if (target.kind() == AnnotationTarget.Kind.FIELD) { + return target.asField().declaringClass().name(); + } else if (target.kind() == AnnotationTarget.Kind.RECORD_COMPONENT) { + return target.asRecordComponent().declaringClass().name(); + } else if (target.kind() == AnnotationTarget.Kind.TYPE) { + return nameOfDeclaringClass(target.asType().enclosingTarget()); + } else { + throw new IllegalArgumentException("Unknown annotation target: " + target); + } + } + + @Override + public Collection getKnownModules() { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (IndexView idx : stack) { + for (ModuleInfo module : idx.getKnownModules()) { + if (seen.add(module.name())) { + result.add(module); + } + } + } + return Collections.unmodifiableList(result); + } + + @Override + public ModuleInfo getModuleByName(DotName moduleName) { + for (IndexView idx : stack) { + ModuleInfo result = idx.getModuleByName(moduleName); + if (result != null) { + return result; + } + } + return null; + } + + @Override + public Collection getKnownUsers(DotName className) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (IndexView idx : stack) { + for (ClassInfo clazz : idx.getKnownUsers(className)) { + if (seen.add(clazz.name())) { + result.add(clazz); + } + } + } + return Collections.unmodifiableList(result); + } + + @Override + public Collection getClassesInPackage(DotName packageName) { + List result = new ArrayList<>(); + Set seen = new HashSet<>(); + for (IndexView idx : stack) { + for (ClassInfo clazz : idx.getClassesInPackage(packageName)) { + if (seen.add(clazz.name())) { + result.add(clazz); + } + } + } + return Collections.unmodifiableList(result); + } + + @Override + public Set getSubpackages(DotName packageName) { + Set result = new HashSet<>(); + for (IndexView idx : stack) { + result.addAll(idx.getSubpackages(packageName)); + } + return Collections.unmodifiableSet(result); + } +} diff --git a/core/src/test/java/org/jboss/jandex/test/IndexNavigationTest.java b/core/src/test/java/org/jboss/jandex/test/IndexNavigationTest.java new file mode 100644 index 00000000..4a39eae9 --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/IndexNavigationTest.java @@ -0,0 +1,321 @@ +package org.jboss.jandex.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; +import java.util.Collection; + +import org.jboss.jandex.ClassInfo; +import org.jboss.jandex.CompositeIndex; +import org.jboss.jandex.DotName; +import org.jboss.jandex.Index; +import org.jboss.jandex.IndexView; +import org.jboss.jandex.StackedIndex; +import org.jboss.jandex.test.util.IndexingUtil; +import org.junit.jupiter.api.Test; + +public class IndexNavigationTest { + // IGrandparent <---------------------------- CGrandparent + // | | + // | | + // IParent <--------------------------------- CParent + // | | + // +--------+ +--------+ + // | | | | + // IChild ISibling <------------------------ CChild CSibling + // | | + // +-------------+ +-------------+ + // | | | | + // IGrandchild1 IGrandchild2 <-------------- CGrandchild1 CGrandchild2 + + interface IGrandparent { + } + + interface IParent extends IGrandparent { + } + + interface IChild extends IParent { + } + + interface ISibling extends IParent { + } + + interface IGrandchild1 extends IChild { + } + + interface IGrandchild2 extends IChild { + } + + static class CGrandparent implements IGrandparent { + } + + static class CParent extends CGrandparent implements IParent { + } + + static class CChild extends CParent implements IChild { + } + + static class CSibling extends CParent implements ISibling { + } + + static class CGrandchild1 extends CChild implements IGrandchild1 { + } + + static class CGrandchild2 extends CChild implements IGrandchild2 { + } + + @Test + public void testSingleIndex() throws IOException { + Index index = Index.of(IGrandparent.class, IParent.class, IChild.class, ISibling.class, IGrandchild1.class, + IGrandchild2.class, CGrandparent.class, CParent.class, CChild.class, CSibling.class, CGrandchild1.class, + CGrandchild2.class); + testIndex(index); + testIndex(CompositeIndex.create(index)); + testIndex(StackedIndex.create(index)); + + index = IndexingUtil.roundtrip(index); + testIndex(index); + testIndex(CompositeIndex.create(index)); + testIndex(StackedIndex.create(index)); + } + + @Test + public void testCompositeIndex() throws IOException { + Index index1 = Index.of(IGrandparent.class, IParent.class, IChild.class, IGrandchild1.class, CGrandparent.class, + CParent.class, CChild.class, CGrandchild1.class); + Index index2 = Index.of(ISibling.class, IGrandchild2.class, CSibling.class, CGrandchild2.class); + + IndexView index = CompositeIndex.create(index1, index2); + testIndex(index); + + index = CompositeIndex.create(IndexingUtil.roundtrip(index1), IndexingUtil.roundtrip(index2)); + testIndex(index); + } + + @Test + public void testOverlappingCompositeIndex() throws IOException { + Index index1 = Index.of(IGrandparent.class, IParent.class, IChild.class, IGrandchild1.class, CGrandparent.class, + CParent.class, CChild.class, CGrandchild1.class); + Index index2 = Index.of(IChild.class, ISibling.class, IGrandchild1.class, IGrandchild2.class, CChild.class, + CSibling.class, CGrandchild1.class, CGrandchild2.class); + + IndexView index = CompositeIndex.create(index1, index2); + doTestOverlappingCompositeIndex(index); + + index = CompositeIndex.create(IndexingUtil.roundtrip(index1), IndexingUtil.roundtrip(index2)); + doTestOverlappingCompositeIndex(index); + } + + private void doTestOverlappingCompositeIndex(IndexView index) { + assertCollection(index.getKnownDirectSubclasses(Object.class), IGrandparent.class, IParent.class, IChild.class, + IGrandchild1.class, CGrandparent.class, IChild.class, ISibling.class, IGrandchild1.class, IGrandchild2.class); + assertCollection(index.getKnownDirectSubclasses(CParent.class), CChild.class, CChild.class, CSibling.class); + assertCollection(index.getKnownDirectSubclasses(CChild.class), CGrandchild1.class, CGrandchild1.class, + CGrandchild2.class); + + assertCollection(index.getAllKnownSubclasses(Object.class), IGrandparent.class, IParent.class, IChild.class, + IGrandchild1.class, CGrandparent.class, CParent.class, CChild.class, CGrandchild1.class, IChild.class, + ISibling.class, IGrandchild1.class, IGrandchild2.class, CChild.class, CSibling.class, CGrandchild1.class, + CGrandchild2.class); + assertCollection(index.getAllKnownSubclasses(CGrandparent.class), CParent.class, CChild.class, CGrandchild1.class, + CChild.class, CSibling.class, CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownSubclasses(CParent.class), CChild.class, CGrandchild1.class, CChild.class, + CSibling.class, CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownSubclasses(CChild.class), CGrandchild1.class, CGrandchild1.class, CGrandchild2.class); + + assertCollection(index.getKnownDirectSubinterfaces(IGrandparent.class), IParent.class); + assertCollection(index.getKnownDirectSubinterfaces(IParent.class), IChild.class, IChild.class, ISibling.class); + assertCollection(index.getKnownDirectSubinterfaces(IChild.class), IGrandchild1.class, IGrandchild1.class, + IGrandchild2.class); + + assertCollection(index.getAllKnownSubinterfaces(IGrandparent.class), IParent.class, IChild.class, IGrandchild1.class, + IChild.class, ISibling.class, IGrandchild1.class, IGrandchild2.class); + assertCollection(index.getAllKnownSubinterfaces(IParent.class), IChild.class, IGrandchild1.class, IChild.class, + ISibling.class, IGrandchild1.class, IGrandchild2.class); + assertCollection(index.getAllKnownSubinterfaces(IChild.class), IGrandchild1.class, IGrandchild1.class, + IGrandchild2.class); + + assertCollection(index.getKnownDirectImplementors(IGrandparent.class), CGrandparent.class, IParent.class); + assertCollection(index.getKnownDirectImplementors(IParent.class), IChild.class, CParent.class, IChild.class, + ISibling.class); + assertCollection(index.getKnownDirectImplementors(IChild.class), IGrandchild1.class, CChild.class, IGrandchild1.class, + IGrandchild2.class, CChild.class); + assertCollection(index.getKnownDirectImplementors(ISibling.class), CSibling.class); + assertCollection(index.getKnownDirectImplementors(IGrandchild1.class), CGrandchild1.class, CGrandchild1.class); + assertCollection(index.getKnownDirectImplementors(IGrandchild2.class), CGrandchild2.class); + + assertCollection(index.getAllKnownImplementors(IGrandparent.class), CGrandparent.class, CParent.class, CChild.class, + CGrandchild1.class, CChild.class, CSibling.class, CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownImplementors(IParent.class), CParent.class, CChild.class, CGrandchild1.class, + CChild.class, CSibling.class, CGrandchild1.class, CGrandchild2.class); + // doesn't behave as expected, but the behavior is actually not defined + //assertCollection(index.getAllKnownImplementors(IChild.class), CChild.class, CGrandchild1.class, CChild.class, CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownImplementors(ISibling.class), CSibling.class); + assertCollection(index.getAllKnownImplementors(IGrandchild1.class), CGrandchild1.class); + assertCollection(index.getAllKnownImplementors(IGrandchild2.class), CGrandchild2.class); + + // doesn't contain duplicates, though it probably should (again, the behavior is not defined) + assertCollection(index.getClassesInPackage(IndexNavigationTest.class.getPackage().getName()), IGrandparent.class, + IParent.class, IChild.class, ISibling.class, IGrandchild1.class, IGrandchild2.class, CGrandparent.class, + CParent.class, CChild.class, CSibling.class, CGrandchild1.class, CGrandchild2.class); + } + + @Test + public void testStackedIndex() throws IOException { + Index index1 = Index.of(IGrandparent.class, IParent.class, IChild.class, IGrandchild1.class, CGrandparent.class, + CParent.class, CChild.class, CGrandchild1.class); + Index index2 = Index.of(ISibling.class, IGrandchild2.class, CSibling.class, CGrandchild2.class); + + IndexView index = StackedIndex.create(index1, index2); + testIndex(index); + + index = StackedIndex.create(IndexingUtil.roundtrip(index1), IndexingUtil.roundtrip(index2)); + testIndex(index); + } + + @Test + public void testOverlappingStackedIndex() throws IOException { + Index index1 = Index.of(IGrandparent.class, IParent.class, IChild.class, IGrandchild1.class, CGrandparent.class, + CParent.class, CChild.class, CGrandchild1.class); + Index index2 = Index.of(IChild.class, ISibling.class, IGrandchild1.class, IGrandchild2.class, CChild.class, + CSibling.class, CGrandchild1.class, CGrandchild2.class); + + IndexView index = StackedIndex.create(index1, index2); + testIndex(index); + + index = StackedIndex.create(IndexingUtil.roundtrip(index1), IndexingUtil.roundtrip(index2)); + testIndex(index); + } + + private void testIndex(IndexView index) { + testDirectSubclasses(index); + testAllSubclasses(index); + testDirectSubinterfaces(index); + testAllSubinterfaces(index); + testDirectImplementors(index); + testAllImplementors(index); + testClassesInPackage(index); + } + + private void testDirectSubclasses(IndexView index) { + assertCollection(index.getKnownDirectSubclasses(Object.class), IGrandparent.class, IParent.class, IChild.class, + ISibling.class, IGrandchild1.class, IGrandchild2.class, CGrandparent.class); + assertCollection(index.getKnownDirectSubclasses(IGrandparent.class)); + assertCollection(index.getKnownDirectSubclasses(IParent.class)); + assertCollection(index.getKnownDirectSubclasses(IChild.class)); + assertCollection(index.getKnownDirectSubclasses(ISibling.class)); + assertCollection(index.getKnownDirectSubclasses(IGrandchild1.class)); + assertCollection(index.getKnownDirectSubclasses(IGrandchild2.class)); + assertCollection(index.getKnownDirectSubclasses(CGrandparent.class), CParent.class); + assertCollection(index.getKnownDirectSubclasses(CParent.class), CChild.class, CSibling.class); + assertCollection(index.getKnownDirectSubclasses(CChild.class), CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getKnownDirectSubclasses(CSibling.class)); + assertCollection(index.getKnownDirectSubclasses(CGrandchild1.class)); + assertCollection(index.getKnownDirectSubclasses(CGrandchild2.class)); + } + + private void testAllSubclasses(IndexView index) { + assertCollection(index.getAllKnownSubclasses(Object.class), IGrandparent.class, IParent.class, IChild.class, + ISibling.class, IGrandchild1.class, IGrandchild2.class, CGrandparent.class, CParent.class, CChild.class, + CSibling.class, CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownSubclasses(IGrandparent.class)); + assertCollection(index.getAllKnownSubclasses(IParent.class)); + assertCollection(index.getAllKnownSubclasses(IChild.class)); + assertCollection(index.getAllKnownSubclasses(ISibling.class)); + assertCollection(index.getAllKnownSubclasses(IGrandchild1.class)); + assertCollection(index.getAllKnownSubclasses(IGrandchild2.class)); + assertCollection(index.getAllKnownSubclasses(CGrandparent.class), CParent.class, CChild.class, CSibling.class, + CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownSubclasses(CParent.class), CChild.class, CSibling.class, CGrandchild1.class, + CGrandchild2.class); + assertCollection(index.getAllKnownSubclasses(CChild.class), CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownSubclasses(CSibling.class)); + assertCollection(index.getAllKnownSubclasses(CGrandchild1.class)); + assertCollection(index.getAllKnownSubclasses(CGrandchild2.class)); + } + + private void testDirectSubinterfaces(IndexView index) { + assertCollection(index.getKnownDirectSubinterfaces(Object.class)); + assertCollection(index.getKnownDirectSubinterfaces(IGrandparent.class), IParent.class); + assertCollection(index.getKnownDirectSubinterfaces(IParent.class), IChild.class, ISibling.class); + assertCollection(index.getKnownDirectSubinterfaces(IChild.class), IGrandchild1.class, IGrandchild2.class); + assertCollection(index.getKnownDirectSubinterfaces(ISibling.class)); + assertCollection(index.getKnownDirectSubinterfaces(IGrandchild1.class)); + assertCollection(index.getKnownDirectSubinterfaces(IGrandchild2.class)); + assertCollection(index.getKnownDirectSubinterfaces(CGrandparent.class)); + assertCollection(index.getKnownDirectSubinterfaces(CParent.class)); + assertCollection(index.getKnownDirectSubinterfaces(CChild.class)); + assertCollection(index.getKnownDirectSubinterfaces(CSibling.class)); + assertCollection(index.getKnownDirectSubinterfaces(CGrandchild1.class)); + assertCollection(index.getKnownDirectSubinterfaces(CGrandchild2.class)); + } + + private void testAllSubinterfaces(IndexView index) { + assertCollection(index.getAllKnownSubinterfaces(Object.class)); + assertCollection(index.getAllKnownSubinterfaces(IGrandparent.class), IParent.class, IChild.class, ISibling.class, + IGrandchild1.class, IGrandchild2.class); + assertCollection(index.getAllKnownSubinterfaces(IParent.class), IChild.class, ISibling.class, IGrandchild1.class, + IGrandchild2.class); + assertCollection(index.getAllKnownSubinterfaces(IChild.class), IGrandchild1.class, IGrandchild2.class); + assertCollection(index.getAllKnownSubinterfaces(ISibling.class)); + assertCollection(index.getAllKnownSubinterfaces(IGrandchild1.class)); + assertCollection(index.getAllKnownSubinterfaces(IGrandchild2.class)); + assertCollection(index.getAllKnownSubinterfaces(CGrandparent.class)); + assertCollection(index.getAllKnownSubinterfaces(CParent.class)); + assertCollection(index.getAllKnownSubinterfaces(CChild.class)); + assertCollection(index.getAllKnownSubinterfaces(CSibling.class)); + assertCollection(index.getAllKnownSubinterfaces(CGrandchild1.class)); + assertCollection(index.getAllKnownSubinterfaces(CGrandchild2.class)); + } + + private void testDirectImplementors(IndexView index) { + assertCollection(index.getKnownDirectImplementors(Object.class)); + assertCollection(index.getKnownDirectImplementors(IGrandparent.class), CGrandparent.class, IParent.class); + assertCollection(index.getKnownDirectImplementors(IParent.class), CParent.class, IChild.class, ISibling.class); + assertCollection(index.getKnownDirectImplementors(IChild.class), CChild.class, IGrandchild1.class, IGrandchild2.class); + assertCollection(index.getKnownDirectImplementors(ISibling.class), CSibling.class); + assertCollection(index.getKnownDirectImplementors(IGrandchild1.class), CGrandchild1.class); + assertCollection(index.getKnownDirectImplementors(IGrandchild2.class), CGrandchild2.class); + assertCollection(index.getKnownDirectImplementors(CGrandparent.class)); + assertCollection(index.getKnownDirectImplementors(CParent.class)); + assertCollection(index.getKnownDirectImplementors(CChild.class)); + assertCollection(index.getKnownDirectImplementors(CSibling.class)); + assertCollection(index.getKnownDirectImplementors(CGrandchild1.class)); + assertCollection(index.getKnownDirectImplementors(CGrandchild2.class)); + } + + private void testAllImplementors(IndexView index) { + assertCollection(index.getAllKnownImplementors(Object.class)); + assertCollection(index.getAllKnownImplementors(IGrandparent.class), CGrandparent.class, CParent.class, CChild.class, + CSibling.class, CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownImplementors(IParent.class), CParent.class, CChild.class, CSibling.class, + CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownImplementors(IChild.class), CChild.class, CGrandchild1.class, CGrandchild2.class); + assertCollection(index.getAllKnownImplementors(ISibling.class), CSibling.class); + assertCollection(index.getAllKnownImplementors(IGrandchild1.class), CGrandchild1.class); + assertCollection(index.getAllKnownImplementors(IGrandchild2.class), CGrandchild2.class); + assertCollection(index.getAllKnownImplementors(CGrandparent.class)); + assertCollection(index.getAllKnownImplementors(CParent.class)); + assertCollection(index.getAllKnownImplementors(CChild.class)); + assertCollection(index.getAllKnownImplementors(CSibling.class)); + assertCollection(index.getAllKnownImplementors(CGrandchild1.class)); + assertCollection(index.getAllKnownImplementors(CGrandchild2.class)); + } + + private void testClassesInPackage(IndexView index) { + assertCollection(index.getClassesInPackage(IndexNavigationTest.class.getPackage().getName()), IGrandparent.class, + IParent.class, IChild.class, ISibling.class, IGrandchild1.class, IGrandchild2.class, CGrandparent.class, + CParent.class, CChild.class, CSibling.class, CGrandchild1.class, CGrandchild2.class); + } + + private static void assertCollection(Collection collection, Class... expectation) { + assertEquals(expectation.length, collection.size(), + "Expected " + expectation.length + " items in " + collection); + for (Class clazz : expectation) { + DotName name = DotName.createSimple(clazz); + assertTrue(collection.stream().anyMatch(it -> name.equals(it.name())), + "Expected " + name + " in " + collection); + } + } +} diff --git a/core/src/test/java/org/jboss/jandex/test/StackedIndexTest.java b/core/src/test/java/org/jboss/jandex/test/StackedIndexTest.java new file mode 100644 index 00000000..b7f38970 --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/StackedIndexTest.java @@ -0,0 +1,146 @@ +package org.jboss.jandex.test; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; + +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.jboss.jandex.AnnotationInstance; +import org.jboss.jandex.Index; +import org.jboss.jandex.Indexer; +import org.jboss.jandex.StackedIndex; +import org.jboss.jandex.test.util.IOUtil; +import org.junit.jupiter.api.Test; + +public class StackedIndexTest { + // this test only verifies annotation access, most of other methods are tested in `IndexNavigationTest` + + // `AnnotatedClass3` is intentionally a copy of `AnnotatedClass1` with modified annotation members, + // so that we can load its bytecode, replace all occurrences of `AnnotatedClass3` with `AnnotatedClass1`, + // index both of them separately and have two indexes with the same class, only with different annotations + // (`AnnotatedClass2` and `AnnotatedClass4` also look the same, but are still treated as different classes) + + @MyRepeatableAnnotation("cr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("cr2"), + @MyRepeatableAnnotation("cr3") + }) + @MyAnnotation("c1") + static class AnnotatedClass1<@MyAnnotation("c2") T extends @MyAnnotation("c3") Number> { + @MyRepeatableAnnotation("fr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("fr2"), + @MyRepeatableAnnotation("fr3") + }) + @MyAnnotation("f1") + Map<@MyAnnotation("f2") String, @MyAnnotation("f3") List> field; + + @MyRepeatableAnnotation("mr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("mr2"), + @MyRepeatableAnnotation("mr3") + }) + @MyAnnotation("m1") + boolean method( + @MyAnnotation("m2") Map<@MyAnnotation("m3") String, @MyAnnotation("m4") List> param, + @MyAnnotation("m6") int @MyAnnotation("m7") [] @MyAnnotation("m8") [] otherParam) { + return false; + } + } + + @MyAnnotation("extra1") + @MyRepeatableAnnotation("extra2") + static class AnnotatedClass2 { + } + + @MyRepeatableAnnotation("XXXcr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("XXXcr2"), + @MyRepeatableAnnotation("XXXcr3") + }) + @MyAnnotation("XXXc1") + static class AnnotatedClass3<@MyAnnotation("XXXc2") T extends @MyAnnotation("XXXc3") Number> { + @MyRepeatableAnnotation("XXXfr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("XXXfr2"), + @MyRepeatableAnnotation("XXXfr3") + }) + @MyAnnotation("XXXf1") + Map<@MyAnnotation("XXXf2") String, @MyAnnotation("XXXf3") List> field; + + @MyRepeatableAnnotation("XXXmr1") + @MyRepeatableAnnotation.List({ + @MyRepeatableAnnotation("XXXmr2"), + @MyRepeatableAnnotation("XXXmr3") + }) + @MyAnnotation("XXXm1") + boolean method( + @MyAnnotation("XXXm2") Map<@MyAnnotation("XXXm3") String, @MyAnnotation("XXXm4") List> param, + @MyAnnotation("XXXm6") int @MyAnnotation("XXXm7") [] @MyAnnotation("XXXm8") [] otherParam) { + return false; + } + } + + @MyAnnotation("extra3") + @MyRepeatableAnnotation("extra4") + static class AnnotatedClass4 { + } + + @Test + public void test() throws IOException { + Index upperIndex = Index.of(AnnotatedClass1.class, AnnotatedClass2.class); + Indexer indexer = new Indexer(); + indexer.index(new ByteArrayInputStream(annotatedClass3AsAnnotatedClass1())); + indexer.indexClass(AnnotatedClass4.class); + Index lowerIndex = indexer.complete(); + Index baseIndex = Index.of(MyAnnotation.class, MyRepeatableAnnotation.class); + + StackedIndex index = StackedIndex.create(baseIndex, lowerIndex, upperIndex); + + Collection annotations = index.getAnnotations(MyAnnotation.DOT_NAME); + assertEquals(19 + 2, annotations.size()); // 19 from AnnotatedClass1, 2 from AnnotatedClass2/4 + for (AnnotationInstance annotation : annotations) { + assertFalse(annotation.value().asString().startsWith("XXX")); + } + + // ecj also puts the `MyRepeatableAnnotation` and `MyRepeatableAnnotation.List` annotations + // on the _types_ of `AnnotatedClassN.field` and `method`, contrary to the `@Target` declarations + + annotations = index.getAnnotations(MyRepeatableAnnotation.DOT_NAME); + assertEquals(CompiledWith.ecj() ? 11 : 5, annotations.size()); + for (AnnotationInstance annotation : annotations) { + assertFalse(annotation.value().asString().startsWith("XXX")); + } + + annotations = index.getAnnotations(MyRepeatableAnnotation.List.DOT_NAME); + assertEquals(CompiledWith.ecj() ? 5 : 3, annotations.size()); + for (AnnotationInstance annotation : annotations) { + assertFalse(annotation.value().asString().startsWith("XXX")); + } + + annotations = index.getAnnotationsWithRepeatable(MyRepeatableAnnotation.DOT_NAME, index); + assertEquals(CompiledWith.ecj() ? 21 : 11, annotations.size()); + for (AnnotationInstance annotation : annotations) { + assertFalse(annotation.value().asString().startsWith("XXX")); + } + } + + private static byte[] annotatedClass3AsAnnotatedClass1() throws IOException { + ByteArrayOutputStream out = new ByteArrayOutputStream(); + String path = "/" + AnnotatedClass3.class.getName().replace('.', '/') + ".class"; + try (InputStream in = StackedIndexTest.class.getResourceAsStream(path)) { + IOUtil.copy(in, out); + } + byte[] clazz = out.toByteArray(); + IOUtil.searchAndReplace(clazz, "AnnotatedClass3".getBytes(StandardCharsets.UTF_8), + "AnnotatedClass1".getBytes(StandardCharsets.UTF_8)); + return clazz; + } +} diff --git a/core/src/test/java/org/jboss/jandex/test/util/IOUtil.java b/core/src/test/java/org/jboss/jandex/test/util/IOUtil.java new file mode 100644 index 00000000..fecb930d --- /dev/null +++ b/core/src/test/java/org/jboss/jandex/test/util/IOUtil.java @@ -0,0 +1,55 @@ +package org.jboss.jandex.test.util; + +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Objects; + +public class IOUtil { + private static final int BUFFER_SIZE = 8192; + + /** + * Copies the remaining of what's to read from given input stream into the given output stream. + * The given input stream is not closed, that is left to the caller. + */ + public static void copy(InputStream in, OutputStream out) throws IOException { + Objects.requireNonNull(in); + Objects.requireNonNull(out); + + byte[] buffer = new byte[BUFFER_SIZE]; + int n; + while ((n = in.read(buffer)) >= 0) { + out.write(buffer, 0, n); + } + } + + /** + * Finds all occurences of given {@code search} in given {@code array} and replaces them + * with given {@code replacement}. The {@code search} and {@code replacement} arrays + * must have the same length. + */ + public static void searchAndReplace(byte[] array, byte[] search, byte[] replacement) { + Objects.requireNonNull(array); + Objects.requireNonNull(search); + Objects.requireNonNull(replacement); + + if (search.length != replacement.length) { + throw new IllegalArgumentException("Search and replacement must have the same length"); + } + if (array.length < search.length) { + throw new IllegalArgumentException("Array must be at least as long as search"); + } + if (search.length == 0) { + return; + } + + outer: for (int i = 0; i < array.length - search.length + 1; i++) { + for (int j = 0; j < search.length; j++) { + if (array[i + j] != search[j]) { + continue outer; + } + } + System.arraycopy(replacement, 0, array, i, replacement.length); + } + } +}