From 05274e0e3916418d420b93d4aa57d958528195e9 Mon Sep 17 00:00:00 2001 From: Matej Novotny Date: Tue, 29 Sep 2026 17:44:00 +0200 Subject: [PATCH] Fix synthetic bean priority collisions (#3541) --- .../SyntheticBeanPriorityIsolationTest.java | 393 ++++++++++++++++++ .../enablement/GlobalEnablementBuilder.java | 48 ++- .../jboss/weld/bootstrap/enablement/Item.java | 26 +- .../weld/bootstrap/enablement/ItemKey.java | 60 +++ .../enablement/ModuleEnablement.java | 40 +- .../events/AfterBeanDiscoveryImpl.java | 6 +- .../AbstractTypeSafeBeanResolver.java | 4 +- .../enablement/EnablementListViewTest.java | 32 ++ .../SyntheticPriorityAcrossArchivesTest.java | 302 ++++++++++++++ 9 files changed, 867 insertions(+), 44 deletions(-) create mode 100644 environments/se/core/src/test/java/org/jboss/weld/environment/se/test/synthetic/priority/SyntheticBeanPriorityIsolationTest.java create mode 100644 impl/src/main/java/org/jboss/weld/bootstrap/enablement/ItemKey.java create mode 100644 tests/src/test/java/org/jboss/weld/tests/unit/deployment/structure/synthetic/SyntheticPriorityAcrossArchivesTest.java diff --git a/environments/se/core/src/test/java/org/jboss/weld/environment/se/test/synthetic/priority/SyntheticBeanPriorityIsolationTest.java b/environments/se/core/src/test/java/org/jboss/weld/environment/se/test/synthetic/priority/SyntheticBeanPriorityIsolationTest.java new file mode 100644 index 00000000000..0081c273aef --- /dev/null +++ b/environments/se/core/src/test/java/org/jboss/weld/environment/se/test/synthetic/priority/SyntheticBeanPriorityIsolationTest.java @@ -0,0 +1,393 @@ +/* + * Copyright The Weld Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.jboss.weld.environment.se.test.synthetic.priority; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assume.assumeTrue; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; +import java.util.function.Supplier; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Alternative; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Instance; +import jakarta.enterprise.inject.Produces; +import jakarta.enterprise.inject.Reserve; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.AfterTypeDiscovery; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.inject.spi.Prioritized; +import jakarta.inject.Inject; + +import org.jboss.weld.environment.se.Weld; +import org.jboss.weld.environment.se.WeldContainer; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +@RunWith(Parameterized.class) +public class SyntheticBeanPriorityIsolationTest { + @Parameters(name = "reserve={0}, configurator={1}, reverse={2}") + public static Object[][] parameters() { + return new Object[][] { + { false, false, false }, { false, false, true }, + { true, false, false }, { true, false, true }, + { false, true, false }, { false, true, true }, + { true, true, false }, { true, true, true } + }; + } + + private final boolean reserve; + private final boolean configurator; + private final boolean reverse; + + public SyntheticBeanPriorityIsolationTest(boolean reserve, boolean configurator, boolean reverse) { + this.reserve = reserve; + this.configurator = configurator; + this.reverse = reverse; + } + + @Test + public void distinctClassesSelectHigherPriority() { + check(false, false, false); + } + + @Test + public void unrelatedBeanDoesNotChangeWinner() { + check(false, true, false); + } + + @Test + public void sharedClassSelectsHigherPriority() { + check(true, false, false); + } + + @Test + public void equalPrioritiesRemainAmbiguous() { + check(true, false, true); + } + + @Test + public void priorityDoesNotEnableAnotherSyntheticBeanWithSharedClass() { + Extension extension = new Extension() { + void register(@Observes AfterBeanDiscovery event) { + TestBean prioritized = new TestBean<>(SharedMarker.class, OtherService.class, + () -> () -> "C", 20, reserve); + registerBean(event, prioritized); + event.addBean().beanClass(SharedMarker.class).types(Service.class) + .alternative(!reserve).reserve(reserve).createWith(ctx -> (Service) () -> "disabled"); + } + }; + try (WeldContainer container = new Weld().disableDiscovery().addBeanClass(Consumer.class) + .addExtension(extension).initialize()) { + assertTrue(container.select(Consumer.class).get().services.isUnsatisfied()); + } + } + + @Test + public void ordinaryBeanSuppressesReserves() { + assumeTrue(reserve); + Extension extension = new Extension() { + void register(@Observes AfterBeanDiscovery event) { + registerBean(event, new TestBean(SharedMarker.class, Service.class, () -> () -> "reserve", + 20, true)); + event.addBean().beanClass(SharedMarker.class).types(Service.class) + .createWith(ctx -> (Service) () -> "ordinary"); + } + }; + try (WeldContainer container = new Weld().disableDiscovery().addBeanClass(Consumer.class) + .addExtension(extension).initialize()) { + assertEquals("ordinary", container.select(Consumer.class).get().services.get().value()); + } + } + + @Test + public void syntheticPriorityDoesNotOverrideManagedBeanPriority() { + Class managedClass = reserve ? ManagedReserve.class : ManagedAlternative.class; + Extension extension = new Extension() { + void register(@Observes AfterBeanDiscovery event) { + List> beans = new ArrayList<>(); + beans.add(new TestBean(DistinctMarker.class, Service.class, () -> () -> "B", 18, reserve)); + beans.add(new TestBean(managedClass, OtherService.class, () -> () -> "C", 20, reserve)); + if (reverse) { + Collections.reverse(beans); + } + beans.forEach(bean -> registerBean(event, bean)); + } + }; + try (WeldContainer container = new Weld().disableDiscovery().addBeanClasses(Consumer.class, managedClass) + .addExtension(extension).initialize()) { + assertEquals("B", container.select(Consumer.class).get().services.get().value()); + } + } + + @Test + public void reserveProducerPrioritiesArePreserved() { + assumeTrue(reserve); + Extension extension = new Extension() { + void register(@Observes AfterBeanDiscovery event) { + registerBean(event, new TestBean(DistinctMarker.class, Service.class, () -> () -> "synthetic", + 15, true)); + registerBean(event, new TestBean(ReserveProducers.class, OtherService.class, () -> () -> "C", + 30, true)); + } + }; + try (WeldContainer container = new Weld().disableDiscovery().addBeanClasses(Consumer.class, ReserveProducers.class) + .addExtension(extension).initialize()) { + assertEquals("field", container.select(Consumer.class).get().services.get().value()); + } + } + + @Test + public void syntheticPriorityUsesSameScaleAsAfterTypeDiscoveryList() { + Class low = reserve ? LowReserve.class : LowAlternative.class; + Class high = reserve ? HighReserve.class : HighAlternative.class; + Class inserted = reserve ? InsertedReserve.class : InsertedAlternative.class; + Extension extension = new Extension() { + void reorder(@Observes AfterTypeDiscovery event) { + // Inserting between adjacent priorities forces Weld to rescale the list. + List> enabled = reserve ? event.getReserves() : event.getAlternatives(); + enabled.add(enabled.indexOf(high), inserted); + } + + void register(@Observes AfterBeanDiscovery event) { + registerBean(event, new TestBean(SharedMarker.class, Service.class, + () -> () -> "synthetic", 15, reserve)); + } + }; + try (WeldContainer container = new Weld().disableDiscovery().addBeanClasses(Consumer.class, low, high, inserted) + .addExtension(extension).initialize()) { + assertEquals("synthetic", container.select(Consumer.class).get().services.get().value()); + } + } + + @Dependent + @Alternative + @Priority(10) + public static class LowAlternative implements Service { + public String value() { + return "managed"; + } + } + + @Dependent + @Alternative + @Priority(11) + public static class HighAlternative extends LowAlternative { + } + + @Dependent + @Alternative + public static class InsertedAlternative implements Service { + public String value() { + return "inserted"; + } + } + + @Dependent + @Reserve + @Priority(10) + public static class LowReserve implements Service { + public String value() { + return "managed"; + } + } + + @Dependent + @Reserve + @Priority(11) + public static class HighReserve extends LowReserve { + } + + @Dependent + @Reserve + public static class InsertedReserve implements Service { + public String value() { + return "inserted"; + } + } + + @Dependent + public static class ReserveProducers { + @Produces + @Reserve + @Priority(20) + Service field = () -> "field"; + + @Produces + @Reserve + @Priority(10) + Service method() { + return () -> "method"; + } + } + + @Dependent + @Alternative + @Priority(15) + public static class ManagedAlternative implements Service { + public String value() { + return "managed"; + } + } + + @Dependent + @Reserve + @Priority(15) + public static class ManagedReserve implements Service { + public String value() { + return "managed"; + } + } + + private void registerBean(AfterBeanDiscovery event, TestBean bean) { + if (configurator) { + event.addBean().beanClass(bean.getBeanClass()).types(bean.getTypes()) + .qualifiers(bean.getQualifiers()).scope(Dependent.class) + .alternative(bean.isAlternative()).reserve(bean.isReserve()).priority(bean.getPriority()) + .createWith(ctx -> bean.supplier.get()); + } else { + event.addBean(bean); + } + } + + private void check(boolean shared, boolean unrelated, boolean equal) { + List> beans = new ArrayList<>(); + beans.add(new TestBean(SharedMarker.class, Service.class, () -> () -> "A", 10, reserve)); + beans.add(new TestBean(shared ? SharedMarker.class : DistinctMarker.class, + Service.class, () -> () -> "B", equal ? 10 : 15, reserve)); + if (unrelated) { + beans.add(new TestBean(SharedMarker.class, OtherService.class, () -> () -> "C", 20, reserve)); + } + if (reverse) { + Collections.reverse(beans); + } + Extension extension = new Extension() { + void register(@Observes AfterBeanDiscovery event) { + for (TestBean bean : beans) { + registerBean(event, bean); + } + } + }; + try (WeldContainer container = new Weld().disableDiscovery().addBeanClass(Consumer.class) + .addExtension(extension).initialize()) { + Instance services = container.select(Consumer.class).get().services; + if (equal) { + assertTrue(services.isAmbiguous()); + } else { + assertEquals("B", services.get().value()); + } + } + } + + public static class Consumer { + @Inject + Instance services; + } + + interface Service { + String value(); + } + + interface OtherService { + String value(); + } + + static class SharedMarker { + } + + static class DistinctMarker { + } + + static class TestBean implements Bean, Prioritized { + private final Class beanClass; + private final Class type; + private final Supplier supplier; + private final int priority; + private final boolean reserve; + + TestBean(Class beanClass, Class type, Supplier supplier, int priority, boolean reserve) { + this.beanClass = beanClass; + this.type = type; + this.supplier = supplier; + this.priority = priority; + this.reserve = reserve; + } + + @Override + public Class getBeanClass() { + return beanClass; + } + + @Override + public Set getTypes() { + return Set.of(type, Object.class); + } + + @Override + public Set getQualifiers() { + return Set.of(Default.Literal.INSTANCE, Any.Literal.INSTANCE); + } + + @Override + public Class getScope() { + return Dependent.class; + } + + @Override + public String getName() { + return null; + } + + @Override + public Set> getStereotypes() { + return Set.of(); + } + + @Override + public Set getInjectionPoints() { + return Set.of(); + } + + @Override + public boolean isAlternative() { + return !reserve; + } + + @Override + public boolean isReserve() { + return reserve; + } + + @Override + public int getPriority() { + return priority; + } + + @Override + public T create(CreationalContext ctx) { + return supplier.get(); + } + + @Override + public void destroy(T instance, CreationalContext ctx) { + ctx.release(); + } + } +} diff --git a/impl/src/main/java/org/jboss/weld/bootstrap/enablement/GlobalEnablementBuilder.java b/impl/src/main/java/org/jboss/weld/bootstrap/enablement/GlobalEnablementBuilder.java index fb9712abf52..3750dca745b 100644 --- a/impl/src/main/java/org/jboss/weld/bootstrap/enablement/GlobalEnablementBuilder.java +++ b/impl/src/main/java/org/jboss/weld/bootstrap/enablement/GlobalEnablementBuilder.java @@ -19,7 +19,6 @@ import static org.jboss.weld.util.reflection.Reflections.cast; import java.lang.annotation.Annotation; -import java.math.BigInteger; import java.util.ArrayList; import java.util.Collections; import java.util.HashMap; @@ -30,6 +29,7 @@ import java.util.function.Function; import java.util.stream.Collectors; +import jakarta.enterprise.inject.spi.Bean; import jakarta.enterprise.inject.spi.Extension; import org.jboss.weld.bootstrap.BeanDeployment; @@ -60,41 +60,47 @@ public class GlobalEnablementBuilder extends AbstractBootstrapService { private final List interceptors = Collections.synchronizedList(new ArrayList()); private final List decorators = Collections.synchronizedList(new ArrayList()); - private volatile Map, Integer> cachedAlternativeMap; - private volatile Map, Integer> cachedReserveMap; + private volatile Map cachedAlternativeMap; + private volatile Map cachedReserveMap; private volatile boolean sorted; private volatile boolean dirty; - private void addItem(List list, Class javaClass, int priority) { + private void addItem(List list, Item item) { sorted = false; dirty = true; synchronized (list) { - int originalPriority = priority; if (!list.isEmpty()) { int scaling = list.get(0).getNumberOfScaling(); - if (scaling > 0) { - // We have to scale the priority if necessary - priority *= new BigInteger("" + Item.ITEM_PRIORITY_SCALE_POWER).pow(scaling).intValue(); + for (int i = 0; i < scaling; i++) { + item.scalePriority(); } } - list.add(new Item(javaClass, originalPriority, priority)); + list.add(item); } } + public void addSyntheticAlternative(Bean bean, int priority) { + addItem(alternatives, new Item(bean, priority)); + } + + public void addSyntheticReserve(Bean bean, int priority) { + addItem(reserves, new Item(bean, priority)); + } + public void addAlternative(Class javaClass, int priority) { - addItem(alternatives, javaClass, priority); + addItem(alternatives, new Item(javaClass, priority)); } public void addReserve(Class javaClass, int priority) { - addItem(reserves, javaClass, priority); + addItem(reserves, new Item(javaClass, priority)); } public void addInterceptor(Class javaClass, int priority) { - addItem(interceptors, javaClass, priority); + addItem(interceptors, new Item(javaClass, priority)); } public void addDecorator(Class javaClass, int priority) { - addItem(decorators, javaClass, priority); + addItem(decorators, new Item(javaClass, priority)); } public List> getAlternativeList(final Extension extension) { @@ -196,22 +202,22 @@ public boolean isDirty() { * need to synchronize access to * cachedAlternativeMap. */ - private Map, Integer> getGlobalAlternativeMap() { + private Map getGlobalAlternativeMap() { if (cachedAlternativeMap == null || dirty) { - Map, Integer> map = new HashMap, Integer>(); + Map map = new HashMap(); for (Item item : alternatives) { - map.put(item.getJavaClass(), item.getPriority()); + map.put(item.getKey(), item.getPriority()); } cachedAlternativeMap = ImmutableMap.copyOf(map); } return cachedAlternativeMap; } - private Map, Integer> getGlobalReserveMap() { + private Map getGlobalReserveMap() { if (cachedReserveMap == null || dirty) { - Map, Integer> map = new HashMap, Integer>(); + Map map = new HashMap(); for (Item item : reserves) { - map.put(item.getJavaClass(), item.getPriority()); + map.put(item.getKey(), item.getPriority()); } cachedReserveMap = ImmutableMap.copyOf(map); } @@ -274,8 +280,8 @@ public ModuleEnablement createModuleEnablement(BeanDeployment deployment) { alternativeStereotypes = Collections.emptySet(); } - Map, Integer> globalAlternatives = getGlobalAlternativeMap(); - Map, Integer> globalReserves = getGlobalReserveMap(); + Map globalAlternatives = getGlobalAlternativeMap(); + Map globalReserves = getGlobalReserveMap(); // We suppose that enablements are always created all at once dirty = false; diff --git a/impl/src/main/java/org/jboss/weld/bootstrap/enablement/Item.java b/impl/src/main/java/org/jboss/weld/bootstrap/enablement/Item.java index 23f8cfb827c..0a7b41e454d 100644 --- a/impl/src/main/java/org/jboss/weld/bootstrap/enablement/Item.java +++ b/impl/src/main/java/org/jboss/weld/bootstrap/enablement/Item.java @@ -19,6 +19,8 @@ import java.util.Objects; import java.util.concurrent.atomic.AtomicInteger; +import jakarta.enterprise.inject.spi.Bean; + import org.jboss.weld.util.Preconditions; /** @@ -32,21 +34,27 @@ class Item implements Comparable { private final Class javaClass; + private final ItemKey key; + private final int originalPriority; private final AtomicInteger priority; Item(Class javaClass, int priority) { - this(javaClass, priority, priority); + this(new ItemKey(javaClass), javaClass, priority); + } + + Item(Bean bean, int priority) { + this(new ItemKey(bean), bean.getBeanClass(), priority); } - Item(Class javaClass, int originalPriority, int priority) { + private Item(ItemKey key, Class javaClass, int priority) { Preconditions.checkArgumentNotNull(javaClass, "javaClass"); Preconditions.checkArgumentNotNull(priority, "priority"); - Preconditions.checkArgumentNotNull(originalPriority, "originalPriority"); this.javaClass = javaClass; + this.key = key; this.priority = new AtomicInteger(priority); - this.originalPriority = originalPriority; + this.originalPriority = priority; } void scalePriority() { @@ -57,6 +65,10 @@ Class getJavaClass() { return javaClass; } + ItemKey getKey() { + return key; + } + int getPriority() { return priority.get(); } @@ -101,6 +113,8 @@ public int hashCode() { return javaClass.hashCode(); } + // EnablementListView exposes classes and relies on class-based equality. + // Priority maps use getKey() to distinguish individual synthetic beans. @Override public boolean equals(Object obj) { if (this == obj) { @@ -115,6 +129,6 @@ public boolean equals(Object obj) { @Override public String toString() { - return "[Class=" + javaClass + ", priority=" + priority + "]"; + return "[Key=" + key + ", priority=" + priority + "]"; } -} \ No newline at end of file +} diff --git a/impl/src/main/java/org/jboss/weld/bootstrap/enablement/ItemKey.java b/impl/src/main/java/org/jboss/weld/bootstrap/enablement/ItemKey.java new file mode 100644 index 00000000000..4cba17dd1e4 --- /dev/null +++ b/impl/src/main/java/org/jboss/weld/bootstrap/enablement/ItemKey.java @@ -0,0 +1,60 @@ +/* + * Copyright The Weld Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.jboss.weld.bootstrap.enablement; + +import java.util.Objects; + +import jakarta.enterprise.inject.spi.Bean; + +import org.jboss.weld.util.Preconditions; + +/** + * Identifies an enablement entry by the bean class for a discovered bean or the {@link Bean} metadata object for a synthetic + * bean. All beans have {@code Bean} metadata, but synthetic beans may share a bean class, so their metadata objects are used + * to distinguish individual entries. + */ +public final class ItemKey { + + private final Class javaClass; + private final Bean bean; + + ItemKey(Class javaClass) { + Preconditions.checkArgumentNotNull(javaClass, "javaClass"); + this.javaClass = javaClass; + this.bean = null; + } + + ItemKey(Bean bean) { + Preconditions.checkArgumentNotNull(bean, "bean"); + this.javaClass = null; + this.bean = bean; + } + + Class getJavaClass() { + return javaClass; + } + + @Override + public int hashCode() { + return Objects.hash(javaClass, bean); + } + + @Override + public boolean equals(Object obj) { + if (this == obj) { + return true; + } + if (obj instanceof ItemKey) { + ItemKey that = (ItemKey) obj; + return Objects.equals(javaClass, that.javaClass) && Objects.equals(bean, that.bean); + } + return false; + } + + @Override + public String toString() { + return String.valueOf(bean != null ? bean : javaClass); + } +} diff --git a/impl/src/main/java/org/jboss/weld/bootstrap/enablement/ModuleEnablement.java b/impl/src/main/java/org/jboss/weld/bootstrap/enablement/ModuleEnablement.java index e5cc18272e2..5213f2d0819 100644 --- a/impl/src/main/java/org/jboss/weld/bootstrap/enablement/ModuleEnablement.java +++ b/impl/src/main/java/org/jboss/weld/bootstrap/enablement/ModuleEnablement.java @@ -32,6 +32,7 @@ import jakarta.enterprise.inject.spi.Interceptor; import org.jboss.weld.util.collections.ImmutableMap; +import org.jboss.weld.util.collections.ImmutableSet; /** * Holds information about interceptors, decorators and alternatives that are enabled in this module. @@ -42,8 +43,8 @@ public class ModuleEnablement { public static final ModuleEnablement EMPTY_ENABLEMENT = new ModuleEnablement(Collections.> emptyList(), - Collections.> emptyList(), Collections., Integer> emptyMap(), - Collections., Integer> emptyMap(), Collections.> emptySet(), + Collections.> emptyList(), Collections. emptyMap(), + Collections. emptyMap(), Collections.> emptySet(), Collections.> emptySet()); private final List> interceptors; @@ -51,8 +52,8 @@ Collections., Integer> emptyMap(), Collections.> emptySet(), private final Map, Integer> interceptorMap; private final Map, Integer> decoratorMap; - private final Map, Integer> globalAlternatives; - private final Map, Integer> globalReserves; + private final Map globalAlternatives; + private final Map globalReserves; private final Set> localAlternativeClasses; private final Set> localAlternativeStereotypes; @@ -60,8 +61,8 @@ Collections., Integer> emptyMap(), Collections.> emptySet(), private final Comparator> decoratorComparator; private final Comparator> interceptorComparator; - public ModuleEnablement(List> interceptors, List> decorators, Map, Integer> globalAlternatives, - Map, Integer> globalReserves, Set> localAlternativeClasses, + public ModuleEnablement(List> interceptors, List> decorators, Map globalAlternatives, + Map globalReserves, Set> localAlternativeClasses, Set> localAlternativeStereotypes) { this.interceptors = interceptors; this.decorators = decorators; @@ -115,23 +116,35 @@ public Comparator> getInterceptorComparator() { } public Integer getAlternativePriority(Class javaClass) { - return globalAlternatives.get(javaClass); + return globalAlternatives.get(new ItemKey(javaClass)); } public Integer getReservePriority(Class javaClass) { - return globalReserves.get(javaClass); + return globalReserves.get(new ItemKey(javaClass)); + } + + public Integer getAlternativePriority(Bean bean) { + Integer priority = globalAlternatives.get(new ItemKey(bean)); + return priority != null ? priority : getAlternativePriority(bean.getBeanClass()); + } + + public Integer getReservePriority(Bean bean) { + Integer priority = globalReserves.get(new ItemKey(bean)); + return priority != null ? priority : getReservePriority(bean.getBeanClass()); } public boolean isEnabledAlternativeClass(Class alternativeClass) { - return globalAlternatives.containsKey(alternativeClass) || localAlternativeClasses.contains(alternativeClass); + return globalAlternatives.containsKey(new ItemKey(alternativeClass)) + || localAlternativeClasses.contains(alternativeClass); } public boolean isEnabledReserveClass(Class reserveClass) { - return globalReserves.containsKey(reserveClass); + return globalReserves.containsKey(new ItemKey(reserveClass)); } public boolean isEnabledAlternativeStereotype(Class alternativeClass) { - return globalAlternatives.containsKey(alternativeClass) || localAlternativeStereotypes.contains(alternativeClass); + return globalAlternatives.containsKey(new ItemKey(alternativeClass)) + || localAlternativeStereotypes.contains(alternativeClass); } public Set> getAlternativeClasses() { @@ -143,7 +156,10 @@ public Set> getAlternativeStereotypes() { } public Set> getGlobalAlternatives() { - return globalAlternatives.keySet(); + return globalAlternatives.keySet().stream() + .map(ItemKey::getJavaClass) + .filter(javaClass -> javaClass != null) + .collect(ImmutableSet.collector()); } public Set> getAllAlternatives() { diff --git a/impl/src/main/java/org/jboss/weld/bootstrap/events/AfterBeanDiscoveryImpl.java b/impl/src/main/java/org/jboss/weld/bootstrap/events/AfterBeanDiscoveryImpl.java index 71747c62bea..d9c8c382953 100644 --- a/impl/src/main/java/org/jboss/weld/bootstrap/events/AfterBeanDiscoveryImpl.java +++ b/impl/src/main/java/org/jboss/weld/bootstrap/events/AfterBeanDiscoveryImpl.java @@ -233,12 +233,12 @@ private void processBeanRegistration(BeanRegistration registration, GlobalEn } else { beanManager.addBean(bean); if (priority != null) { - // validation of bean being both, alternative and reserve happens during validation + // Synthetic priorities belong to individual beans; bean classes need not be unique. if (bean.isAlternative()) { - globalEnablementBuilder.addAlternative(bean.getBeanClass(), priority); + globalEnablementBuilder.addSyntheticAlternative(bean, priority); } if (bean.isReserve()) { - globalEnablementBuilder.addReserve(bean.getBeanClass(), priority); + globalEnablementBuilder.addSyntheticReserve(bean, priority); } } } diff --git a/impl/src/main/java/org/jboss/weld/resolution/AbstractTypeSafeBeanResolver.java b/impl/src/main/java/org/jboss/weld/resolution/AbstractTypeSafeBeanResolver.java index 2dfd8fdc281..7b2aa67f937 100644 --- a/impl/src/main/java/org/jboss/weld/resolution/AbstractTypeSafeBeanResolver.java +++ b/impl/src/main/java/org/jboss/weld/resolution/AbstractTypeSafeBeanResolver.java @@ -147,7 +147,7 @@ public Set> resolveAlternatives(Set> alternatives) { priority = beanManager.getEnabled().getAlternativePriority(bean.getBeanClass()); } } else { - priority = beanManager.getEnabled().getAlternativePriority(bean.getBeanClass()); + priority = beanManager.getEnabled().getAlternativePriority(bean); } if (priority == null) { // not all the beans left are alternatives with a priority - we are not able to resolve @@ -183,7 +183,7 @@ public Set> resolveReserves(Set> reserves) { // first check for explicit priority declaration on producers priority = ((AbstractProducerBean) bean).getPriority(); } else { - priority = beanManager.getEnabled().getReservePriority(bean.getBeanClass()); + priority = beanManager.getEnabled().getReservePriority(bean); } if (priority > highestPriority) { highestPriority = priority; diff --git a/impl/src/test/java/org/jboss/weld/bootstrap/enablement/EnablementListViewTest.java b/impl/src/test/java/org/jboss/weld/bootstrap/enablement/EnablementListViewTest.java index e30a1ce77cf..2230366617a 100644 --- a/impl/src/test/java/org/jboss/weld/bootstrap/enablement/EnablementListViewTest.java +++ b/impl/src/test/java/org/jboss/weld/bootstrap/enablement/EnablementListViewTest.java @@ -28,8 +28,10 @@ import java.util.List; import java.util.ListIterator; +import jakarta.enterprise.inject.spi.Bean; import jakarta.enterprise.inject.spi.Extension; +import org.jboss.weld.bean.ForwardingBean; import org.junit.Test; /** @@ -39,6 +41,36 @@ */ public class EnablementListViewTest { + @Test + public void testClassViewOfSyntheticItems() { + Bean bean = new ForwardingBean() { + @Override + public Bean delegate() { + throw new AssertionError("Only the bean class is needed for this list view"); + } + + @Override + public Class getBeanClass() { + return String.class; + } + }; + GlobalEnablementBuilder builder = new GlobalEnablementBuilder(); + builder.addSyntheticAlternative(bean, 10); + builder.addAlternative(String.class, 20); + List> view = builder.getAlternativeList(null); + + assertEquals(String.class, view.get(0)); + assertTrue(view.contains(String.class)); + // The synthetic bean and the regular class entry are both exposed as String.class in the view. + assertEquals(0, view.indexOf(String.class)); + assertEquals(1, view.lastIndexOf(String.class)); + assertTrue(view.remove(String.class)); + assertEquals(1, view.size()); + assertEquals(String.class, view.get(0)); + assertTrue(view.remove(String.class)); + assertTrue(view.isEmpty()); + } + @Test public void testBasicOperations() { final List list = new ArrayList<>(); diff --git a/tests/src/test/java/org/jboss/weld/tests/unit/deployment/structure/synthetic/SyntheticPriorityAcrossArchivesTest.java b/tests/src/test/java/org/jboss/weld/tests/unit/deployment/structure/synthetic/SyntheticPriorityAcrossArchivesTest.java new file mode 100644 index 00000000000..88aac2e65a6 --- /dev/null +++ b/tests/src/test/java/org/jboss/weld/tests/unit/deployment/structure/synthetic/SyntheticPriorityAcrossArchivesTest.java @@ -0,0 +1,302 @@ +/* + * Copyright The Weld Authors + * SPDX-License-Identifier: Apache-2.0 + */ +package org.jboss.weld.tests.unit.deployment.structure.synthetic; + +import static org.jboss.weld.test.util.Utils.getReference; +import static org.testng.Assert.assertEquals; +import static org.testng.Assert.assertNotSame; +import static org.testng.Assert.assertTrue; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Type; +import java.util.Set; +import java.util.function.Supplier; + +import jakarta.annotation.Priority; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.context.spi.CreationalContext; +import jakarta.enterprise.event.Observes; +import jakarta.enterprise.inject.Alternative; +import jakarta.enterprise.inject.Any; +import jakarta.enterprise.inject.Default; +import jakarta.enterprise.inject.Reserve; +import jakarta.enterprise.inject.Vetoed; +import jakarta.enterprise.inject.spi.AfterBeanDiscovery; +import jakarta.enterprise.inject.spi.Bean; +import jakarta.enterprise.inject.spi.BeanManager; +import jakarta.enterprise.inject.spi.Extension; +import jakarta.enterprise.inject.spi.InjectionPoint; +import jakarta.enterprise.inject.spi.Prioritized; +import jakarta.enterprise.inject.spi.ProcessBean; +import jakarta.enterprise.inject.spi.ProcessSyntheticBean; +import jakarta.inject.Inject; + +import org.jboss.arquillian.container.weld.embedded.mock.BeanDeploymentArchiveImpl; +import org.jboss.arquillian.container.weld.embedded.mock.FlatDeployment; +import org.jboss.arquillian.container.weld.embedded.mock.TestContainer; +import org.jboss.weld.bootstrap.spi.BeanDeploymentArchive; +import org.jboss.weld.bootstrap.spi.Deployment; +import org.testng.annotations.DataProvider; +import org.testng.annotations.Test; + +/** + * Extends the WELD-2000 custom priority and WELD-2658 lifecycle-event coverage to + * multiple bean deployment archives, for both alternatives and reserves. + */ +public class SyntheticPriorityAcrossArchivesTest { + + @DataProvider + public Object[][] priorities() { + return new Object[][] { { false, false }, { false, true }, { true, false }, { true, true } }; + } + + @Test(dataProvider = "priorities") + public void testSyntheticPriorityAcrossArchives(boolean reserve, boolean configurator) { + PriorityExtension extension = new PriorityExtension(reserve, configurator); + BeanDeploymentArchiveImpl owner = new BeanDeploymentArchiveImpl("owner", Marker.class, PriorityExtension.class); + BeanDeploymentArchiveImpl first = new BeanDeploymentArchiveImpl("first-client", FirstConsumer.class); + BeanDeploymentArchiveImpl second = new BeanDeploymentArchiveImpl("second-client", SecondConsumer.class); + BeanDeploymentArchiveImpl isolated = new BeanDeploymentArchiveImpl("isolated", IsolatedMarker.class); + first.getBeanDeploymentArchives().add(owner); + second.getBeanDeploymentArchives().add(owner); + + Deployment deployment = new FlatDeployment(new BeanDeploymentArchive[] { owner, first, second, isolated }, extension) { + @Override + public BeanDeploymentArchive loadBeanDeploymentArchive(Class beanClass) { + if (beanClass == FirstConsumer.class) { + return first; + } + if (beanClass == SecondConsumer.class) { + return second; + } + if (beanClass == IsolatedMarker.class) { + return isolated; + } + return owner; + } + + @Override + public BeanDeploymentArchive getBeanDeploymentArchive(Class beanClass) { + return loadBeanDeploymentArchive(beanClass); + } + }; + + TestContainer container = new TestContainer(deployment); + try { + container.startContainer(); + BeanManager ownerManager = container.getBeanManager(owner); + BeanManager firstManager = container.getBeanManager(first); + BeanManager secondManager = container.getBeanManager(second); + BeanManager isolatedManager = container.getBeanManager(isolated); + assertNotSame(ownerManager, firstManager); + assertNotSame(ownerManager, secondManager); + assertNotSame(firstManager, secondManager); + for (BeanManager manager : new BeanManager[] { ownerManager, firstManager, secondManager }) { + assertEquals(manager.getBeans(Service.class).size(), 2); + assertEquals(getReference(manager, Service.class).value(), "higher"); + } + // Direct injection also checks enablement during deployment validation. + assertEquals(getReference(firstManager, FirstConsumer.class).service.value(), "higher"); + assertEquals(getReference(secondManager, SecondConsumer.class).service.value(), "higher"); + assertTrue(isolatedManager.getBeans(Service.class).isEmpty()); + assertEquals(extension.processBeanEvents, 2); + assertEquals(extension.processSyntheticBeanEvents, 2); + } finally { + container.stopContainer(); + } + } + + @Test + public void testArchiveCreatedDuringAfterBeanDiscovery() { + BeanDeploymentArchiveImpl owner = new BeanDeploymentArchiveImpl("owner", ManagedAlternative.class, + ManagedReserve.class); + BeanDeploymentArchiveImpl late = new BeanDeploymentArchiveImpl("late", Marker.class); + late.getBeanDeploymentArchives().add(owner); + Extension extension = new Extension() { + void register(@Observes AfterBeanDiscovery event) { + event.addBean(new TestBean(Marker.class, Service.class, () -> () -> "synthetic", 20, false)); + } + }; + Deployment deployment = new FlatDeployment(owner, extension) { + @Override + public BeanDeploymentArchive loadBeanDeploymentArchive(Class beanClass) { + if (beanClass == Marker.class) { + owner.getBeanDeploymentArchives().add(late); + return late; + } + return owner; + } + + @Override + public BeanDeploymentArchive getBeanDeploymentArchive(Class beanClass) { + return loadBeanDeploymentArchive(beanClass); + } + }; + TestContainer container = new TestContainer(deployment); + try { + container.startContainer(); + BeanManager ownerManager = container.getBeanManager(owner); + BeanManager lateManager = container.getBeanManager(late); + assertNotSame(ownerManager, lateManager); + assertEquals(getReference(lateManager, Service.class).value(), "synthetic"); + assertEquals(lateManager.getBeans(ManagedAlternative.class).size(), 1); + assertEquals(lateManager.getBeans(ManagedReserve.class).size(), 1); + } finally { + container.stopContainer(); + } + } + + @Dependent + @Alternative + @Priority(10) + public static class ManagedAlternative { + } + + @Dependent + @Reserve + @Priority(10) + public static class ManagedReserve { + } + + public static class PriorityExtension implements Extension { + private final boolean reserve; + private final boolean configurator; + int processBeanEvents; + int processSyntheticBeanEvents; + + PriorityExtension(boolean reserve, boolean configurator) { + this.reserve = reserve; + this.configurator = configurator; + } + + void register(@Observes AfterBeanDiscovery event) { + register(event, 10, "lower"); + register(event, 20, "higher"); + } + + private void register(AfterBeanDiscovery event, int priority, String value) { + if (configurator) { + event.addBean().beanClass(Marker.class).types(Service.class, Object.class) + .qualifiers(Default.Literal.INSTANCE, Any.Literal.INSTANCE).scope(Dependent.class) + .alternative(!reserve).reserve(reserve).priority(priority) + .createWith(ctx -> (Service) () -> value); + } else { + event.addBean(new TestBean(Marker.class, Service.class, () -> () -> value, priority, reserve)); + } + } + + void processBean(@Observes ProcessBean event) { + if (event.getBean().getTypes().contains(Service.class)) { + processBeanEvents++; + } + } + + void processSyntheticBean(@Observes ProcessSyntheticBean event) { + if (event.getBean().getTypes().contains(Service.class)) { + processSyntheticBeanEvents++; + } + } + } + + public interface Service { + String value(); + } + + @Dependent + public static class FirstConsumer { + @Inject + Service service; + } + + @Dependent + public static class SecondConsumer { + @Inject + Service service; + } + + @Vetoed + public static class Marker { + } + + @Vetoed + public static class IsolatedMarker { + } + + static class TestBean implements Bean, Prioritized { + private final Class beanClass; + private final Class type; + private final Supplier supplier; + private final int priority; + private final boolean reserve; + + TestBean(Class beanClass, Class type, Supplier supplier, int priority, boolean reserve) { + this.beanClass = beanClass; + this.type = type; + this.supplier = supplier; + this.priority = priority; + this.reserve = reserve; + } + + @Override + public Class getBeanClass() { + return beanClass; + } + + @Override + public Set getTypes() { + return Set.of(type, Object.class); + } + + @Override + public Set getQualifiers() { + return Set.of(Default.Literal.INSTANCE, Any.Literal.INSTANCE); + } + + @Override + public Class getScope() { + return Dependent.class; + } + + @Override + public String getName() { + return null; + } + + @Override + public Set> getStereotypes() { + return Set.of(); + } + + @Override + public Set getInjectionPoints() { + return Set.of(); + } + + @Override + public boolean isAlternative() { + return !reserve; + } + + @Override + public boolean isReserve() { + return reserve; + } + + @Override + public int getPriority() { + return priority; + } + + @Override + public T create(CreationalContext ctx) { + return supplier.get(); + } + + @Override + public void destroy(T instance, CreationalContext ctx) { + ctx.release(); + } + } +}