Skip to content

Commit ff857f1

Browse files
authored
Merge pull request #722 from SentryMan/plugin-detection
Add annotation for module-path `InjectPlugin` auto-detection
2 parents 4992b44 + e3281dd commit ff857f1

File tree

11 files changed

+228
-55
lines changed

11 files changed

+228
-55
lines changed
Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import org.example.external.aspect.spi.AspectPlugin;
2+
13
module blackbox.aspect {
24

35
exports org.example.external.aspect;
@@ -6,7 +8,6 @@
68
requires io.avaje.inject;
79
requires io.avaje.inject.aop;
810

9-
//remove this and compilation fails
10-
provides io.avaje.inject.spi.InjectExtension with org.example.external.aspect.sub.ExampleExternalAspectModule;
11+
provides io.avaje.inject.spi.InjectExtension with AspectPlugin, org.example.external.aspect.sub.ExampleExternalAspectModule;
1112

1213
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package org.example.external.aspect;
2+
3+
public class PluginProvidedClass {}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
package org.example.external.aspect.spi;
2+
3+
import org.example.external.aspect.PluginProvidedClass;
4+
5+
import io.avaje.inject.BeanScopeBuilder;
6+
import io.avaje.inject.spi.InjectPlugin;
7+
import io.avaje.inject.spi.PluginProvides;
8+
9+
@PluginProvides(provides = PluginProvidedClass.class)
10+
public class AspectPlugin implements InjectPlugin {
11+
12+
@Override
13+
public Class<?>[] provides() {
14+
return new Class<?>[] {PluginProvidedClass.class};
15+
}
16+
17+
@Override
18+
public void apply(BeanScopeBuilder builder) {
19+
builder.beans(new PluginProvidedClass());
20+
}
21+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package org.example.myapp.other;
2+
3+
import org.example.external.aspect.PluginProvidedClass;
4+
import org.other.one.OtherComponent;
5+
6+
import jakarta.inject.Singleton;
7+
8+
@Singleton
9+
public class WireOther {
10+
OtherComponent component;
11+
PluginProvidedClass plugin;
12+
13+
public WireOther(OtherComponent component, PluginProvidedClass plugin) {
14+
this.component = component;
15+
this.plugin = plugin;
16+
}
17+
}

inject-generator/src/main/java/io/avaje/inject/generator/ExternalProvider.java

Lines changed: 128 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@
44
import static java.util.Map.entry;
55
import static java.util.stream.Collectors.toList;
66

7+
import java.io.FileWriter;
8+
import java.io.IOException;
79
import java.lang.reflect.Type;
810
import java.util.ArrayList;
911
import java.util.Arrays;
@@ -108,17 +110,9 @@ static void registerModuleProvidedTypes(Set<String> providedTypes) {
108110
* types and the only thing providing them is the plugin.
109111
*/
110112
static void registerPluginProvidedTypes(ScopeInfo defaultScope) {
111-
avajePlugins.forEach((k, v) -> {
112-
if (APContext.typeElement(k) != null) {
113-
APContext.logNote("Loaded Plugin: %s", k);
114-
v.forEach(defaultScope::pluginProvided);
115-
}
116-
});
117-
defaultScope.pluginProvided("io.avaje.inject.event.ObserverManager");
118113
if (!INJECT_AVAILABLE) {
119114
if (!pluginExists("avaje-module-dependencies.csv")) {
120-
APContext.logNote(
121-
"Unable to detect Avaje Inject in Annotation Processor ClassPath, use the Avaje Inject Maven/Gradle plugin for detecting Inject Plugins from dependencies");
115+
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");
122116
}
123117
return;
124118
}
@@ -168,67 +162,153 @@ static void readMetaDataProvides(Collection<String> providedTypes) {
168162
});
169163
}
170164

171-
static void scanTheWorld(Collection<String> providedTypes) {
165+
static void scanAllInjectPlugins(ScopeInfo defaultScope) {
166+
final var hasPlugins = !defaultScope.pluginProvided().isEmpty();
167+
avajePlugins.forEach((k, v) -> {
168+
if (APContext.typeElement(k) != null) {
169+
APContext.logNote("Loaded Plugin: %s", k);
170+
v.forEach(defaultScope::pluginProvided);
171+
}
172+
});
173+
defaultScope.pluginProvided("io.avaje.inject.event.ObserverManager");
174+
if (hasPlugins) {
175+
return;
176+
}
177+
178+
injectExtensions()
179+
.filter(PluginProvidesPrism::isPresent)
180+
.distinct()
181+
.forEach(pluginType -> addPluginToScope(defaultScope, pluginType));
182+
183+
if (defaultScope.pluginProvided().isEmpty()) {
184+
APContext.logNote("No external plugins detected");
185+
}
186+
writePluginProvides(defaultScope);
187+
}
188+
189+
private static void writePluginProvides(ScopeInfo defaultScope) {
190+
// write detected plugins to a text file for test compilation
191+
try (final var pluginWriter = new FileWriter(APContext.getBuildResource("avaje-plugin-provides.txt").toFile())) {
192+
for (var providedType : defaultScope.pluginProvided()) {
193+
pluginWriter.write(providedType);
194+
pluginWriter.write("\n");
195+
}
196+
} catch (IOException e) {
197+
APContext.logWarn("Failed to write avaje-plugin-provides.txt due to %s", e.getMessage());
198+
}
199+
}
200+
201+
private static void addPluginToScope(ScopeInfo defaultScope, TypeElement pluginType) {
202+
final var name = pluginType.getQualifiedName().toString();
203+
if (avajePlugins.containsKey(name)) {
204+
return;
205+
}
206+
var prism = PluginProvidesPrism.getInstanceOn(pluginType);
207+
for (final var provide : prism.provides()) {
208+
defaultScope.pluginProvided(provide.toString());
209+
}
210+
for (final var provide : prism.providesStrings()) {
211+
defaultScope.pluginProvided(provide);
212+
}
213+
for (final var provide : prism.providesAspects()) {
214+
defaultScope.pluginProvided(Util.wrapAspect(provide.toString()));
215+
}
216+
APContext.logNote("Loaded Plugin: %s", name);
217+
}
218+
219+
static void scanAllAvajeModules(Collection<String> providedTypes) {
172220
if (!externalMeta.isEmpty()) {
173221
return;
174222
}
175-
var allModules =
223+
final var types = APContext.types();
224+
final var avajeModuleType = APContext.typeElement("io.avaje.inject.spi.AvajeModule").asType();
225+
injectExtensions()
226+
.filter(t -> t.getInterfaces().stream().anyMatch(i -> types.isAssignable(i, avajeModuleType)))
227+
.distinct()
228+
.forEach(otherModule -> addOtherModuleProvides(providedTypes, otherModule));
229+
230+
if (externalMeta.isEmpty()) {
231+
APContext.logNote("No external modules detected");
232+
}
233+
writeModuleDependencies();
234+
}
235+
236+
private static void writeModuleDependencies() {
237+
// write detected modules to a csv for test compilation
238+
try (final var moduleWriter = new FileWriter(APContext.getBuildResource("avaje-module-dependencies.csv").toFile())) {
239+
moduleWriter.write("External Module Type|Provides|Requires");
240+
for (ModuleData avajeModule : ProcessingContext.modules()) {
241+
moduleWriter.write("\n");
242+
moduleWriter.write(avajeModule.name());
243+
moduleWriter.write("|");
244+
var provides = String.join(",", avajeModule.provides());
245+
moduleWriter.write(provides.isEmpty() ? " " : provides);
246+
moduleWriter.write("|");
247+
var requires = String.join(",", avajeModule.requires());
248+
moduleWriter.write(requires.isEmpty() ? " " : requires);
249+
}
250+
251+
} catch (IOException e) {
252+
APContext.logWarn("Failed to write avaje-module-dependencies.csv due to %s", e.getMessage());
253+
}
254+
}
255+
256+
private static void addOtherModuleProvides(Collection<String> providedTypes, TypeElement otherModule) {
257+
final var provides = new HashSet<String>();
258+
final var requires = new HashSet<String>();
259+
260+
ElementFilter.methodsIn(otherModule.getEnclosedElements()).stream()
261+
.map(DependencyMetaPrism::getInstanceOn)
262+
.filter(Objects::nonNull)
263+
.map(MetaData::new)
264+
.forEach(m -> {
265+
externalMeta.add(m);
266+
provides.addAll(m.autoProvides());
267+
provides.addAll(m.provides());
268+
m.dependsOn().stream()
269+
.filter(d -> !d.isSoftDependency())
270+
.map(Dependency::name)
271+
.forEach(requires::add);
272+
273+
providedTypes.add(m.key());
274+
providedTypes.add(m.type());
275+
providedTypes.addAll(Util.addQualifierSuffix(m.provides(), m.name()));
276+
providedTypes.addAll(Util.addQualifierSuffix(m.autoProvides(), m.name()));
277+
});
278+
279+
final var name = otherModule.getQualifiedName().toString();
280+
APContext.logNote("Detected Module: %s", name);
281+
ProcessingContext.addModule(new ModuleData(name, List.copyOf(provides), List.copyOf(requires)));
282+
}
283+
284+
private static Stream<TypeElement> injectExtensions() {
285+
final var allModules =
176286
APContext.elements().getAllModuleElements().stream()
177287
.filter(m -> !m.getQualifiedName().toString().startsWith("java"))
178288
.filter(m -> !m.getQualifiedName().toString().startsWith("jdk"))
179289
// for whatever reason, compilation breaks if we don't filter out the current module
180-
.filter(m -> m != APContext.getProjectModuleElement())
290+
.filter(m -> !m.equals(APContext.getProjectModuleElement()))
181291
.collect(toList());
182292

183-
var types = APContext.types();
184-
var spi = APContext.typeElement("io.avaje.inject.spi.AvajeModule").asType();
293+
final var types = APContext.types();
294+
final var extensionType = APContext.typeElement("io.avaje.inject.spi.InjectExtension").asType();
295+
185296
final var checkEnclosing =
186297
allModules.stream()
187298
.flatMap(m -> m.getEnclosedElements().stream())
188299
.flatMap(p -> p.getEnclosedElements().stream())
189300
.map(TypeElement.class::cast)
190301
.filter(t -> t.getKind() == ElementKind.CLASS)
191-
.filter(t -> t.getModifiers().contains(Modifier.PUBLIC));
302+
.filter(t -> t.getModifiers().contains(Modifier.PUBLIC))
303+
.filter(t -> types.isAssignable(t.asType(), extensionType));
192304

193305
final var checkDirectives =
194306
allModules.stream()
195307
.flatMap(m -> ElementFilter.providesIn(m.getDirectives()).stream())
196308
.filter(ExternalProvider::isInjectExtension)
197309
.flatMap(p -> p.getImplementations().stream());
198310

199-
Stream.concat(checkEnclosing, checkDirectives)
200-
.filter(t -> t.getInterfaces().stream().anyMatch(i -> types.isAssignable(i, spi)))
201-
.distinct()
202-
.forEach(t -> {
203-
final var provides = new HashSet<String>();
204-
final var requires = new HashSet<String>();
205-
206-
ElementFilter.methodsIn(t.getEnclosedElements()).stream()
207-
.map(DependencyMetaPrism::getInstanceOn)
208-
.filter(Objects::nonNull)
209-
.map(MetaData::new)
210-
.forEach(m -> {
211-
externalMeta.add(m);
212-
provides.addAll(m.autoProvides());
213-
provides.addAll(m.provides());
214-
m.dependsOn().stream()
215-
.filter(d -> !d.isSoftDependency())
216-
.map(Dependency::name)
217-
.forEach(requires::add);
218-
219-
providedTypes.add(m.key());
220-
providedTypes.add(m.type());
221-
providedTypes.addAll(Util.addQualifierSuffix(m.provides(), m.name()));
222-
providedTypes.addAll(Util.addQualifierSuffix(m.autoProvides(), m.name()));
223-
});
224-
225-
final var name = t.getQualifiedName().toString();
226-
APContext.logNote("Detected Module: %s", name);
227-
ProcessingContext.addModule(new ModuleData(name, List.copyOf(provides), List.copyOf(requires)));
228-
});
229-
if (externalMeta.isEmpty()) {
230-
APContext.logNote("No external modules detected");
231-
}
311+
return Stream.concat(checkEnclosing, checkDirectives);
232312
}
233313

234314
private static boolean isInjectExtension(ModuleElement.ProvidesDirective p) {

inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -39,11 +39,12 @@
3939
FactoryPrism.PRISM_TYPE,
4040
ImportPrism.PRISM_TYPE,
4141
InjectModulePrism.PRISM_TYPE,
42+
PluginProvidesPrism.PRISM_TYPE,
4243
PrototypePrism.PRISM_TYPE,
4344
QualifierPrism.PRISM_TYPE,
4445
ScopePrism.PRISM_TYPE,
4546
SingletonPrism.PRISM_TYPE,
46-
"io.avaje.spi.ServiceProvider"
47+
ServiceProviderPrism.PRISM_TYPE
4748
})
4849
public final class InjectProcessor extends AbstractProcessor {
4950

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

161-
maybeElements(roundEnv, "io.avaje.spi.ServiceProvider").ifPresent(this::registerSPI);
162+
maybeElements(roundEnv, ServiceProviderPrism.PRISM_TYPE).ifPresent(this::registerSPI);
163+
maybeElements(roundEnv, PluginProvidesPrism.PRISM_TYPE).ifPresent(this::registerSPI);
162164
allScopes.readBeans(roundEnv);
163165
defaultScope.write(processingOver);
164166
allScopes.write(processingOver);
@@ -348,6 +350,17 @@ private void registerSPI(Set<? extends Element> beans) {
348350
}
349351

350352
private boolean isExtension(TypeElement te) {
353+
PluginProvidesPrism.getOptionalOn(te).ifPresent(t -> {
354+
if (!APContext.isAssignable(te, "io.avaje.inject.spi.InjectPlugin")) {
355+
APContext.logError(te, "PluginProvides can only be placed on io.avaje.inject.spi.InjectPlugin");
356+
}
357+
});
358+
ServiceProviderPrism.getOptionalOn(te).ifPresent(t -> {
359+
if (APContext.isAssignable(te, "io.avaje.inject.spi.InjectPlugin")) {
360+
APContext.logWarn(te, "PluginProvides should be used to auto register InjectPlugins");
361+
}
362+
});
363+
351364
return APContext.isAssignable(te, "io.avaje.inject.spi.InjectExtension");
352365
}
353366

inject-generator/src/main/java/io/avaje/inject/generator/ProcessingContext.java

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -249,7 +249,8 @@ private static void readExistingMetaInfServices() {
249249
}
250250
}
251251

252-
static void registerExternalProvidedTypes() {
253-
ExternalProvider.scanTheWorld(CTX.get().providedTypes);
252+
static void registerExternalProvidedTypes(ScopeInfo scopeInfo) {
253+
ExternalProvider.scanAllInjectPlugins(scopeInfo);
254+
ExternalProvider.scanAllAvajeModules(CTX.get().providedTypes);
254255
}
255256
}

inject-generator/src/main/java/io/avaje/inject/generator/ScopeInfo.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -343,7 +343,7 @@ void write(boolean processingOver) {
343343
writeBeanHelpers();
344344
initialiseModule();
345345
if (processingOver && !metaData.isEmpty()) {
346-
ProcessingContext.registerExternalProvidedTypes();
346+
ProcessingContext.registerExternalProvidedTypes(this);
347347
writeModule();
348348
}
349349
}

inject-generator/src/main/java/io/avaje/inject/generator/package-info.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
@GeneratePrism(BeanTypes.class)
1616
@GeneratePrism(Lazy.class)
1717
@GeneratePrism(Named.class)
18+
@GeneratePrism(PluginProvides.class)
1819
@GeneratePrism(PreDestroy.class)
1920
@GeneratePrism(Primary.class)
2021
@GeneratePrism(Profile.class)
@@ -29,6 +30,7 @@
2930
@GeneratePrism(Singleton.class)
3031
@GeneratePrism(Scope.class)
3132
@GeneratePrism(Secondary.class)
33+
@GeneratePrism(io.avaje.spi.ServiceProvider.class)
3234
package io.avaje.inject.generator;
3335

3436
import io.avaje.inject.*;

inject-generator/src/main/java/module-info.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
requires io.avaje.inject.events;
77

88
requires static io.avaje.prism;
9+
requires static io.avaje.spi;
910

1011
uses io.avaje.inject.spi.InjectExtension;
1112
uses io.avaje.inject.spi.Plugin;

0 commit comments

Comments
 (0)