Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add annotation for module-path InjectPlugin auto-detection #722

Merged
merged 11 commits into from
Nov 5, 2024
5 changes: 3 additions & 2 deletions blackbox-aspect/src/main/java/module-info.java
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import org.example.external.aspect.spi.AspectPlugin;

module blackbox.aspect {

exports org.example.external.aspect;
Expand All @@ -6,7 +8,6 @@
requires io.avaje.inject;
requires io.avaje.inject.aop;

//remove this and compilation fails
provides io.avaje.inject.spi.InjectExtension with org.example.external.aspect.sub.ExampleExternalAspectModule;
provides io.avaje.inject.spi.InjectExtension with AspectPlugin, org.example.external.aspect.sub.ExampleExternalAspectModule;

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package org.example.external.aspect;

public class PluginProvidedClass {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package org.example.external.aspect.spi;

import org.example.external.aspect.PluginProvidedClass;

import io.avaje.inject.BeanScopeBuilder;
import io.avaje.inject.spi.InjectPlugin;
import io.avaje.inject.spi.PluginProvides;

@PluginProvides(provides = PluginProvidedClass.class)
public class AspectPlugin implements InjectPlugin {

@Override
public Class<?>[] provides() {
return new Class<?>[] {PluginProvidedClass.class};
}

@Override
public void apply(BeanScopeBuilder builder) {
builder.beans(new PluginProvidedClass());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package org.example.myapp.other;

import org.example.external.aspect.PluginProvidedClass;
import org.other.one.OtherComponent;

import jakarta.inject.Singleton;

@Singleton
public class WireOther {
OtherComponent component;
PluginProvidedClass plugin;

public WireOther(OtherComponent component, PluginProvidedClass plugin) {
this.component = component;
this.plugin = plugin;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
import static java.util.Map.entry;
import static java.util.stream.Collectors.toList;

import java.io.FileWriter;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Arrays;
Expand Down Expand Up @@ -108,17 +110,9 @@ static void registerModuleProvidedTypes(Set<String> providedTypes) {
* types and the only thing providing them is the plugin.
*/
static void registerPluginProvidedTypes(ScopeInfo defaultScope) {
avajePlugins.forEach((k, v) -> {
if (APContext.typeElement(k) != null) {
APContext.logNote("Loaded Plugin: %s", k);
v.forEach(defaultScope::pluginProvided);
}
});
defaultScope.pluginProvided("io.avaje.inject.event.ObserverManager");
if (!INJECT_AVAILABLE) {
if (!pluginExists("avaje-module-dependencies.csv")) {
APContext.logNote(
"Unable to detect Avaje Inject in Annotation Processor ClassPath, use the Avaje Inject Maven/Gradle plugin for detecting Inject Plugins from dependencies");
APContext.logNote("Unable to detect Avaje Inject Maven/Gradle plugin, use the Avaje Inject Maven/Gradle plugin for auto detecting External Inject Plugins/Modules from dependencies");
}
return;
}
Expand Down Expand Up @@ -168,67 +162,153 @@ static void readMetaDataProvides(Collection<String> providedTypes) {
});
}

static void scanTheWorld(Collection<String> providedTypes) {
static void scanAllInjectPlugins(ScopeInfo defaultScope) {
final var hasPlugins = !defaultScope.pluginProvided().isEmpty();
avajePlugins.forEach((k, v) -> {
if (APContext.typeElement(k) != null) {
APContext.logNote("Loaded Plugin: %s", k);
v.forEach(defaultScope::pluginProvided);
}
});
defaultScope.pluginProvided("io.avaje.inject.event.ObserverManager");
if (hasPlugins) {
return;
}

injectExtensions()
.filter(PluginProvidesPrism::isPresent)
.distinct()
.forEach(pluginType -> addPluginToScope(defaultScope, pluginType));

if (defaultScope.pluginProvided().isEmpty()) {
APContext.logNote("No external plugins detected");
}
writePluginProvides(defaultScope);
}

private static void writePluginProvides(ScopeInfo defaultScope) {
// write detected plugins to a text file for test compilation
try (final var pluginWriter = new FileWriter(APContext.getBuildResource("avaje-plugin-provides.txt").toFile())) {
for (var providedType : defaultScope.pluginProvided()) {
pluginWriter.write(providedType);
pluginWriter.write("\n");
}
} catch (IOException e) {
APContext.logWarn("Failed to write avaje-plugin-provides.txt due to %s", e.getMessage());
}
}

private static void addPluginToScope(ScopeInfo defaultScope, TypeElement pluginType) {
final var name = pluginType.getQualifiedName().toString();
if (avajePlugins.containsKey(name)) {
return;
}
var prism = PluginProvidesPrism.getInstanceOn(pluginType);
for (final var provide : prism.provides()) {
defaultScope.pluginProvided(provide.toString());
}
for (final var provide : prism.providesStrings()) {
defaultScope.pluginProvided(provide);
}
for (final var provide : prism.providesAspects()) {
defaultScope.pluginProvided(Util.wrapAspect(provide.toString()));
}
APContext.logNote("Loaded Plugin: %s", name);
}

static void scanAllAvajeModules(Collection<String> providedTypes) {
if (!externalMeta.isEmpty()) {
return;
}
var allModules =
final var types = APContext.types();
final var avajeModuleType = APContext.typeElement("io.avaje.inject.spi.AvajeModule").asType();
injectExtensions()
.filter(t -> t.getInterfaces().stream().anyMatch(i -> types.isAssignable(i, avajeModuleType)))
.distinct()
.forEach(otherModule -> addOtherModuleProvides(providedTypes, otherModule));

if (externalMeta.isEmpty()) {
APContext.logNote("No external modules detected");
}
writeModuleDependencies();
}

private static void writeModuleDependencies() {
// write detected modules to a csv for test compilation
try (final var moduleWriter = new FileWriter(APContext.getBuildResource("avaje-module-dependencies.csv").toFile())) {
moduleWriter.write("External Module Type|Provides|Requires");
for (ModuleData avajeModule : ProcessingContext.modules()) {
moduleWriter.write("\n");
moduleWriter.write(avajeModule.name());
moduleWriter.write("|");
var provides = String.join(",", avajeModule.provides());
moduleWriter.write(provides.isEmpty() ? " " : provides);
moduleWriter.write("|");
var requires = String.join(",", avajeModule.requires());
moduleWriter.write(requires.isEmpty() ? " " : requires);
}

} catch (IOException e) {
APContext.logWarn("Failed to write avaje-module-dependencies.csv due to %s", e.getMessage());
}
}

private static void addOtherModuleProvides(Collection<String> providedTypes, TypeElement otherModule) {
final var provides = new HashSet<String>();
final var requires = new HashSet<String>();

ElementFilter.methodsIn(otherModule.getEnclosedElements()).stream()
.map(DependencyMetaPrism::getInstanceOn)
.filter(Objects::nonNull)
.map(MetaData::new)
.forEach(m -> {
externalMeta.add(m);
provides.addAll(m.autoProvides());
provides.addAll(m.provides());
m.dependsOn().stream()
.filter(d -> !d.isSoftDependency())
.map(Dependency::name)
.forEach(requires::add);

providedTypes.add(m.key());
providedTypes.add(m.type());
providedTypes.addAll(Util.addQualifierSuffix(m.provides(), m.name()));
providedTypes.addAll(Util.addQualifierSuffix(m.autoProvides(), m.name()));
});

final var name = otherModule.getQualifiedName().toString();
APContext.logNote("Detected Module: %s", name);
ProcessingContext.addModule(new ModuleData(name, List.copyOf(provides), List.copyOf(requires)));
}

private static Stream<TypeElement> injectExtensions() {
final var allModules =
APContext.elements().getAllModuleElements().stream()
.filter(m -> !m.getQualifiedName().toString().startsWith("java"))
.filter(m -> !m.getQualifiedName().toString().startsWith("jdk"))
// for whatever reason, compilation breaks if we don't filter out the current module
.filter(m -> m != APContext.getProjectModuleElement())
.filter(m -> !m.equals(APContext.getProjectModuleElement()))
.collect(toList());

var types = APContext.types();
var spi = APContext.typeElement("io.avaje.inject.spi.AvajeModule").asType();
final var types = APContext.types();
final var extensionType = APContext.typeElement("io.avaje.inject.spi.InjectExtension").asType();

final var checkEnclosing =
allModules.stream()
.flatMap(m -> m.getEnclosedElements().stream())
.flatMap(p -> p.getEnclosedElements().stream())
.map(TypeElement.class::cast)
.filter(t -> t.getKind() == ElementKind.CLASS)
.filter(t -> t.getModifiers().contains(Modifier.PUBLIC));
.filter(t -> t.getModifiers().contains(Modifier.PUBLIC))
.filter(t -> types.isAssignable(t.asType(), extensionType));

final var checkDirectives =
allModules.stream()
.flatMap(m -> ElementFilter.providesIn(m.getDirectives()).stream())
.filter(ExternalProvider::isInjectExtension)
.flatMap(p -> p.getImplementations().stream());

Stream.concat(checkEnclosing, checkDirectives)
.filter(t -> t.getInterfaces().stream().anyMatch(i -> types.isAssignable(i, spi)))
.distinct()
.forEach(t -> {
final var provides = new HashSet<String>();
final var requires = new HashSet<String>();

ElementFilter.methodsIn(t.getEnclosedElements()).stream()
.map(DependencyMetaPrism::getInstanceOn)
.filter(Objects::nonNull)
.map(MetaData::new)
.forEach(m -> {
externalMeta.add(m);
provides.addAll(m.autoProvides());
provides.addAll(m.provides());
m.dependsOn().stream()
.filter(d -> !d.isSoftDependency())
.map(Dependency::name)
.forEach(requires::add);

providedTypes.add(m.key());
providedTypes.add(m.type());
providedTypes.addAll(Util.addQualifierSuffix(m.provides(), m.name()));
providedTypes.addAll(Util.addQualifierSuffix(m.autoProvides(), m.name()));
});

final var name = t.getQualifiedName().toString();
APContext.logNote("Detected Module: %s", name);
ProcessingContext.addModule(new ModuleData(name, List.copyOf(provides), List.copyOf(requires)));
});
if (externalMeta.isEmpty()) {
APContext.logNote("No external modules detected");
}
return Stream.concat(checkEnclosing, checkDirectives);
}

private static boolean isInjectExtension(ModuleElement.ProvidesDirective p) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,11 +39,12 @@
FactoryPrism.PRISM_TYPE,
ImportPrism.PRISM_TYPE,
InjectModulePrism.PRISM_TYPE,
PluginProvidesPrism.PRISM_TYPE,
PrototypePrism.PRISM_TYPE,
QualifierPrism.PRISM_TYPE,
ScopePrism.PRISM_TYPE,
SingletonPrism.PRISM_TYPE,
"io.avaje.spi.ServiceProvider"
ServiceProviderPrism.PRISM_TYPE
})
public final class InjectProcessor extends AbstractProcessor {

Expand Down Expand Up @@ -158,7 +159,8 @@ public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment
ProcessingContext.addOptionalType(type.fullWithoutAnnotations(), null);
});

maybeElements(roundEnv, "io.avaje.spi.ServiceProvider").ifPresent(this::registerSPI);
maybeElements(roundEnv, ServiceProviderPrism.PRISM_TYPE).ifPresent(this::registerSPI);
maybeElements(roundEnv, PluginProvidesPrism.PRISM_TYPE).ifPresent(this::registerSPI);
allScopes.readBeans(roundEnv);
defaultScope.write(processingOver);
allScopes.write(processingOver);
Expand Down Expand Up @@ -348,6 +350,17 @@ private void registerSPI(Set<? extends Element> beans) {
}

private boolean isExtension(TypeElement te) {
PluginProvidesPrism.getOptionalOn(te).ifPresent(t -> {
if (!APContext.isAssignable(te, "io.avaje.inject.spi.InjectPlugin")) {
APContext.logError(te, "PluginProvides can only be placed on io.avaje.inject.spi.InjectPlugin");
}
});
ServiceProviderPrism.getOptionalOn(te).ifPresent(t -> {
if (APContext.isAssignable(te, "io.avaje.inject.spi.InjectPlugin")) {
APContext.logWarn(te, "PluginProvides should be used to auto register InjectPlugins");
}
});

return APContext.isAssignable(te, "io.avaje.inject.spi.InjectExtension");
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,8 @@ private static void readExistingMetaInfServices() {
}
}

static void registerExternalProvidedTypes() {
ExternalProvider.scanTheWorld(CTX.get().providedTypes);
static void registerExternalProvidedTypes(ScopeInfo scopeInfo) {
ExternalProvider.scanAllInjectPlugins(scopeInfo);
ExternalProvider.scanAllAvajeModules(CTX.get().providedTypes);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -343,7 +343,7 @@ void write(boolean processingOver) {
writeBeanHelpers();
initialiseModule();
if (processingOver && !metaData.isEmpty()) {
ProcessingContext.registerExternalProvidedTypes();
ProcessingContext.registerExternalProvidedTypes(this);
writeModule();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
@GeneratePrism(BeanTypes.class)
@GeneratePrism(Lazy.class)
@GeneratePrism(Named.class)
@GeneratePrism(PluginProvides.class)
@GeneratePrism(PreDestroy.class)
@GeneratePrism(Primary.class)
@GeneratePrism(Profile.class)
Expand All @@ -29,6 +30,7 @@
@GeneratePrism(Singleton.class)
@GeneratePrism(Scope.class)
@GeneratePrism(Secondary.class)
@GeneratePrism(io.avaje.spi.ServiceProvider.class)
package io.avaje.inject.generator;

import io.avaje.inject.*;
Expand Down
1 change: 1 addition & 0 deletions inject-generator/src/main/java/module-info.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
requires io.avaje.inject.events;

requires static io.avaje.prism;
requires static io.avaje.spi;

uses io.avaje.inject.spi.InjectExtension;
uses io.avaje.inject.spi.Plugin;
Expand Down
Loading
Loading