diff --git a/blackbox-other/src/main/java/org/other/one/events/SomeOptionalDep$Publisher.java b/blackbox-other/src/main/java/org/other/one/events/SomeOptionalDep$Publisher.java new file mode 100644 index 000000000..5d5eeb027 --- /dev/null +++ b/blackbox-other/src/main/java/org/other/one/events/SomeOptionalDep$Publisher.java @@ -0,0 +1,19 @@ +package org.other.one.events; + +import io.avaje.inject.Component; +import io.avaje.inject.event.Event; +import io.avaje.inject.event.ObserverManager; +import io.avaje.inject.spi.Generated; +import java.lang.reflect.Type; +import org.other.one.SomeOptionalDep; + +@Component +@Generated("avaje-inject-generator") +public class SomeOptionalDep$Publisher extends Event { + + private static final Type TYPE = SomeOptionalDep.class; + + public SomeOptionalDep$Publisher(ObserverManager manager) { + super(manager, TYPE, ""); + } +} diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalEventPublisher.java b/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalEventPublisher.java new file mode 100644 index 000000000..5f9a75732 --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalEventPublisher.java @@ -0,0 +1,22 @@ +package org.example.myapp.events; + +import org.example.external.aspect.MyExternalAspect; +import org.other.one.SomeOptionalDep; + +import io.avaje.inject.Component; +import io.avaje.inject.event.Event; + +@Component +public class ExternalEventPublisher { + Event event; + + public ExternalEventPublisher(Event event) { + + this.event = event; + } + + @MyExternalAspect + public void fire(SomeOptionalDep dep) { + event.fire(dep); + } +} diff --git a/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalObserver.java b/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalObserver.java new file mode 100644 index 000000000..0b06a7361 --- /dev/null +++ b/blackbox-test-inject/src/main/java/org/example/myapp/events/ExternalObserver.java @@ -0,0 +1,19 @@ +package org.example.myapp.events; + +import org.other.one.SomeOptionalDep; + +import io.avaje.inject.event.Observes; +import jakarta.inject.Singleton; + +@Singleton +public class ExternalObserver { + + boolean invoked; + SomeOptionalDep recievedEvent; + + public void observe(@Observes SomeOptionalDep event) { + invoked = true; + + this.recievedEvent = event; + } +} diff --git a/blackbox-test-inject/src/test/java/org/example/myapp/events/TestEventMessaging.java b/blackbox-test-inject/src/test/java/org/example/myapp/events/TestEventMessaging.java new file mode 100644 index 000000000..23cc78e85 --- /dev/null +++ b/blackbox-test-inject/src/test/java/org/example/myapp/events/TestEventMessaging.java @@ -0,0 +1,33 @@ +package org.example.myapp.events; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.other.one.SomeOptionalDep; + +import io.avaje.inject.test.InjectTest; +import jakarta.inject.Inject; + +@InjectTest +class TestEventMessaging { + + @Inject ExternalObserver observer; + @Inject ExternalEventPublisher event; + + @BeforeEach + void before() { + observer.invoked = false; + observer.recievedEvent = null; + } + + @Test + void test() { + var message = new SomeOptionalDep() {}; + + event.fire(message); + + assertThat(observer.invoked).isTrue(); + assertThat(observer.recievedEvent).isSameAs(message); + } +} diff --git a/inject-generator/pom.xml b/inject-generator/pom.xml index 07cad1330..4cddd1628 100644 --- a/inject-generator/pom.xml +++ b/inject-generator/pom.xml @@ -51,6 +51,19 @@ false + + org.apache.maven.plugins + maven-compiler-plugin + + + + io.avaje + avaje-prisms + ${avaje.prisms.version} + + + + diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/EventPublisherWriter.java b/inject-generator/src/main/java/io/avaje/inject/generator/EventPublisherWriter.java index f9ebe45ad..94c68959d 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/EventPublisherWriter.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/EventPublisherWriter.java @@ -2,42 +2,93 @@ import static io.avaje.inject.generator.APContext.createSourceFile; import static io.avaje.inject.generator.APContext.logError; +import static java.util.function.Predicate.not; import java.io.IOException; import java.io.Writer; +import java.lang.reflect.Type; import java.text.MessageFormat; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; import javax.lang.model.element.Element; import javax.tools.JavaFileObject; /** Write the source code for the bean. */ final class EventPublisherWriter { - - private static final String TEMPLATE = + private static final Map GENERATED_PUBLISHERS = new HashMap<>(); + private final String TEMPLATE = "package {0};\n\n" + "{1}" + "@Component\n" + + "{2}" + "@Generated(\"avaje-inject-generator\")\n" - + "public class {2}Publisher extends Event<{2}> '{'\n" + + "public class {3} extends Event<{4}> '{'\n" + + "\n" + + " private static final Type TYPE = {5};\n" + "\n" - + " public {2}Publisher(ObserverManager manager) '{'\n" - + " super(manager, {2}.class);\n" + + " public {3}(ObserverManager manager) '{'\n" + + " super(manager, TYPE, \"{6}\");\n" + " '}'\n" + "'}'\n"; private final String originName; private final ImportTypeMap importTypes = new ImportTypeMap(); private final UType utype; private final String packageName; + private final String qualifier; - EventPublisherWriter(Element element) { - this.packageName = APContext.elements().getPackageOf(element).getQualifiedName().toString(); - this.utype = UType.parse(element.asType()); - this.originName = utype.mainType() + "Publisher"; - importTypes.addAll(utype.importTypes()); - if (utype.isGeneric()) { - APContext.logError(element, "Event publishers generation may not be used for generic classes. Generic event publishers must be constructed manually"); + static void write(Element element) { + new EventPublisherWriter(element); + } + + private EventPublisherWriter(Element element) { + final var asType = element.asType(); + this.utype = UType.parse(asType).param0(); + + this.packageName = + APContext.elements() + .getPackageOf(APContext.typeElement(utype.mainType())) + .getQualifiedName() + .toString() + .replaceFirst("java.", "") + + ".events"; + qualifier = Optional.ofNullable(Util.getNamed(element)).orElse(""); + var className = + packageName + + "." + + (qualifier.isEmpty() ? "" : "Qualified") + + Util.shortName(utype).replace(".", "_") + + "$Publisher"; + + originName = getUniqueClassName(className, 0); + + if (GENERATED_PUBLISHERS.containsKey(originName)) { + return; } + importTypes.addAll(utype.importTypes()); write(); + GENERATED_PUBLISHERS.put(originName, qualifier); + } + + private String getUniqueClassName(String className, Integer recursiveIndex) { + + Optional.ofNullable(APContext.typeElement(className)) + .ifPresent( + e -> + GENERATED_PUBLISHERS.put( + e.getQualifiedName().toString(), + Optional.ofNullable(Util.getNamed(e)).orElse(""))); + + if (Optional.ofNullable(GENERATED_PUBLISHERS.get(className)) + .filter(not(qualifier::equals)) + .isPresent()) { + var index = className.indexOf("$Publisher"); + className = className.substring(0, index) + "$Publisher" + ++recursiveIndex; + return getUniqueClassName(className, recursiveIndex); + } + return className; } private Writer createFileWriter() throws IOException { @@ -48,7 +99,15 @@ private Writer createFileWriter() throws IOException { void write() { try { var writer = new Append(createFileWriter()); - writer.append(MessageFormat.format(TEMPLATE, packageName, imports(), utype.shortType())); + + final var shortType = utype.shortWithoutAnnotations(); + var typeString = utype.isGeneric() ? getGenericType() : shortType + ".class"; + + var name = qualifier.isBlank() ? "" : "@Named(\"" + qualifier + "\")\n"; + var className = originName.replace(packageName + ".", ""); + writer.append( + MessageFormat.format( + TEMPLATE, packageName, imports(), name, className, shortType, typeString, qualifier)); writer.close(); } catch (Exception e) { e.printStackTrace(); @@ -61,6 +120,15 @@ String imports() { importTypes.add("io.avaje.inject.event.Event"); importTypes.add("io.avaje.inject.event.ObserverManager"); importTypes.add("io.avaje.inject.spi.Generated"); + importTypes.add(Type.class.getCanonicalName()); + if (!qualifier.isBlank()) { + importTypes.add(NamedPrism.PRISM_TYPE); + } + if (utype.isGeneric()) { + + importTypes.add("io.avaje.inject.spi.GenericType"); + } + StringBuilder writer = new StringBuilder(); for (String importType : importTypes.forImport()) { if (Util.validImportType(importType)) { @@ -69,4 +137,39 @@ String imports() { } return writer.append("\n").toString(); } + + String getGenericType() { + var sb = new StringBuilder(); + sb.append("\n new GenericType<"); + writeGenericType(utype, new HashMap<>(), sb); + sb.append(">(){}.type();"); + return sb.toString(); + } + + private void writeGenericType( + UType type, Map seenShortNames, StringBuilder writer) { + final var typeShortName = Util.shortName(type.mainType()); + final var mainType = seenShortNames.computeIfAbsent(typeShortName, k -> type.mainType()); + if (type.isGeneric()) { + final var shortName = + Objects.equals(type.mainType(), mainType) ? typeShortName : type.mainType(); + writer.append(shortName); + writer.append("<"); + boolean first = true; + for (final var param : type.componentTypes()) { + if (first) { + first = false; + writeGenericType(param, seenShortNames, writer); + continue; + } + writer.append(", "); + writeGenericType(param, seenShortNames, writer); + } + writer.append(">"); + } else { + final var shortName = + Objects.equals(type.mainType(), mainType) ? typeShortName : type.mainType(); + writer.append(shortName); + } + } } diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java index 8e4f62f06..d9f570379 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/FieldReader.java @@ -29,6 +29,9 @@ final class FieldReader { if (nullable || element.asType().toString().startsWith("java.util.Optional<")) { ProcessingContext.addOptionalType(fieldType); } + if (type.fullWithoutAnnotations().startsWith("io.avaje.inject.event.Event")) { + EventPublisherWriter.write(element); + } } boolean isGenericParam() { diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java b/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java index 744de519e..3b1584d4b 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/InjectProcessor.java @@ -29,7 +29,6 @@ @GenerateAPContext @GenerateModuleInfoReader @SupportedAnnotationTypes({ - ApplicationEventPrism.PRISM_TYPE, AspectImportPrism.PRISM_TYPE, AssistFactoryPrism.PRISM_TYPE, ComponentPrism.PRISM_TYPE, @@ -146,9 +145,6 @@ public boolean process(Set annotations, RoundEnvironment maybeElements(roundEnv, Constants.CONTROLLER).ifPresent(this::readBeans); maybeElements(roundEnv, ProxyPrism.PRISM_TYPE).ifPresent(this::readBeans); maybeElements(roundEnv, AssistFactoryPrism.PRISM_TYPE).ifPresent(this::readAssisted); - maybeElements(roundEnv, ApplicationEventPrism.PRISM_TYPE).stream() - .flatMap(Set::stream) - .forEach(EventPublisherWriter::new); maybeElements(roundEnv, ExternalPrism.PRISM_TYPE).stream() .flatMap(Set::stream) @@ -157,11 +153,12 @@ public boolean process(Set annotations, RoundEnvironment .map(u -> "java.util.List".equals(u.mainType()) ? u.param0() : u) .map(UType::fullWithoutAnnotations) .forEach(ProcessingContext::addOptionalType); + allScopes.readBeans(roundEnv); defaultScope.write(processingOver); allScopes.write(processingOver); - if (roundEnv.processingOver()) { + if (processingOver) { var order = new FactoryOrder(ProcessingContext.avajeModules(), defaultScope.pluginProvided()) .orderModules(); diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java index 232bb3277..36a2c7b20 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/MethodReader.java @@ -445,6 +445,10 @@ static class MethodParam { if (nullable || param.asType().toString().startsWith("java.util.Optional<")) { ProcessingContext.addOptionalType(paramType); } + + if (fullUType.fullWithoutAnnotations().startsWith("io.avaje.inject.event.Event")) { + EventPublisherWriter.write(param); + } } @Override diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java index 711fab997..2feaac6e7 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/TypeExtendsReader.java @@ -119,15 +119,16 @@ String providesAspect() { } List autoProvides() { - if (!autoProvide || !providesAspect.isEmpty()) { - return List.of(); - } if (baseTypeIsInterface) { return List.of(Util.unwrapProvider(baseUType)); } var autoProvides = new ArrayList<>(interfaceTypes); autoProvides.addAll(extendsTypes); - autoProvides.add(Util.unwrapProvider(baseUType)); + if (!autoProvide || !providesAspect.isEmpty()) { + autoProvides.remove(baseUType); + } else { + autoProvides.add(Util.unwrapProvider(baseUType)); + } return autoProvides; } diff --git a/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java b/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java index 769473009..c9eac994c 100644 --- a/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java +++ b/inject-generator/src/main/java/io/avaje/inject/generator/package-info.java @@ -1,5 +1,4 @@ @GeneratePrism(AOPFallback.class) -@GeneratePrism(ApplicationEvent.class) @GeneratePrism(Aspect.class) @GeneratePrism(value = Aspect.Import.class, name = "AspectImportPrism") @GeneratePrism(Assisted.class) diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java b/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java index 7602e6ebf..5d62511bf 100644 --- a/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java +++ b/inject-generator/src/test/java/io/avaje/inject/generator/InjectProcessorTest.java @@ -13,6 +13,7 @@ import java.util.Comparator; import java.util.List; import java.util.Set; +import java.util.stream.Stream; import javax.tools.JavaCompiler; import javax.tools.JavaCompiler.CompilationTask; @@ -30,7 +31,9 @@ class InjectProcessorTest { @AfterEach void deleteGeneratedFiles() throws IOException { try { - Files.walk(Paths.get("io").toAbsolutePath()) + Stream.concat( + Files.walk(Paths.get("io").toAbsolutePath()), + Files.walk(Paths.get("lang").toAbsolutePath())) .sorted(Comparator.reverseOrder()) .map(Path::toFile) .forEach(File::delete); diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/CustomEvent.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/CustomEvent.java index e6a73a5b8..58cdce89b 100644 --- a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/CustomEvent.java +++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/CustomEvent.java @@ -1,8 +1,5 @@ package io.avaje.inject.generator.models.valid.observes; -import io.avaje.inject.event.ApplicationEvent; - -@ApplicationEvent public class CustomEvent { } diff --git a/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/EventSender.java b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/EventSender.java new file mode 100644 index 000000000..d22c8299a --- /dev/null +++ b/inject-generator/src/test/java/io/avaje/inject/generator/models/valid/observes/EventSender.java @@ -0,0 +1,26 @@ +package io.avaje.inject.generator.models.valid.observes; + +import java.util.List; + +import io.avaje.inject.event.Event; +import io.avaje.inject.spi.GenericType; +import jakarta.inject.Inject; +import jakarta.inject.Named; +import jakarta.inject.Singleton; + +@Singleton +public class EventSender { + private final Event event; + + @Inject Event> fieldInject; + + public EventSender( + @Named("same") Event event, + @Named("same") Event dup, + @Named("different") Event celsius) { + this.event = event; + } + + @Inject + public void methodInject(@Named("method") Event celsius) {} +} diff --git a/inject-test/src/test/java/org/example/observes/CustomEvent.java b/inject-test/src/test/java/org/example/observes/CustomEvent.java index b7ca2519b..ca9942a01 100644 --- a/inject-test/src/test/java/org/example/observes/CustomEvent.java +++ b/inject-test/src/test/java/org/example/observes/CustomEvent.java @@ -1,8 +1,5 @@ package org.example.observes; -import io.avaje.inject.event.ApplicationEvent; - -@ApplicationEvent public class CustomEvent { private final String string; diff --git a/inject-test/src/test/java/org/example/observes/EventSender.java b/inject-test/src/test/java/org/example/observes/EventSender.java new file mode 100644 index 000000000..18b9be0a4 --- /dev/null +++ b/inject-test/src/test/java/org/example/observes/EventSender.java @@ -0,0 +1,11 @@ +package org.example.observes; + +import io.avaje.inject.Component; +import io.avaje.inject.event.Event; +import jakarta.inject.Inject; + +@Component +public class EventSender { + + @Inject Event event; +} diff --git a/inject/src/main/java/io/avaje/inject/event/ApplicationEvent.java b/inject/src/main/java/io/avaje/inject/event/ApplicationEvent.java deleted file mode 100644 index e5d463d6e..000000000 --- a/inject/src/main/java/io/avaje/inject/event/ApplicationEvent.java +++ /dev/null @@ -1,15 +0,0 @@ -package io.avaje.inject.event; - -import static java.lang.annotation.ElementType.TYPE; -import static java.lang.annotation.RetentionPolicy.SOURCE; - -import java.lang.annotation.Retention; -import java.lang.annotation.Target; - -/** - * Marks a class/record as an event class, signaling the annotation processor to generate a - * publisher class for the annotated type. - */ -@Target(TYPE) -@Retention(SOURCE) -public @interface ApplicationEvent {} diff --git a/inject/src/main/java/io/avaje/inject/event/Event.java b/inject/src/main/java/io/avaje/inject/event/Event.java index 7e2c0b0e3..981208250 100644 --- a/inject/src/main/java/io/avaje/inject/event/Event.java +++ b/inject/src/main/java/io/avaje/inject/event/Event.java @@ -37,9 +37,16 @@ public abstract class Event { protected final List> observers; + protected final String defaultQualifier; protected Event(ObserverManager manager, Type type) { this.observers = manager.observersByType(type); + this.defaultQualifier = ""; + } + + protected Event(ObserverManager manager, Type type, String qualifier) { + this.observers = manager.observersByType(type); + this.defaultQualifier = qualifier; } /** @@ -83,26 +90,26 @@ public CompletionStage fireAsync(T event, String qualifier) { } /** - * Fires an event and notifies observers with no qualifier. + * Fires an event and notifies observers with the qualifier set for this instance. * * @param event the event object */ public void fire(T event) { - fire(event, ""); + fire(event, defaultQualifier); } /** - * Fires an event to asynchronous observers without qualifiers + * Fires an event to asynchronous observers with the qualifier set for this instance. * * @param event the event object * @return a {@link CompletionStage} allowing further pipeline composition on the asynchronous * operation. */ public CompletionStage fireAsync(T event) { - return fireAsync(event, ""); + return fireAsync(event, defaultQualifier); } - private static class CollectingExceptionHandler { + private static final class CollectingExceptionHandler { private final List throwables; @@ -114,17 +121,17 @@ private static class CollectingExceptionHandler { this.throwables = throwables; } - public void handle(Exception throwable) { + void handle(Exception throwable) { throwables.add(throwable); } - public List getHandledExceptions() { + List handledExceptions() { return throwables; } } private void handleExceptions(CollectingExceptionHandler handler) { - var handledExceptions = handler.getHandledExceptions(); + var handledExceptions = handler.handledExceptions(); if (!handledExceptions.isEmpty()) { var exception = handledExceptions.size() == 1 diff --git a/inject/src/main/java/io/avaje/inject/event/ObserverManager.java b/inject/src/main/java/io/avaje/inject/event/ObserverManager.java index db1a6657c..f7c59b665 100644 --- a/inject/src/main/java/io/avaje/inject/event/ObserverManager.java +++ b/inject/src/main/java/io/avaje/inject/event/ObserverManager.java @@ -5,8 +5,8 @@ /** * Manages all {@link Observer} instances in the BeanScope. - * - *

A default implementation is provided by avaje-inject. + *

+ * A default implementation is provided by avaje-inject. */ public interface ObserverManager { diff --git a/inject/src/main/java/io/avaje/inject/spi/Builder.java b/inject/src/main/java/io/avaje/inject/spi/Builder.java index 6b4d9ed71..a887ac5fa 100644 --- a/inject/src/main/java/io/avaje/inject/spi/Builder.java +++ b/inject/src/main/java/io/avaje/inject/spi/Builder.java @@ -76,8 +76,10 @@ static Builder newBuilder(Set profiles, PropertyRequiresPlugin plugin, L */ void registerProvider(Provider provider); - /** Register the observer into the context. */ - void registerObserver(Type type, int priority, boolean sync, Consumer observer, String qualifier); + /** + * Register the observer into the context. + */ + void registerObserver(Type type, int priority, boolean async, Consumer observer, String qualifier); /** * Register the bean instance into the context. diff --git a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java index 372d21f80..97c59ba7d 100644 --- a/inject/src/main/java/io/avaje/inject/spi/DBuilder.java +++ b/inject/src/main/java/io/avaje/inject/spi/DBuilder.java @@ -190,9 +190,9 @@ public final void registerProvider(Provider provider) { } @Override - public void registerObserver(Type type, int priority, boolean sync, Consumer observer, String qualifier) { + public void registerObserver(Type type, int priority, boolean async, Consumer observer, String qualifier) { get(ObserverManager.class) - .registerObserver(type, new Observer(priority, sync, observer, qualifier)); + .registerObserver(type, new Observer<>(priority, async, observer, qualifier)); } @Override