Allow HK2 AbstractBinder class to bind before the Feature is called (#4394)

Signed-off-by: Jan Supol <jan.supol@oracle.com>
diff --git a/core-common/src/main/java/org/glassfish/jersey/model/internal/CommonConfig.java b/core-common/src/main/java/org/glassfish/jersey/model/internal/CommonConfig.java
index ca51259..d70faea 100644
--- a/core-common/src/main/java/org/glassfish/jersey/model/internal/CommonConfig.java
+++ b/core-common/src/main/java/org/glassfish/jersey/model/internal/CommonConfig.java
@@ -1,5 +1,5 @@
 /*
- * Copyright (c) 2012, 2019 Oracle and/or its affiliates. All rights reserved.
+ * Copyright (c) 2012, 2020 Oracle and/or its affiliates. All rights reserved.
  *
  * This program and the accompanying materials are made available under the
  * terms of the Eclipse Public License v. 2.0, which is available at
@@ -28,6 +28,7 @@
 import java.util.Map;
 import java.util.Set;
 import java.util.TreeSet;
+import java.util.function.Consumer;
 import java.util.function.Function;
 import java.util.function.Predicate;
 import java.util.logging.Level;
@@ -618,15 +619,19 @@
      * @param injectionManager injection manager in which the binders and features should be configured.
      */
     public void configureMetaProviders(InjectionManager injectionManager, ManagedObjectsFinalizer finalizer) {
+        final Set<Object> configuredExternals = Collections.newSetFromMap(new IdentityHashMap<>());
+
         // First, configure existing binders
-        Set<Binder> configuredBinders = configureBinders(injectionManager, Collections.emptySet());
+        final Set<Binder> configuredBinders = configureBinders(injectionManager, Collections.emptySet());
 
         // Check whether meta providers have been initialized for a config this config has been loaded from.
         if (!disableMetaProviderConfiguration) {
+            // Next, register external meta objects
+            configureExternalObjects(injectionManager, configuredExternals);
             // Configure all features
             configureFeatures(injectionManager, new HashSet<>(), resetRegistrations(), finalizer);
-            // Next, register external meta objects
-            configureExternalObjects(injectionManager);
+            // Next, register external meta objects registered by features
+            configureExternalObjects(injectionManager, configuredExternals);
             // At last, configure any new binders added by features
             configureBinders(injectionManager, configuredBinders);
         }
@@ -653,11 +658,17 @@
                 .collect(Collectors.toList());
     }
 
-    private void configureExternalObjects(InjectionManager injectionManager) {
+    private void configureExternalObjects(InjectionManager injectionManager, Set<Object> externalObjects) {
+        Consumer<Object> registerOnce = o -> {
+            if (!externalObjects.contains(o)) {
+                injectionManager.register(o);
+                externalObjects.add(o);
+            }
+        };
         componentBag.getInstances(model -> ComponentBag.EXTERNAL_ONLY.test(model, injectionManager))
-                .forEach(injectionManager::register);
+                .forEach(registerOnce);
         componentBag.getClasses(model -> ComponentBag.EXTERNAL_ONLY.test(model, injectionManager))
-                .forEach(injectionManager::register);
+                .forEach(registerOnce);
     }
 
     private void configureFeatures(InjectionManager injectionManager,
diff --git a/tests/e2e-inject/hk2/src/test/java/org/glassfish/jersey/tests/e2e/inject/hk2/HK2AbstractBinderInFeaturesTest.java b/tests/e2e-inject/hk2/src/test/java/org/glassfish/jersey/tests/e2e/inject/hk2/HK2AbstractBinderInFeaturesTest.java
new file mode 100644
index 0000000..7a348e2
--- /dev/null
+++ b/tests/e2e-inject/hk2/src/test/java/org/glassfish/jersey/tests/e2e/inject/hk2/HK2AbstractBinderInFeaturesTest.java
@@ -0,0 +1,111 @@
+/*
+ * Copyright (c) 2020 Oracle and/or its affiliates. All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v. 2.0, which is available at
+ * http://www.eclipse.org/legal/epl-2.0.
+ *
+ * This Source Code may also be made available under the following Secondary
+ * Licenses when the conditions for such availability set forth in the
+ * Eclipse Public License v. 2.0 are satisfied: GNU General Public License,
+ * version 2 with the GNU Classpath Exception, which is available at
+ * https://www.gnu.org/software/classpath/license.html.
+ *
+ * SPDX-License-Identifier: EPL-2.0 OR GPL-2.0 WITH Classpath-exception-2.0
+ */
+
+package org.glassfish.jersey.tests.e2e.inject.hk2;
+
+import org.glassfish.jersey.internal.inject.AbstractBinder;
+import org.glassfish.jersey.server.ResourceConfig;
+import org.glassfish.jersey.test.JerseyTest;
+import org.junit.Test;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+import javax.ws.rs.core.Application;
+import javax.ws.rs.core.Feature;
+import javax.ws.rs.core.FeatureContext;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.MatcherAssert.assertThat;
+
+/**
+ * Test that HK2 binder allows for injection into a feature
+ */
+public class HK2AbstractBinderInFeaturesTest extends JerseyTest {
+
+    private static final AtomicInteger binderCounter = new AtomicInteger();
+    private static final AtomicInteger feature1Counter = new AtomicInteger();
+    private static final AtomicInteger feature2Counter = new AtomicInteger();
+    private static final String VALUE = "CONFIGURED_VALUE";
+
+    public static class InjectableHK2Binder extends org.glassfish.hk2.utilities.binding.AbstractBinder {
+        @Override
+        protected void configure() {
+            binderCounter.incrementAndGet();
+            bindAsContract(ConfigurableInjectable.class).to(Injectable.class).in(Singleton.class);
+        }
+    }
+
+    public static class JerseyInjectableHK2Binder extends AbstractBinder {
+        @Override
+        protected void configure() {
+            bindAsContract(ConfigurableInjectable.class).to(ExtendedInjectable.class).in(Singleton.class);
+        }
+    }
+
+    public static final class InjectableHK2BindingFeature implements Feature {
+        private final Injectable service;
+        private final ExtendedInjectable extendedService;
+
+        @Inject
+        public InjectableHK2BindingFeature(Injectable service, ExtendedInjectable extendedService) {
+            feature1Counter.incrementAndGet();
+            this.service = service;
+            this.extendedService = extendedService;
+        }
+
+        @Override
+        public boolean configure(FeatureContext context) {
+            if (service != null) {
+                ((ConfigurableInjectable) service).set(VALUE);
+            }
+            if (extendedService != null) {
+                feature2Counter.incrementAndGet();
+            }
+            return true;
+        }
+    }
+
+    public static class ConfigurableInjectable implements ExtendedInjectable {
+        private String value;
+        public void set(String value) {
+            this.value = value;
+        }
+
+        @Override
+        public String toString() {
+            return value;
+        }
+    }
+
+    public static interface ExtendedInjectable extends Injectable {
+    };
+
+    @Override
+    protected Application configure() {
+        return new ResourceConfig(InjectableHK2BindingFeature.class, AbstractBinderTestResource.class,
+                InjectableTestFilter.class, InjectableHK2Binder.class).register(new JerseyInjectableHK2Binder());
+    }
+
+    @Test
+    public void testInjectableInjection() {
+        String response = target().request().get(String.class);
+        assertThat(response, is(VALUE));
+        assertThat(1, is(binderCounter.get()));
+        assertThat(1, is(feature1Counter.get()));
+        assertThat(1, is(feature2Counter.get()));
+    }
+}