diff --git a/bootique-jersey-jakarta/src/main/java/io/bootique/jersey/BaseBqHk2Bridge.java b/bootique-jersey-jakarta/src/main/java/io/bootique/jersey/BaseBqHk2Bridge.java index 88a3a97..4198dd3 100644 --- a/bootique-jersey-jakarta/src/main/java/io/bootique/jersey/BaseBqHk2Bridge.java +++ b/bootique-jersey-jakarta/src/main/java/io/bootique/jersey/BaseBqHk2Bridge.java @@ -4,6 +4,7 @@ import io.bootique.di.Injector; import io.bootique.di.Key; import io.bootique.di.TypeLiteral; +import jakarta.inject.Named; import org.glassfish.hk2.api.Injectee; import javax.inject.Provider; @@ -35,15 +36,22 @@ protected Provider resolveBqProvider(Injectee injectee) { Annotation bindingAnnotation = injectee.getRequiredQualifiers().isEmpty() ? null : injectee.getRequiredQualifiers().iterator().next(); - Key key = bindingAnnotation == null - ? Key.get(typeLiteral) - : Key.get(typeLiteral, bindingAnnotation); - + Key key = getKey(typeLiteral, bindingAnnotation); if(!injector.hasProvider(key) && !allowDynamicInjectionForKey(injectee, key)) { return null; } - return injector.getProvider(key); + return injector. getProvider(key); + } + + private Key getKey(TypeLiteral typeLiteral, Annotation bindingAnnotation) { + if (bindingAnnotation instanceof Named) { + return Key.get(typeLiteral, ((Named) bindingAnnotation).value()); + } else { + return bindingAnnotation == null + ? Key.get(typeLiteral) + : Key.get(typeLiteral, bindingAnnotation); + } } /** diff --git a/bootique-jersey-jakarta/src/main/java/io/bootique/jersey/BqInjectorBridge.java b/bootique-jersey-jakarta/src/main/java/io/bootique/jersey/BqInjectorBridge.java index 5737f1f..bf92a10 100644 --- a/bootique-jersey-jakarta/src/main/java/io/bootique/jersey/BqInjectorBridge.java +++ b/bootique-jersey-jakarta/src/main/java/io/bootique/jersey/BqInjectorBridge.java @@ -96,7 +96,7 @@ public BqBindingActiveDescriptor(Provider provider, Type implType, Set provider, Type implType, Set qualifiers){ + for (Annotation qualifier : qualifiers) { + if(qualifier instanceof jakarta.inject.Named) { + return ((jakarta.inject.Named) qualifier).value(); + } else if(qualifier instanceof javax.inject.Named) { + return ((javax.inject.Named) qualifier).value(); + } + } + return null; + } + public BqBindingActiveDescriptor() { this.provider = null; this.implClass = null; diff --git a/bootique-jersey-jakarta/src/test/java/io/bootique/jersey/IssuesNamedAnnotationInjectionIT.java b/bootique-jersey-jakarta/src/test/java/io/bootique/jersey/IssuesNamedAnnotationInjectionIT.java new file mode 100644 index 0000000..4787c73 --- /dev/null +++ b/bootique-jersey-jakarta/src/test/java/io/bootique/jersey/IssuesNamedAnnotationInjectionIT.java @@ -0,0 +1,223 @@ +/* + * Licensed to ObjectStyle LLC under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ObjectStyle LLC licenses + * this file to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ + +package io.bootique.jersey; + +import io.bootique.BQRuntime; +import io.bootique.Bootique; +import io.bootique.di.BQInject; +import io.bootique.jetty.junit5.JettyTester; +import io.bootique.junit5.BQApp; +import io.bootique.junit5.BQTest; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.Configuration; +import jakarta.ws.rs.core.Context; +import jakarta.ws.rs.core.MediaType; +import jakarta.ws.rs.core.Response; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; + +import javax.inject.Inject; +import javax.inject.Named; +import java.util.concurrent.atomic.AtomicInteger; + +import static java.lang.annotation.RetentionPolicy.RUNTIME; +import static org.junit.jupiter.api.Assertions.assertEquals; + +@Disabled("Demonstrates several issues with @Inject and @Named annotations") +@BQTest +public class IssuesNamedAnnotationInjectionIT { + + private static final String TEST_PROPERTY = "bq.test.label"; + private static final InjectedServiceInterface serviceA = new InjectedServiceImplA(); + private static final InjectedServiceInterface serviceB = new InjectedServiceImplB(); + private static final InjectedServiceInterface serviceC = new InjectedServiceImplC(); + + static final JettyTester jetty = JettyTester.create(); + + @BQApp + static final BQRuntime app = Bootique.app("-s") + .autoLoadModules() + .module(b -> b.bind(InjectedServiceInterface.class, "A").toInstance(serviceA)) + .module(b -> b.bind(InjectedServiceInterface.class, "B").toInstance(serviceB)) + .module(b -> b.bind(InjectedServiceInterface.class, CustomQualifierC.class).toInstance(serviceC)) + .module(b -> JerseyModule.extend(b) + .addFeature(ctx -> { + ctx.property(TEST_PROPERTY, "x"); + return false; + }) + .addResource(NamedFieldInjectedResourceJavaXAnnotations.class) + .addResource(NamedFieldInjectedResourceBQAnnotations.class) + .addResource(NamedFieldInjectedResourceCustomJavaXAnnotations.class)) + .module(jetty.moduleReplacingConnectors()) + .createRuntime(); + + @BeforeEach + public void before() { + serviceA.reset(); + serviceB.reset(); + serviceC.reset(); + } + + + @Test + public void testNamedFieldInjectedJakartaAnnotations() { + + Response r1 = jetty.getTarget().path("nfJakartaInject").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("nf_1x", r1.readEntity(String.class)); + r1.close(); + + Response r2 = jetty.getTarget().path("nfJakartaInject").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("nf_2x", r2.readEntity(String.class)); + r2.close(); + } + + @Test + public void testNamedFieldInjectedBQAnnotations() { + + Response r1 = jetty.getTarget().path("nfBQInject").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("nf_1x", r1.readEntity(String.class)); + r1.close(); + + Response r2 = jetty.getTarget().path("nfBQInject").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("nf_2x", r2.readEntity(String.class)); + r2.close(); + } + + @Test + public void testNamedFieldInjectedCustomJavaXAnnotations() { + + Response r1 = jetty.getTarget().path("nfCustomInjectJavaX").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("nfC_1x", r1.readEntity(String.class)); + r1.close(); + + Response r2 = jetty.getTarget().path("nfCustomInjectJavaX").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("nfC_2x", r2.readEntity(String.class)); + r2.close(); + } + + @Test + public void testNamedFieldInjectedCustomJakartaAnnotations() { + + Response r1 = jetty.getTarget().path("nfCustomInjectJakarta").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("nfD_1x", r1.readEntity(String.class)); + r1.close(); + + Response r2 = jetty.getTarget().path("nfCustomInjectJakarta").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("nfD_2x", r2.readEntity(String.class)); + r2.close(); + } + + + @Path("/nfJavaXInject") + @Produces(MediaType.TEXT_PLAIN) + public static class NamedFieldInjectedResourceJavaXAnnotations { + + @Inject + @Named("A") + private InjectedServiceInterface serviceA; + + @Context + private Configuration config; + + @GET + public String get() { + return "nf_" + serviceA.getNext() + config.getProperty(TEST_PROPERTY); + } + } + + @Path("/nfBQInject") + @Produces(MediaType.TEXT_PLAIN) + public static class NamedFieldInjectedResourceBQAnnotations { + + @BQInject + @Named("B") + private InjectedServiceInterface serviceB; + + @Context + private Configuration config; + + @GET + public String get() { + return "nf_" + serviceB.getNext() + config.getProperty(TEST_PROPERTY); + } + } + + @Path("/nfCustomInjectJavaX") + @Produces(MediaType.TEXT_PLAIN) + public static class NamedFieldInjectedResourceCustomJavaXAnnotations { + + @Inject + @CustomQualifierC + private InjectedServiceInterface serviceC; + + @Context + private Configuration config; + + @GET + public String get() { + return "nfC_" + serviceC.getNext() + config.getProperty(TEST_PROPERTY); + } + } + + + public static interface InjectedServiceInterface { + AtomicInteger atomicInt = new AtomicInteger(); + default void reset() { + atomicInt.set(0); + } + int getNext(); + } + + public static class InjectedServiceImplA implements InjectedServiceInterface { + public int getNext() { + return atomicInt.incrementAndGet(); + } + } + + public static class InjectedServiceImplB implements InjectedServiceInterface { + public int getNext() { + return atomicInt.incrementAndGet(); + } + } + + public static class InjectedServiceImplC implements InjectedServiceInterface { + public int getNext() { + return atomicInt.incrementAndGet(); + } + } + + @java.lang.annotation.Documented + @java.lang.annotation.Retention(RUNTIME) + @javax.inject.Qualifier + public @interface CustomQualifierC { + } + +} diff --git a/bootique-jersey-jakarta/src/test/java/io/bootique/jersey/ResourceInjectionIT.java b/bootique-jersey-jakarta/src/test/java/io/bootique/jersey/ResourceInjectionIT.java index b17bddd..ed83eb4 100644 --- a/bootique-jersey-jakarta/src/test/java/io/bootique/jersey/ResourceInjectionIT.java +++ b/bootique-jersey-jakarta/src/test/java/io/bootique/jersey/ResourceInjectionIT.java @@ -21,7 +21,6 @@ import io.bootique.BQRuntime; import io.bootique.Bootique; -import io.bootique.jersey.JerseyModule; import io.bootique.jetty.junit5.JettyTester; import io.bootique.junit5.BQApp; import io.bootique.junit5.BQTest; @@ -38,6 +37,7 @@ import javax.inject.Inject; import java.util.concurrent.atomic.AtomicInteger; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.jupiter.api.Assertions.assertEquals; @BQTest @@ -45,6 +45,8 @@ public class ResourceInjectionIT { private static final String TEST_PROPERTY = "bq.test.label"; private static final InjectedService service = new InjectedService(); + private static final InjectedServiceInterface serviceA = new InjectedServiceImplA(); + private static final InjectedServiceInterface serviceB = new InjectedServiceImplB(); static final JettyTester jetty = JettyTester.create(); @@ -52,6 +54,8 @@ public class ResourceInjectionIT { static final BQRuntime app = Bootique.app("-s") .autoLoadModules() .module(b -> b.bind(InjectedService.class).toInstance(service)) + .module(b -> b.bind(InjectedServiceInterface.class, "A").toInstance(serviceA)) + .module(b -> b.bind(InjectedServiceInterface.class, CustomQualifier.class).toInstance(serviceB)) .module(b -> b.bind(UnInjectedResource.class).toProviderInstance(() -> new UnInjectedResource(service))) .module(b -> JerseyModule.extend(b) .addFeature(ctx -> { @@ -59,6 +63,8 @@ public class ResourceInjectionIT { return false; }) .addResource(FieldInjectedResource.class) + .addResource(NamedFieldInjectedResourceWithJakartaAnnotations.class) + .addResource(NamedFieldInjectedResourceCustomJakartaAnnotations.class) .addResource(ConstructorInjectedResource.class) .addResource(UnInjectedResource.class)) .module(jetty.moduleReplacingConnectors()) @@ -67,6 +73,8 @@ public class ResourceInjectionIT { @BeforeEach public void before() { service.reset(); + serviceA.reset(); + serviceB.reset(); } @Test @@ -83,6 +91,34 @@ public void testFieldInjected() { r2.close(); } + @Test + public void testNamedFieldInjectedJakartaAnnotations() { + + Response r1 = jetty.getTarget().path("nfJakartaInject").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("nf_1x", r1.readEntity(String.class)); + r1.close(); + + Response r2 = jetty.getTarget().path("nfJakartaInject").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("nf_2x", r2.readEntity(String.class)); + r2.close(); + } + + @Test + public void testNamedFieldInjectedCustomJakartaAnnotations() { + + Response r1 = jetty.getTarget().path("nfCustomInjectJakarta").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("nfB_1x", r1.readEntity(String.class)); + r1.close(); + + Response r2 = jetty.getTarget().path("nfCustomInjectJakarta").request().get(); + assertEquals(Response.Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("nfB_2x", r2.readEntity(String.class)); + r2.close(); + } + @Test public void testConstructorInjected() { @@ -127,6 +163,41 @@ public String get() { } } + @Path("/nfJakartaInject") + @Produces(MediaType.TEXT_PLAIN) + public static class NamedFieldInjectedResourceWithJakartaAnnotations { + + @jakarta.inject.Inject + @jakarta.inject.Named("A") + private InjectedServiceInterface serviceA; + + @Context + private Configuration config; + + @GET + public String get() { + return "nf_" + serviceA.getNext() + config.getProperty(TEST_PROPERTY); + } + } + + @Path("/nfCustomInjectJakarta") + @Produces(MediaType.TEXT_PLAIN) + public static class NamedFieldInjectedResourceCustomJakartaAnnotations { + + @jakarta.inject.Inject + @CustomQualifier + private InjectedServiceInterface serviceB; + + @Context + private Configuration config; + + @GET + public String get() { + return "nfB_" + serviceB.getNext() + config.getProperty(TEST_PROPERTY); + } + } + + @Path("/c") @Produces(MediaType.TEXT_PLAIN) public static class ConstructorInjectedResource { @@ -178,4 +249,27 @@ public int getNext() { return atomicInt.incrementAndGet(); } } + + + public static interface InjectedServiceInterface { + AtomicInteger atomicInt = new AtomicInteger(); + default void reset() {atomicInt.set(0);} + public int getNext(); + } + + public static class InjectedServiceImplA implements InjectedServiceInterface { + public int getNext() { + return atomicInt.incrementAndGet(); + } + } + public static class InjectedServiceImplB implements InjectedServiceInterface { + public int getNext() {return atomicInt.incrementAndGet();} + } + + @java.lang.annotation.Documented + @java.lang.annotation.Retention(RUNTIME) + @jakarta.inject.Qualifier + public @interface CustomQualifier { + } + } diff --git a/bootique-jersey/src/main/java/io/bootique/jersey/BqInjectorBridge.java b/bootique-jersey/src/main/java/io/bootique/jersey/BqInjectorBridge.java index 4c334e3..eb95ac9 100644 --- a/bootique-jersey/src/main/java/io/bootique/jersey/BqInjectorBridge.java +++ b/bootique-jersey/src/main/java/io/bootique/jersey/BqInjectorBridge.java @@ -27,6 +27,7 @@ import org.glassfish.hk2.utilities.ServiceLocatorUtilities; import javax.inject.Inject; +import javax.inject.Named; import javax.inject.Provider; import javax.inject.Singleton; import java.lang.annotation.Annotation; @@ -98,7 +99,7 @@ public BqBindingActiveDescriptor(Provider provider, Type implType, Set provider, Type implType, Set qualifiers){ + for (Annotation qualifier : qualifiers) { + if(qualifier instanceof Named) { + // special case for @Named annotation + return ((Named) qualifier).value(); + } + } + return null; + } + public BqBindingActiveDescriptor() { this.provider = null; this.implClass = null; diff --git a/bootique-jersey/src/test/java/io/bootique/jersey/ResourceInjectionIT.java b/bootique-jersey/src/test/java/io/bootique/jersey/ResourceInjectionIT.java index 9995eb0..882a536 100644 --- a/bootique-jersey/src/test/java/io/bootique/jersey/ResourceInjectionIT.java +++ b/bootique-jersey/src/test/java/io/bootique/jersey/ResourceInjectionIT.java @@ -28,6 +28,7 @@ import org.junit.jupiter.api.Test; import javax.inject.Inject; +import javax.inject.Named; import javax.ws.rs.GET; import javax.ws.rs.Path; import javax.ws.rs.Produces; @@ -38,6 +39,7 @@ import javax.ws.rs.core.Response.Status; import java.util.concurrent.atomic.AtomicInteger; +import static java.lang.annotation.RetentionPolicy.RUNTIME; import static org.junit.jupiter.api.Assertions.assertEquals; @BQTest @@ -45,6 +47,8 @@ public class ResourceInjectionIT { private static final String TEST_PROPERTY = "bq.test.label"; private static final InjectedService service = new InjectedService(); + private static final InjectedServiceInterface serviceA = new InjectedServiceImplA(); + private static final InjectedServiceInterface serviceB = new InjectedServiceImplB(); static final JettyTester jetty = JettyTester.create(); @@ -52,6 +56,8 @@ public class ResourceInjectionIT { static final BQRuntime app = Bootique.app("-s") .autoLoadModules() .module(b -> b.bind(InjectedService.class).toInstance(service)) + .module(b -> b.bind(InjectedServiceInterface.class, "A").toInstance(serviceA)) + .module(b -> b.bind(InjectedServiceInterface.class, ServiceBQualifier.class).toInstance(serviceB)) .module(b -> b.bind(UnInjectedResource.class).toProviderInstance(() -> new UnInjectedResource(service))) .module(b -> JerseyModule.extend(b) .addFeature(ctx -> { @@ -59,6 +65,7 @@ public class ResourceInjectionIT { return false; }) .addResource(FieldInjectedResource.class) + .addResource(NamedFieldInjectedResource.class) .addResource(ConstructorInjectedResource.class) .addResource(UnInjectedResource.class)) .module(jetty.moduleReplacingConnectors()) @@ -83,6 +90,20 @@ public void testFieldInjected() { r2.close(); } + @Test + public void testNamedFieldInjected() { + + Response r1 = jetty.getTarget().path("nf").request().get(); + assertEquals(Status.OK.getStatusCode(), r1.getStatus()); + assertEquals("nf_1_2_x", r1.readEntity(String.class)); + r1.close(); + + Response r2 = jetty.getTarget().path("nf").request().get(); + assertEquals(Status.OK.getStatusCode(), r2.getStatus()); + assertEquals("nf_3_4_x", r2.readEntity(String.class)); + r2.close(); + } + @Test public void testConstructorInjected() { @@ -127,6 +148,27 @@ public String get() { } } + @Path("/nf") + @Produces(MediaType.TEXT_PLAIN) + public static class NamedFieldInjectedResource { + + @Inject + @Named("A") + private InjectedServiceInterface serviceA; + + @Inject + @ServiceBQualifier + private InjectedServiceInterface serviceB; + + @Context + private Configuration config; + + @GET + public String get() { + return "nf_" + serviceA.getNext() + "_" + serviceA.getNext()+ "_" + config.getProperty(TEST_PROPERTY); + } + } + @Path("/c") @Produces(MediaType.TEXT_PLAIN) public static class ConstructorInjectedResource { @@ -178,4 +220,41 @@ public int getNext() { return atomicInt.incrementAndGet(); } } + + public static interface InjectedServiceInterface { + public void reset(); + public int getNext(); + } + + public static class InjectedServiceImplA implements InjectedServiceInterface { + + private AtomicInteger atomicInt = new AtomicInteger(); + + public void reset() { + atomicInt.set(0); + } + + public int getNext() { + return atomicInt.incrementAndGet(); + } + } + + public static class InjectedServiceImplB implements InjectedServiceInterface { + + private AtomicInteger atomicInt = new AtomicInteger(); + + public void reset() { + atomicInt.set(0); + } + + public int getNext() { + return atomicInt.incrementAndGet(); + } + } + + @java.lang.annotation.Documented + @java.lang.annotation.Retention(RUNTIME) + @javax.inject.Qualifier + public @interface ServiceBQualifier { + } }