summaryrefslogtreecommitdiffstats
path: root/container-di/src/main/java
diff options
context:
space:
mode:
Diffstat (limited to 'container-di/src/main/java')
-rw-r--r--container-di/src/main/java/com/yahoo/container/bundle/MockBundle.java264
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/CloudSubscriberFactory.java148
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/ConfigRetriever.java206
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/Container.java273
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/Osgi.java43
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentGraph.java407
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentNode.java304
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.java105
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Exceptions.java45
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/core/GuiceNode.java78
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/core/JerseyNode.java92
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Keys.java37
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Node.java164
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java4
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/osgi/BundleClasses.java27
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/osgi/OsgiUtil.java168
16 files changed, 2362 insertions, 3 deletions
diff --git a/container-di/src/main/java/com/yahoo/container/bundle/MockBundle.java b/container-di/src/main/java/com/yahoo/container/bundle/MockBundle.java
new file mode 100644
index 00000000000..a6524b41886
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/bundle/MockBundle.java
@@ -0,0 +1,264 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.bundle;
+
+import org.osgi.framework.Bundle;
+import org.osgi.framework.BundleContext;
+import org.osgi.framework.ServiceReference;
+import org.osgi.framework.Version;
+import org.osgi.framework.wiring.BundleCapability;
+import org.osgi.framework.wiring.BundleRequirement;
+import org.osgi.framework.wiring.BundleRevision;
+import org.osgi.framework.wiring.BundleWire;
+import org.osgi.framework.wiring.BundleWiring;
+import org.osgi.resource.Capability;
+import org.osgi.resource.Requirement;
+import org.osgi.resource.Wire;
+
+import java.io.File;
+import java.io.InputStream;
+import java.net.URL;
+import java.security.cert.X509Certificate;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Dictionary;
+import java.util.Enumeration;
+import java.util.Hashtable;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * @author gjoranv
+ * @author ollivir
+ */
+public class MockBundle implements Bundle, BundleWiring {
+ public static final String SymbolicName = "mock-bundle";
+ public static final Version BundleVersion = new Version(1, 0, 0);
+
+ private static final Class<BundleWiring> bundleWiringClass = BundleWiring.class;
+
+ @Override
+ public int getState() {
+ return Bundle.ACTIVE;
+ }
+
+ @Override
+ public void start(int options) {
+ }
+
+ @Override
+ public void start() {
+ }
+
+ @Override
+ public void stop(int options) {
+ }
+
+ @Override
+ public void stop() {
+ }
+
+ @Override
+ public void update(InputStream input) {
+ }
+
+ @Override
+ public void update() {
+ }
+
+ @Override
+ public void uninstall() {
+ }
+
+ @Override
+ public Dictionary<String, String> getHeaders(String locale) {
+ return getHeaders();
+ }
+
+ @Override
+ public String getSymbolicName() {
+ return SymbolicName;
+ }
+
+ @Override
+ public Version getVersion() {
+ return BundleVersion;
+ }
+
+ @Override
+ public String getLocation() {
+ return getSymbolicName();
+ }
+
+ @Override
+ public long getBundleId() {
+ return 0L;
+ }
+
+ @Override
+ public Dictionary<String, String> getHeaders() {
+ return new Hashtable<>();
+ }
+
+ @Override
+ public ServiceReference<?>[] getRegisteredServices() {
+ return new ServiceReference<?>[0];
+ }
+
+ @Override
+ public ServiceReference<?>[] getServicesInUse() {
+ return getRegisteredServices();
+ }
+
+ @Override
+ public boolean hasPermission(Object permission) {
+ return true;
+ }
+
+ @Override
+ public URL getResource(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Class<?> loadClass(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Enumeration<URL> getResources(String name) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Enumeration<String> getEntryPaths(String path) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public URL getEntry(String path) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Enumeration<URL> findEntries(String path, String filePattern, boolean recurse) {
+ return Collections.emptyEnumeration();
+ }
+
+
+ @Override
+ public long getLastModified() {
+ return 1L;
+ }
+
+ @Override
+ public BundleContext getBundleContext() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Map<X509Certificate, List<X509Certificate>> getSignerCertificates(int signersType) {
+ return Collections.emptyMap();
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public <T> T adapt(Class<T> type) {
+ if (type.equals(bundleWiringClass)) {
+ return (T) this;
+ } else {
+ throw new UnsupportedOperationException();
+ }
+ }
+
+ @Override
+ public File getDataFile(String filename) {
+ return null;
+ }
+
+ @Override
+ public int compareTo(Bundle o) {
+ return Long.compare(getBundleId(), o.getBundleId());
+ }
+
+
+ //TODO: replace with mockito
+ @Override
+ public List<URL> findEntries(String p1, String p2, int p3) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<Wire> getRequiredResourceWires(String p1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<Capability> getResourceCapabilities(String p1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isCurrent() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<BundleWire> getRequiredWires(String p1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<BundleCapability> getCapabilities(String p1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<Wire> getProvidedResourceWires(String p1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<BundleWire> getProvidedWires(String p1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BundleRevision getRevision() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public List<Requirement> getResourceRequirements(String p1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public boolean isInUse() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Collection<String> listResources(String p1, String p2, int p3) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public ClassLoader getClassLoader() {
+ return MockBundle.class.getClassLoader();
+ }
+
+ @Override
+ public List<BundleRequirement> getRequirements(String p1) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public BundleRevision getResource() {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ public Bundle getBundle() {
+ throw new UnsupportedOperationException();
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/CloudSubscriberFactory.java b/container-di/src/main/java/com/yahoo/container/di/CloudSubscriberFactory.java
new file mode 100644
index 00000000000..3f3991760e3
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/CloudSubscriberFactory.java
@@ -0,0 +1,148 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.subscription.ConfigHandle;
+import com.yahoo.config.subscription.ConfigSource;
+import com.yahoo.config.subscription.ConfigSourceSet;
+import com.yahoo.config.subscription.ConfigSubscriber;
+import com.yahoo.container.di.config.Subscriber;
+import com.yahoo.container.di.config.SubscriberFactory;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.WeakHashMap;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+/**
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class CloudSubscriberFactory implements SubscriberFactory {
+ private static final Logger log = Logger.getLogger(CloudSubscriberFactory.class.getName());
+
+ private final ConfigSource configSource;
+ private Optional<Long> testGeneration = Optional.empty();
+ private Map<CloudSubscriber, Integer> activeSubscribers = new WeakHashMap<>();
+
+ public CloudSubscriberFactory(ConfigSource configSource) {
+ this.configSource = configSource;
+ }
+
+ @Override
+ public Subscriber getSubscriber(Set<? extends ConfigKey<?>> configKeys) {
+ Set<ConfigKey<ConfigInstance>> subscriptionKeys = new HashSet<>();
+ for(ConfigKey<?> key: configKeys) {
+ @SuppressWarnings("unchecked") // ConfigKey is defined as <CONFIGCLASS extends ConfigInstance>
+ ConfigKey<ConfigInstance> invariant = (ConfigKey<ConfigInstance>) key;
+ subscriptionKeys.add(invariant);
+ }
+ CloudSubscriber subscriber = new CloudSubscriber(subscriptionKeys, configSource);
+
+ testGeneration.ifPresent(subscriber.subscriber::reload); //TODO: test specific code, remove
+ activeSubscribers.put(subscriber, 0);
+
+ return subscriber;
+ }
+
+ //TODO: test specific code, remove
+ @Override
+ public void reloadActiveSubscribers(long generation) {
+ testGeneration = Optional.of(generation);
+
+ List<CloudSubscriber> subscribers = new ArrayList<>(activeSubscribers.keySet());
+ subscribers.forEach(s -> s.subscriber.reload(generation));
+ }
+
+ private static class CloudSubscriber implements Subscriber {
+ private final ConfigSubscriber subscriber;
+ private final Map<ConfigKey<ConfigInstance>, ConfigHandle<ConfigInstance>> handles = new HashMap<>();
+
+ // if waitNextGeneration has not yet been called, -1 should be returned
+ private long generation = -1L;
+
+ // True if this reconfiguration was caused by a system-internal redeploy, not an external application change
+ private boolean internalRedeploy = false;
+
+ private CloudSubscriber(Set<ConfigKey<ConfigInstance>> keys, ConfigSource configSource) {
+ this.subscriber = new ConfigSubscriber(configSource);
+ keys.forEach(k -> handles.put(k, subscriber.subscribe(k.getConfigClass(), k.getConfigId())));
+ }
+
+ @Override
+ public boolean configChanged() {
+ return handles.values().stream().anyMatch(ConfigHandle::isChanged);
+ }
+
+ @Override
+ public long generation() {
+ return generation;
+ }
+
+ @Override
+ public boolean internalRedeploy() {
+ return internalRedeploy;
+ }
+
+ //mapValues returns a view,, so we need to force evaluation of it here to prevent deferred evaluation.
+ @Override
+ public Map<ConfigKey<ConfigInstance>, ConfigInstance> config() {
+ Map<ConfigKey<ConfigInstance>, ConfigInstance> ret = new HashMap<>();
+ handles.forEach((k, v) -> ret.put(k, v.getConfig()));
+ return ret;
+ }
+
+ @Override
+ public long waitNextGeneration() {
+ if (handles.isEmpty()) {
+ throw new IllegalStateException("No config keys registered");
+ }
+
+ /* Catch and just log config exceptions due to missing config values for parameters that do
+ * not have a default value. These exceptions occur when the user has removed a component
+ * from services.xml, and the component takes a config that has parameters without a
+ * default value in the def-file. There is a new 'components' config underway, where the
+ * component is removed, so this old config generation will soon be replaced by a new one. */
+ boolean gotNextGen = false;
+ int numExceptions = 0;
+ while (!gotNextGen) {
+ try {
+ if (subscriber.nextGeneration()) {
+ gotNextGen = true;
+ }
+ } catch (IllegalArgumentException e) {
+ numExceptions++;
+ log.log(Level.WARNING, "Got exception from the config system (please ignore the exception if you just removed "
+ + "a component from your application that used the mentioned config): ", e);
+ if (numExceptions >= 5) {
+ throw new IllegalArgumentException("Failed retrieving the next config generation.", e);
+ }
+ }
+ }
+
+ generation = subscriber.getGeneration();
+ internalRedeploy = subscriber.isInternalRedeploy();
+ return generation;
+ }
+
+ @Override
+ public void close() {
+ subscriber.close();
+ }
+ }
+
+
+ public static class Provider implements com.google.inject.Provider<SubscriberFactory> {
+ @Override
+ public SubscriberFactory get() {
+ return new CloudSubscriberFactory(ConfigSourceSet.createDefault());
+ }
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/ConfigRetriever.java b/container-di/src/main/java/com/yahoo/container/di/ConfigRetriever.java
new file mode 100644
index 00000000000..fe315c0eba5
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/ConfigRetriever.java
@@ -0,0 +1,206 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di;
+
+import com.google.common.collect.Sets;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.container.di.componentgraph.core.Keys;
+import com.yahoo.container.di.config.Subscriber;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.yahoo.log.LogLevel.DEBUG;
+
+/**
+ * @author Tony Vaagenes
+ * @author gjoranv
+ * @author ollivir
+ */
+public final class ConfigRetriever {
+ private static final Logger log = Logger.getLogger(ConfigRetriever.class.getName());
+
+ private final Set<ConfigKey<? extends ConfigInstance>> bootstrapKeys;
+ private Set<ConfigKey<? extends ConfigInstance>> componentSubscriberKeys;
+ private final Subscriber bootstrapSubscriber;
+ private Subscriber componentSubscriber;
+ private final Function<Set<ConfigKey<? extends ConfigInstance>>, Subscriber> subscribe;
+
+ public ConfigRetriever(Set<ConfigKey<? extends ConfigInstance>> bootstrapKeys,
+ Function<Set<ConfigKey<? extends ConfigInstance>>, Subscriber> subscribe) {
+ this.bootstrapKeys = bootstrapKeys;
+ this.componentSubscriberKeys = new HashSet<>();
+ this.subscribe = subscribe;
+ if (bootstrapKeys.isEmpty()) {
+ throw new IllegalArgumentException("Bootstrap key set is empty");
+ }
+ this.bootstrapSubscriber = subscribe.apply(bootstrapKeys);
+ this.componentSubscriber = subscribe.apply(componentSubscriberKeys);
+ }
+
+ /**
+ * Loop forever until we get config
+ */
+ public ConfigSnapshot getConfigs(Set<ConfigKey<? extends ConfigInstance>> componentConfigKeys, long leastGeneration,
+ boolean restartOnRedeploy) {
+ while (true) {
+ if (!Sets.intersection(componentConfigKeys, bootstrapKeys).isEmpty()) {
+ throw new IllegalArgumentException(
+ "Component config keys [" + componentConfigKeys + "] overlaps with bootstrap config keys [" + bootstrapKeys + "]");
+ }
+ log.log(DEBUG, "getConfigs: " + componentConfigKeys);
+ Set<ConfigKey<? extends ConfigInstance>> allKeys = new HashSet<>(componentConfigKeys);
+ allKeys.addAll(bootstrapKeys);
+ setupComponentSubscriber(allKeys);
+
+ Optional<ConfigSnapshot> maybeSnapshot = getConfigsOptional(leastGeneration, restartOnRedeploy);
+ if (maybeSnapshot.isPresent()) {
+ ConfigSnapshot snapshot = maybeSnapshot.get();
+ resetComponentSubscriberIfBootstrap(snapshot);
+ return snapshot;
+ }
+ }
+ }
+
+ public ConfigSnapshot getConfigs(Set<ConfigKey<? extends ConfigInstance>> componentConfigKeys, long leastGeneration) {
+ return getConfigs(componentConfigKeys, leastGeneration, false);
+ }
+
+ /**
+ * Try to get config just once
+ */
+ public Optional<ConfigSnapshot> getConfigsOnce(Set<ConfigKey<? extends ConfigInstance>> componentConfigKeys, long leastGeneration,
+ boolean restartOnRedeploy) {
+ if (!Sets.intersection(componentConfigKeys, bootstrapKeys).isEmpty()) {
+ throw new IllegalArgumentException(
+ "Component config keys [" + componentConfigKeys + "] overlaps with bootstrap config keys [" + bootstrapKeys + "]");
+ }
+ log.log(DEBUG, "getConfigsOnce: " + componentConfigKeys);
+
+ Set<ConfigKey<? extends ConfigInstance>> allKeys = new HashSet<>(componentConfigKeys);
+ allKeys.addAll(bootstrapKeys);
+ setupComponentSubscriber(allKeys);
+
+ Optional<ConfigSnapshot> maybeSnapshot = getConfigsOptional(leastGeneration, restartOnRedeploy);
+ maybeSnapshot.ifPresent(this::resetComponentSubscriberIfBootstrap);
+ return maybeSnapshot;
+ }
+
+ private Optional<ConfigSnapshot> getConfigsOptional(long leastGeneration, boolean restartOnRedeploy) {
+ long newestComponentGeneration = componentSubscriber.waitNextGeneration();
+ log.log(DEBUG, "getConfigsOptional: new component generation: " + newestComponentGeneration);
+
+ // leastGeneration is only used to ensure newer generation when the previous generation was invalidated due to an exception
+ if (newestComponentGeneration < leastGeneration) {
+ return Optional.empty();
+ } else if (restartOnRedeploy && !componentSubscriber.internalRedeploy()) { // Don't reconfig - wait for restart
+ return Optional.empty();
+ } else if (bootstrapSubscriber.generation() < newestComponentGeneration) {
+ long newestBootstrapGeneration = bootstrapSubscriber.waitNextGeneration();
+ log.log(DEBUG, "getConfigsOptional: new bootstrap generation: " + bootstrapSubscriber.generation());
+ Optional<ConfigSnapshot> bootstrapConfig = bootstrapConfigIfChanged();
+ if (bootstrapConfig.isPresent()) {
+ return bootstrapConfig;
+ } else {
+ if (newestBootstrapGeneration == newestComponentGeneration) {
+ log.log(DEBUG, "Got new components configs with unchanged bootstrap configs.");
+ return componentsConfigIfChanged();
+ } else {
+ // This should not be a normal case, and hence a warning to allow investigation.
+ log.warning("Did not get same generation for bootstrap (" + newestBootstrapGeneration + ") and components configs ("
+ + newestComponentGeneration + ").");
+ return Optional.empty();
+ }
+ }
+ } else {
+ // bootstrapGen==componentGen (happens only when a new component subscriber returns first config after bootstrap)
+ return componentsConfigIfChanged();
+ }
+ }
+
+ private Optional<ConfigSnapshot> bootstrapConfigIfChanged() {
+ return configIfChanged(bootstrapSubscriber, BootstrapConfigs::new);
+ }
+
+ private Optional<ConfigSnapshot> componentsConfigIfChanged() {
+ return configIfChanged(componentSubscriber, ComponentsConfigs::new);
+ }
+
+ private Optional<ConfigSnapshot> configIfChanged(Subscriber subscriber,
+ Function<Map<ConfigKey<? extends ConfigInstance>, ConfigInstance>, ConfigSnapshot> constructor) {
+ if (subscriber.configChanged()) {
+ return Optional.of(constructor.apply(Keys.covariantCopy(subscriber.config())));
+ } else {
+ return Optional.empty();
+ }
+ }
+
+ private void resetComponentSubscriberIfBootstrap(ConfigSnapshot snapshot) {
+ if (snapshot instanceof BootstrapConfigs) {
+ setupComponentSubscriber(Collections.emptySet());
+ }
+ }
+
+ private void setupComponentSubscriber(Set<ConfigKey<? extends ConfigInstance>> keys) {
+ if (! componentSubscriberKeys.equals(keys)) {
+ componentSubscriber.close();
+ componentSubscriberKeys = keys;
+ try {
+ log.log(DEBUG, "Setting up new component subscriber for keys: " + keys);
+ componentSubscriber = subscribe.apply(keys);
+ } catch (Throwable e) {
+ log.log(Level.WARNING, "Failed setting up subscriptions for component configs: " + e.getMessage());
+ log.log(Level.WARNING, "Config keys: " + keys);
+ throw e;
+ }
+ }
+ }
+
+ public void shutdown() {
+ bootstrapSubscriber.close();
+ componentSubscriber.close();
+ }
+
+ //TODO: check if these are really needed
+ public long getBootstrapGeneration() {
+ return bootstrapSubscriber.generation();
+ }
+
+ public long getComponentsGeneration() {
+ return componentSubscriber.generation();
+ }
+
+ public static class ConfigSnapshot {
+ private final Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs;
+
+ ConfigSnapshot(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs) {
+ this.configs = configs;
+ }
+
+ public Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs() {
+ return configs;
+ }
+
+ public int size() {
+ return configs.size();
+ }
+ }
+
+ public static class BootstrapConfigs extends ConfigSnapshot {
+ BootstrapConfigs(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs) {
+ super(configs);
+ }
+ }
+
+ public static class ComponentsConfigs extends ConfigSnapshot {
+ ComponentsConfigs(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs) {
+ super(configs);
+ }
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/Container.java b/container-di/src/main/java/com/yahoo/container/di/Container.java
new file mode 100644
index 00000000000..b73b55298d4
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/Container.java
@@ -0,0 +1,273 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di;
+
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.ConfigurationRuntimeException;
+import com.yahoo.config.subscription.ConfigInterruptedException;
+import com.yahoo.container.BundlesConfig;
+import com.yahoo.container.ComponentsConfig;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.container.di.ConfigRetriever.BootstrapConfigs;
+import com.yahoo.container.di.ConfigRetriever.ConfigSnapshot;
+import com.yahoo.container.di.componentgraph.core.ComponentGraph;
+import com.yahoo.container.di.componentgraph.core.ComponentNode;
+import com.yahoo.container.di.componentgraph.core.JerseyNode;
+import com.yahoo.container.di.componentgraph.core.Node;
+import com.yahoo.container.di.config.RestApiContext;
+import com.yahoo.container.di.config.SubscriberFactory;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.time.Duration;
+import java.util.HashSet;
+import java.util.IdentityHashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+
+import static com.yahoo.log.LogLevel.DEBUG;
+
+/**
+ * @author gjoranv
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class Container {
+ private final SubscriberFactory subscriberFactory;
+ private ConfigKey<BundlesConfig> bundlesConfigKey;
+ private ConfigKey<ComponentsConfig> componentsConfigKey;
+ private final ComponentDeconstructor componentDeconstructor;
+ private final Osgi osgi;
+
+ private ConfigRetriever configurer;
+ private long previousConfigGeneration = -1L;
+ private long leastGeneration = -1L;
+
+ public Container(SubscriberFactory subscriberFactory, String configId, ComponentDeconstructor componentDeconstructor, Osgi osgi) {
+ this.subscriberFactory = subscriberFactory;
+ this.bundlesConfigKey = new ConfigKey<>(BundlesConfig.class, configId);
+ this.componentsConfigKey = new ConfigKey<>(ComponentsConfig.class, configId);
+ this.componentDeconstructor = componentDeconstructor;
+ this.osgi = osgi;
+
+ Set<ConfigKey<? extends ConfigInstance>> keySet = new HashSet<>();
+ keySet.add(bundlesConfigKey);
+ keySet.add(componentsConfigKey);
+ this.configurer = new ConfigRetriever(keySet, subscriberFactory::getSubscriber);
+ }
+
+ public Container(SubscriberFactory subscriberFactory, String configId, ComponentDeconstructor componentDeconstructor) {
+ this(subscriberFactory, configId, componentDeconstructor, new Osgi() {
+ });
+ }
+
+ private void deconstructObsoleteComponents(ComponentGraph oldGraph, ComponentGraph newGraph) {
+ IdentityHashMap<Object, Object> oldComponents = new IdentityHashMap<>();
+ oldGraph.allComponentsAndProviders().forEach(c -> oldComponents.put(c, null));
+ newGraph.allComponentsAndProviders().forEach(oldComponents::remove);
+ oldComponents.keySet().forEach(componentDeconstructor::deconstruct);
+ }
+
+ public ComponentGraph getNewComponentGraph(ComponentGraph oldGraph, Injector fallbackInjector /* = Guice.createInjector() */,
+ boolean restartOnRedeploy /* = false */) {
+
+ try {
+ ComponentGraph newGraph = getConfigAndCreateGraph(oldGraph, fallbackInjector, restartOnRedeploy);
+ newGraph.reuseNodes(oldGraph);
+ constructComponents(newGraph);
+ deconstructObsoleteComponents(oldGraph, newGraph);
+ return newGraph;
+ } catch (Throwable t) {
+ // TODO: Wrap ComponentConstructorException in an Error when generation==0 (+ unit test that Error is thrown)
+ invalidateGeneration(oldGraph.generation(), t);
+ throw t;
+ }
+ }
+
+ public ComponentGraph getNewComponentGraph(ComponentGraph oldGraph) {
+ return getNewComponentGraph(oldGraph, Guice.createInjector(), false);
+ }
+
+ public ComponentGraph getNewComponentGraph() {
+ return getNewComponentGraph(new ComponentGraph(), Guice.createInjector(), false);
+ }
+
+ private static String newGraphErrorMessage(long generation, Throwable cause, Duration maxWaitToExit) {
+ String failedFirstMessage = "Failed to set up first component graph";
+ String failedNewMessage = "Failed to set up new component graph";
+ String constructMessage = " due to error when constructing one of the components";
+ String exitMessage = ". Exiting within " + maxWaitToExit.toString();
+ String retainMessage = ". Retaining previous component generation.";
+
+ if (generation == 0) {
+ if (cause instanceof ComponentNode.ComponentConstructorException) {
+ return failedFirstMessage + constructMessage + exitMessage;
+ } else {
+ return failedFirstMessage + exitMessage;
+ }
+ } else {
+ if (cause instanceof ComponentNode.ComponentConstructorException) {
+ return failedNewMessage + constructMessage + retainMessage;
+ } else {
+ return failedNewMessage + retainMessage;
+ }
+ }
+ }
+
+ private void invalidateGeneration(long generation, Throwable cause) {
+ Duration maxWaitToExit = Duration.ofSeconds(60);
+ leastGeneration = Math.max(configurer.getComponentsGeneration(), configurer.getBootstrapGeneration()) + 1;
+ if (!(cause instanceof InterruptedException) && !(cause instanceof ConfigInterruptedException)) {
+ log.log(Level.WARNING, newGraphErrorMessage(generation, cause, maxWaitToExit), cause);
+ }
+ }
+
+ public ComponentGraph getConfigAndCreateGraph(ComponentGraph graph /* =new ComponentGraph*/, Injector fallbackInjector,
+ boolean restartOnRedeploy) {
+
+ ConfigSnapshot snapshot;
+
+ while (true) {
+ snapshot = configurer.getConfigs(graph.configKeys(), leastGeneration, restartOnRedeploy);
+
+ log.log(DEBUG, String.format("createNewGraph:\n" + "graph.configKeys = %s\n" + "graph.generation = %s\n" + "snapshot = %s\n",
+ graph.configKeys(), graph.generation(), snapshot));
+
+ if (snapshot instanceof BootstrapConfigs) {
+ // TODO: remove require when proven unnecessary
+ if (getBootstrapGeneration() <= previousConfigGeneration) {
+ throw new IllegalStateException(String.format(
+ "Got bootstrap configs out of sequence for old config generation %d.\n" + "Previous config generation is %d",
+ getBootstrapGeneration(), previousConfigGeneration));
+ }
+ log.log(DEBUG,
+ String.format(
+ "Got new bootstrap generation\n" + "bootstrap generation = %d\n" + "components generation: %d\n"
+ + "previous generation: %d\n",
+ getBootstrapGeneration(), getComponentsGeneration(), previousConfigGeneration));
+ installBundles(snapshot.configs());
+ graph = createComponentsGraph(snapshot.configs(), getBootstrapGeneration(), fallbackInjector);
+ // Continues loop
+
+ } else if (snapshot instanceof ConfigRetriever.ComponentsConfigs) {
+ break;
+ }
+ }
+ log.log(DEBUG,
+ String.format(
+ "Got components configs,\n" + "bootstrap generation = %d\n" + "components generation: %d\n"
+ + "previous generation: %d",
+ getBootstrapGeneration(), getComponentsGeneration(), previousConfigGeneration));
+ return createAndConfigureComponentsGraph(snapshot.configs(), fallbackInjector);
+ }
+
+ private long getBootstrapGeneration() {
+ return configurer.getBootstrapGeneration();
+ }
+
+ private long getComponentsGeneration() {
+ return configurer.getComponentsGeneration();
+ }
+
+ private ComponentGraph createAndConfigureComponentsGraph(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> componentsConfigs,
+ Injector fallbackInjector) {
+ ComponentGraph componentGraph = createComponentsGraph(componentsConfigs, getComponentsGeneration(), fallbackInjector);
+ componentGraph.setAvailableConfigs(componentsConfigs);
+ return componentGraph;
+ }
+
+ private void injectNodes(ComponentsConfig config, ComponentGraph graph) {
+ for (ComponentsConfig.Components component : config.components()) {
+ Node componentNode = ComponentGraph.getNode(graph, component.id());
+
+ for (ComponentsConfig.Components.Inject inject : component.inject()) {
+ //TODO: Support inject.name()
+ componentNode.inject(ComponentGraph.getNode(graph, inject.id()));
+ }
+ }
+ }
+
+ public void installBundles(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configsIncludingBootstrapConfigs) {
+ BundlesConfig bundlesConfig = getConfig(bundlesConfigKey, configsIncludingBootstrapConfigs);
+ osgi.useBundles(bundlesConfig.bundle());
+ }
+
+ private ComponentGraph createComponentsGraph(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configsIncludingBootstrapConfigs,
+ long generation, Injector fallbackInjector) {
+
+ previousConfigGeneration = generation;
+
+ ComponentGraph graph = new ComponentGraph(generation);
+
+ ComponentsConfig componentsConfig = getConfig(componentsConfigKey, configsIncludingBootstrapConfigs);
+ if (componentsConfig == null) {
+ throw new ConfigurationRuntimeException("The set of all configs does not include a valid 'components' config. Config set: "
+ + configsIncludingBootstrapConfigs.keySet());
+ }
+ addNodes(componentsConfig, graph);
+ injectNodes(componentsConfig, graph);
+
+ graph.complete(fallbackInjector);
+ return graph;
+ }
+
+ private void addNodes(ComponentsConfig componentsConfig, ComponentGraph graph) {
+
+ for (ComponentsConfig.Components config : componentsConfig.components()) {
+ BundleInstantiationSpecification specification = bundleInstatiationSpecification(config);
+ Class<?> componentClass = osgi.resolveClass(specification);
+ Node componentNode;
+
+ if (RestApiContext.class.isAssignableFrom(componentClass)) {
+ Class<? extends RestApiContext> nodeClass = componentClass.asSubclass(RestApiContext.class);
+ componentNode = new JerseyNode(specification.id, config.configId(), nodeClass, osgi);
+ } else {
+ componentNode = new ComponentNode(specification.id, config.configId(), componentClass, null);
+ }
+ graph.add(componentNode);
+ }
+ }
+
+ private void constructComponents(ComponentGraph graph) {
+ graph.nodes().forEach(Node::newOrCachedInstance);
+ }
+
+ public void shutdown(ComponentGraph graph, ComponentDeconstructor deconstructor) {
+ shutdownConfigurer();
+ if (graph != null) {
+ deconstructAllComponents(graph, deconstructor);
+ }
+ }
+
+ public void shutdownConfigurer() {
+ configurer.shutdown();
+ }
+
+ // Reload config manually, when subscribing to non-configserver sources
+ public void reloadConfig(long generation) {
+ subscriberFactory.reloadActiveSubscribers(generation);
+ }
+
+ private void deconstructAllComponents(ComponentGraph graph, ComponentDeconstructor deconstructor) {
+ graph.allComponentsAndProviders().forEach(deconstructor::deconstruct);
+ }
+
+ private static final Logger log = Logger.getLogger(Container.class.getName());
+
+ public static <T extends ConfigInstance> T getConfig(ConfigKey<T> key,
+ Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs) {
+ ConfigInstance inst = configs.get(key);
+
+ if (inst == null || key.getConfigClass() == null) {
+ throw new RuntimeException("Missing config " + key);
+ }
+
+ return key.getConfigClass().cast(inst);
+ }
+
+ public static BundleInstantiationSpecification bundleInstatiationSpecification(ComponentsConfig.Components config) {
+ return BundleInstantiationSpecification.getFromStrings(config.id(), config.classId(), config.bundle());
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/Osgi.java b/container-di/src/main/java/com/yahoo/container/di/Osgi.java
new file mode 100644
index 00000000000..7095180dfc5
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/Osgi.java
@@ -0,0 +1,43 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.config.FileReference;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.container.bundle.MockBundle;
+import com.yahoo.container.di.osgi.BundleClasses;
+import org.osgi.framework.Bundle;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author gjoranv
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public interface Osgi {
+ default BundleClasses getBundleClasses(ComponentSpecification bundle, Set<String> packagesToScan) {
+ return new BundleClasses(new MockBundle(), Collections.emptySet());
+ }
+
+ default void useBundles(Collection<FileReference> bundles) {
+ System.out.println("useBundles " + bundles.stream().map(Object::toString).collect(Collectors.joining(", ")));
+ }
+
+ default Class<?> resolveClass(BundleInstantiationSpecification spec) {
+ System.out.println("resolving class " + spec.classId);
+ try {
+ return Class.forName(spec.classId.getName());
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ default Bundle getBundle(ComponentSpecification spec) {
+ System.out.println("resolving bundle " + spec);
+ return new MockBundle();
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentGraph.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentGraph.java
new file mode 100644
index 00000000000..463de0c089a
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentGraph.java
@@ -0,0 +1,407 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.inject.BindingAnnotation;
+import com.google.inject.ConfigurationException;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.yahoo.collections.Pair;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.container.di.componentgraph.Provider;
+import com.yahoo.log.LogLevel;
+import com.yahoo.vespa.config.ConfigKey;
+import net.jcip.annotations.NotThreadSafe;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.lang.reflect.TypeVariable;
+import java.lang.reflect.WildcardType;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import static com.yahoo.container.di.componentgraph.core.Exceptions.removeStackTrace;
+
+/**
+ * @author Tony Vaagenes
+ * @author gjoranv
+ * @author ollivir
+ */
+@NotThreadSafe
+public class ComponentGraph {
+ private static final Logger log = Logger.getLogger(ComponentGraph.class.getName());
+
+ private long generation;
+ private Map<ComponentId, Node> nodesById = new HashMap<>();
+
+ public ComponentGraph(long generation) {
+ this.generation = generation;
+ }
+
+ public ComponentGraph() {
+ this(0L);
+ }
+
+ public long generation() {
+ return generation;
+ }
+
+ public int size() {
+ return nodesById.size();
+ }
+
+ public Collection<Node> nodes() {
+ return nodesById.values();
+ }
+
+ public void add(Node component) {
+ if (nodesById.containsKey(component.componentId())) {
+ throw new IllegalStateException("Multiple components with the same id " + component.componentId());
+ }
+ nodesById.put(component.componentId(), component);
+ }
+
+ private Optional<Node> lookupGlobalComponent(Key<?> key) {
+ if (!(key.getTypeLiteral().getType() instanceof Class)) {
+
+ throw new RuntimeException("Type not supported " + key.getTypeLiteral());
+ }
+ Class<?> clazz = key.getTypeLiteral().getRawType();
+
+ Collection<ComponentNode> components = matchingComponentNodes(nodes(), key);
+ if (components.isEmpty()) {
+ return Optional.empty();
+ } else if (components.size() == 1) {
+ return Optional.ofNullable(Iterables.get(components, 0));
+ } else {
+
+ List<Node> nonProviderComponents = components.stream().filter(c -> !Provider.class.isAssignableFrom(c.instanceType()))
+ .collect(Collectors.toList());
+ if (nonProviderComponents.isEmpty()) {
+ throw new IllegalStateException("Multiple global component providers for class '" + clazz.getName() + "' found");
+ } else if (nonProviderComponents.size() == 1) {
+ return Optional.of(nonProviderComponents.get(0));
+ } else {
+ throw new IllegalStateException("Multiple global components with class '" + clazz.getName() + "' found");
+ }
+ }
+ }
+
+ public <T> T getInstance(Class<T> clazz) {
+ return getInstance(Key.get(clazz));
+ }
+
+ @SuppressWarnings("unchecked")
+ public <T> T getInstance(Key<T> key) {
+ // TODO: Combine exception handling with lookupGlobalComponent.
+ Object ob = lookupGlobalComponent(key).map(Node::newOrCachedInstance)
+ .orElseThrow(() -> new IllegalStateException(String.format("No global component with key '%s' ", key)));
+ return (T) ob;
+ }
+
+ private Collection<ComponentNode> componentNodes() {
+ return nodesOfType(nodes(), ComponentNode.class);
+ }
+
+ private Collection<ComponentRegistryNode> componentRegistryNodes() {
+ return nodesOfType(nodes(), ComponentRegistryNode.class);
+ }
+
+ private Collection<ComponentNode> osgiComponentsOfClass(Class<?> clazz) {
+ return componentNodes().stream().filter(node -> clazz.isAssignableFrom(node.componentType())).collect(Collectors.toList());
+ }
+
+ public List<Node> complete(Injector fallbackInjector) {
+ componentNodes().forEach(node -> completeNode(node, fallbackInjector));
+ componentRegistryNodes().forEach(this::completeComponentRegistryNode);
+ return topologicalSort(nodes());
+ }
+
+ public List<Node> complete() {
+ return complete(Guice.createInjector());
+ }
+
+ public Set<ConfigKey<? extends ConfigInstance>> configKeys() {
+ return nodes().stream().flatMap(node -> node.configKeys().stream()).collect(Collectors.toSet());
+ }
+
+ public void setAvailableConfigs(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs) {
+ Map<ConfigKey<ConfigInstance>, ConfigInstance> invariantMap = Keys.invariantCopy(configs);
+ componentNodes().forEach(node -> node.setAvailableConfigs(invariantMap));
+ }
+
+ public void reuseNodes(ComponentGraph old) {
+ // copy instances if node equal
+ Set<ComponentId> commonComponentIds = Sets.intersection(nodesById.keySet(), old.nodesById.keySet());
+ for (ComponentId id : commonComponentIds) {
+ if (nodesById.get(id).equals(old.nodesById.get(id))) {
+ nodesById.get(id).instance = old.nodesById.get(id).instance;
+ }
+ }
+
+ // reset instances with modified dependencies
+ for (Node node : topologicalSort(nodes())) {
+ for (Node usedComponent : node.usedComponents()) {
+ if (!usedComponent.instance.isPresent()) {
+ node.instance = Optional.empty();
+ }
+ }
+ }
+ }
+
+ public Collection<?> allComponentsAndProviders() {
+ return nodes().stream().map(node -> node.instance().get()).collect(Collectors.toList());
+ }
+
+ private void completeComponentRegistryNode(ComponentRegistryNode registry) {
+ registry.injectAll(osgiComponentsOfClass(registry.componentClass()));
+ }
+
+ private void completeNode(ComponentNode node, Injector fallbackInjector) {
+ try {
+ Object[] arguments = node.getAnnotatedConstructorParams().stream().map(param -> handleParameter(node, fallbackInjector, param))
+ .toArray();
+
+ node.setArguments(arguments);
+ } catch (Exception e) {
+ throw removeStackTrace(new RuntimeException("When resolving dependencies of " + node.idAndType(), e));
+ }
+ }
+
+ private Object handleParameter(Node node, Injector fallbackInjector, Pair<Type, List<Annotation>> annotatedParameterType) {
+ Type parameterType = annotatedParameterType.getFirst();
+ List<Annotation> annotations = annotatedParameterType.getSecond();
+
+ if (parameterType instanceof Class && parameterType.equals(ComponentId.class)) {
+ return node.componentId();
+ } else if (parameterType instanceof Class && ConfigInstance.class.isAssignableFrom((Class<?>) parameterType)) {
+ return handleConfigParameter((ComponentNode) node, (Class<?>) parameterType);
+ } else if (parameterType instanceof ParameterizedType
+ && ((ParameterizedType) parameterType).getRawType().equals(ComponentRegistry.class)) {
+ ParameterizedType registry = (ParameterizedType) parameterType;
+ return getComponentRegistry(registry.getActualTypeArguments()[0]);
+ } else if (parameterType instanceof Class) {
+ return handleComponentParameter(node, fallbackInjector, (Class<?>) parameterType, annotations);
+ } else if (parameterType instanceof ParameterizedType) {
+ throw new RuntimeException("Injection of parameterized type " + parameterType + " is not supported.");
+ } else {
+ throw new RuntimeException("Injection of type " + parameterType + " is not supported");
+ }
+ }
+
+ private ComponentRegistryNode newComponentRegistryNode(Class<?> componentClass) {
+ ComponentRegistryNode registry = new ComponentRegistryNode(componentClass);
+ add(registry); //TODO: don't mutate nodes here.
+ return registry;
+ }
+
+ private ComponentRegistryNode getComponentRegistry(Type componentType) {
+ Class<?> componentClass;
+ if (componentType instanceof WildcardType) {
+ WildcardType wildcardType = (WildcardType) componentType;
+ if (wildcardType.getLowerBounds().length > 0 || wildcardType.getUpperBounds().length > 1) {
+ throw new RuntimeException("Can't create ComponentRegistry of unknown wildcard type" + wildcardType);
+ }
+ componentClass = (Class<?>) wildcardType.getUpperBounds()[0];
+ } else if (componentType instanceof Class) {
+ componentClass = (Class<?>) componentType;
+ } else if (componentType instanceof TypeVariable) {
+ throw new RuntimeException("Can't create ComponentRegistry of unknown type variable " + componentType);
+ } else {
+ throw new RuntimeException("Can't create ComponentRegistry of unknown type " + componentType);
+ }
+
+ for (ComponentRegistryNode node : componentRegistryNodes()) {
+ if (node.componentClass().equals(componentType)) {
+ return node;
+ }
+ }
+ return newComponentRegistryNode(componentClass);
+ }
+
+ @SuppressWarnings("unchecked")
+ private ConfigKey<ConfigInstance> handleConfigParameter(ComponentNode node, Class<?> clazz) {
+ Class<ConfigInstance> castClass = (Class<ConfigInstance>) clazz;
+ return new ConfigKey<>(castClass, node.configId());
+ }
+
+ private <T> Key<T> getKey(Class<T> clazz, Optional<Annotation> bindingAnnotation) {
+ return bindingAnnotation.map(annotation -> Key.get(clazz, annotation)).orElseGet(() -> Key.get(clazz));
+ }
+
+ private Optional<GuiceNode> matchingGuiceNode(Key<?> key, Object instance) {
+ return matchingNodes(nodes(), GuiceNode.class, key).stream().filter(node -> node.newOrCachedInstance() == instance). // TODO: assert that there is only one (after filter)
+ findFirst();
+ }
+
+ private Node lookupOrCreateGlobalComponent(Node node, Injector fallbackInjector, Class<?> clazz, Key<?> key) {
+ Optional<Node> component = lookupGlobalComponent(key);
+ if (!component.isPresent()) {
+ Object instance;
+ try {
+ log.log(LogLevel.DEBUG, "Trying the fallback injector to create" + messageForNoGlobalComponent(clazz, node));
+ instance = fallbackInjector.getInstance(key);
+ } catch (ConfigurationException e) {
+ throw removeStackTrace(new IllegalStateException(
+ (messageForMultipleClassLoaders(clazz).isEmpty()) ? "No global" + messageForNoGlobalComponent(clazz, node)
+ : messageForMultipleClassLoaders(clazz)));
+ }
+ component = Optional.of(matchingGuiceNode(key, instance).orElseGet(() -> {
+ GuiceNode guiceNode = new GuiceNode(instance, key.getAnnotation());
+ add(guiceNode);
+ return guiceNode;
+ }));
+ }
+ return component.get();
+ }
+
+ private Node handleComponentParameter(Node node, Injector fallbackInjector, Class<?> clazz, Collection<Annotation> annotations) {
+
+ List<Annotation> bindingAnnotations = annotations.stream().filter(ComponentGraph::isBindingAnnotation).collect(Collectors.toList());
+ Key<?> key = getKey(clazz, bindingAnnotations.stream().findFirst());
+
+ if (bindingAnnotations.size() > 1) {
+ throw new RuntimeException(String.format("More than one binding annotation used in class '%s'", node.instanceType()));
+ }
+
+ Collection<ComponentNode> injectedNodesOfCorrectType = matchingComponentNodes(node.componentsToInject, key);
+ if (injectedNodesOfCorrectType.size() == 0) {
+ return lookupOrCreateGlobalComponent(node, fallbackInjector, clazz, key);
+ } else if (injectedNodesOfCorrectType.size() == 1) {
+ return Iterables.get(injectedNodesOfCorrectType, 0);
+ } else {
+ //TODO: !className for last parameter
+ throw new RuntimeException(
+ String.format("Multiple components of type '%s' injected into component '%s'", clazz.getName(), node.instanceType()));
+ }
+ }
+
+ private static String messageForNoGlobalComponent(Class<?> clazz, Node node) {
+ return String.format(" component of class %s to inject into component %s.", clazz.getName(), node.idAndType());
+ }
+
+ private String messageForMultipleClassLoaders(Class<?> clazz) {
+ String errMsg = "Class " + clazz.getName() + " is provided by the framework, and cannot be embedded in a user bundle. "
+ + "To resolve this problem, please refer to osgi-classloading.html#multiple-implementations in the documentation";
+
+ try {
+ Class<?> resolvedClass = Class.forName(clazz.getName(), false, this.getClass().getClassLoader());
+ if (!resolvedClass.equals(clazz)) {
+ return errMsg;
+ }
+ } catch (ClassNotFoundException ignored) {
+
+ }
+ return "";
+ }
+
+ public static Node getNode(ComponentGraph graph, String componentId) {
+ return graph.nodesById.get(new ComponentId(componentId));
+ }
+
+ private static <T> Collection<T> nodesOfType(Collection<Node> nodes, Class<T> clazz) {
+ List<T> ret = new ArrayList<>();
+ for (Node node : nodes) {
+ if (clazz.isInstance(node)) {
+ ret.add(clazz.cast(node));
+ }
+ }
+ return ret;
+ }
+
+ private static Collection<ComponentNode> matchingComponentNodes(Collection<Node> nodes, Key<?> key) {
+ return matchingNodes(nodes, ComponentNode.class, key);
+ }
+
+ // Finds all nodes with a given nodeType and instance with given key
+ private static <T extends Node> Collection<T> matchingNodes(Collection<Node> nodes, Class<T> nodeType, Key<?> key) {
+ Class<?> clazz = key.getTypeLiteral().getRawType();
+ Annotation annotation = key.getAnnotation();
+
+ List<T> filteredByClass = nodesOfType(nodes, nodeType).stream().filter(node -> clazz.isAssignableFrom(node.componentType()))
+ .collect(Collectors.toList());
+
+ if (filteredByClass.size() == 1) {
+ return filteredByClass;
+ } else {
+ List<T> filteredByClassAndAnnotation = filteredByClass.stream()
+ .filter(node -> (annotation == null && node.instanceKey().getAnnotation() == null)
+ || annotation.equals(node.instanceKey().getAnnotation()))
+ .collect(Collectors.toList());
+ if (filteredByClassAndAnnotation.size() > 0) {
+ return filteredByClassAndAnnotation;
+ } else {
+ return filteredByClass;
+ }
+ }
+ }
+
+ // Returns true if annotation is a BindingAnnotation, e.g. com.google.inject.name.Named
+ public static boolean isBindingAnnotation(Annotation annotation) {
+ LinkedList<Class<?>> queue = new LinkedList<>();
+ queue.add(annotation.getClass());
+ queue.addAll(Arrays.asList(annotation.getClass().getInterfaces()));
+
+ while (!queue.isEmpty()) {
+ Class<?> clazz = queue.removeFirst();
+ if (clazz.getAnnotation(BindingAnnotation.class) != null) {
+ return true;
+ } else {
+ if (clazz.getSuperclass() != null) {
+ queue.addFirst(clazz.getSuperclass());
+ }
+ }
+ }
+ return false;
+ }
+
+ /**
+ * The returned list is the nodes from the graph bottom-up.
+ *
+ * @return A list where a earlier than b in the list implies that there is no path from a to b
+ */
+ private static List<Node> topologicalSort(Collection<Node> nodes) {
+ Map<ComponentId, Integer> numIncoming = new HashMap<>();
+
+ nodes.forEach(
+ node -> node.usedComponents().forEach(injectedNode -> numIncoming.merge(injectedNode.componentId(), 1, (a, b) -> a + b)));
+ LinkedList<Node> sorted = new LinkedList<>();
+ List<Node> unsorted = new ArrayList<>(nodes);
+
+ while (!unsorted.isEmpty()) {
+ List<Node> ready = new ArrayList<>();
+ List<Node> notReady = new ArrayList<>();
+ unsorted.forEach(node -> {
+ if (numIncoming.getOrDefault(node.componentId(), 0) == 0) {
+ ready.add(node);
+ } else {
+ notReady.add(node);
+ }
+ });
+
+ if (ready.isEmpty()) {
+ throw new IllegalStateException("There is a cycle in the component injection graph.");
+ }
+
+ ready.forEach(node -> node.usedComponents()
+ .forEach(injectedNode -> numIncoming.merge(injectedNode.componentId(), -1, (a, b) -> a + b)));
+ sorted.addAll(0, ready);
+ unsorted = notReady;
+ }
+ return sorted;
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentNode.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentNode.java
new file mode 100644
index 00000000000..27298ce6c82
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentNode.java
@@ -0,0 +1,304 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core;
+
+import com.google.inject.Inject;
+import com.google.inject.Key;
+import com.yahoo.collections.Pair;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.container.di.componentgraph.Provider;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Modifier;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Comparator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import static com.yahoo.container.di.componentgraph.core.Exceptions.cutStackTraceAtConstructor;
+import static com.yahoo.container.di.componentgraph.core.Exceptions.removeStackTrace;
+import static com.yahoo.container.di.componentgraph.core.Keys.createKey;
+
+/**
+ * @author Tony Vaagenes
+ * @author gjoranv
+ * @author ollivir
+ */
+public class ComponentNode extends Node {
+ private static final Logger log = Logger.getLogger(ComponentNode.class.getName());
+
+ private final Class<?> clazz;
+ private final Annotation key;
+ private Object[] arguments = null;
+ private final String configId;
+
+ private final Constructor<?> constructor;
+
+ private Map<ConfigKey<ConfigInstance>, ConfigInstance> availableConfigs = null;
+
+
+ public ComponentNode(ComponentId componentId,
+ String configId,
+ Class<?> clazz, Annotation XXX_key) // TODO expose key, not javaAnnotation
+ {
+ super(componentId);
+ if (isAbstract(clazz)) {
+ throw new IllegalArgumentException("Can't instantiate abstract class " + clazz.getName());
+ }
+ this.configId = configId;
+ this.clazz = clazz;
+ this.key = XXX_key;
+ this.constructor = bestConstructor(clazz);
+ }
+
+ public ComponentNode(ComponentId componentId, String configId, Class<?> clazz) {
+ this(componentId, configId, clazz, null);
+ }
+
+ public String configId() {
+ return configId;
+ }
+
+ @Override
+ public Key<?> instanceKey() {
+ return createKey(clazz, key);
+ }
+
+ @Override
+ public Class<?> instanceType() {
+ return clazz;
+ }
+
+ @Override
+ public List<Node> usedComponents() {
+ if (arguments == null) {
+ throw new IllegalStateException("Arguments must be set first.");
+ }
+ List<Node> ret = new ArrayList<>();
+ for (Object arg : arguments) {
+ if (arg instanceof Node) {
+ ret.add((Node) arg);
+ }
+ }
+ return ret;
+ }
+
+ private static List<Class<?>> allSuperClasses(Class<?> clazz) {
+ List<Class<?>> ret = new ArrayList<>();
+ while (clazz != null) {
+ ret.add(clazz);
+ clazz = clazz.getSuperclass();
+ }
+ return ret;
+ }
+
+ @Override
+ public Class<?> componentType() {
+ if (Provider.class.isAssignableFrom(clazz)) {
+ //TODO: Test what happens if you ask for something that isn't a class, e.g. a parameterized type.
+
+ List<Type> allGenericInterfaces = allSuperClasses(clazz).stream().flatMap(c -> Arrays.stream(c.getGenericInterfaces())).collect(Collectors.toList());
+ for (Type t : allGenericInterfaces) {
+ if (t instanceof ParameterizedType && ((ParameterizedType) t).getRawType().equals(Provider.class)) {
+ Type[] typeArgs = ((ParameterizedType) t).getActualTypeArguments();
+ if (typeArgs != null && typeArgs.length > 0) {
+ return (Class<?>) typeArgs[0];
+ }
+ }
+ }
+ throw new IllegalStateException("Component type cannot be resolved");
+ } else {
+ return clazz;
+ }
+ }
+
+ public void setArguments(Object[] arguments) {
+ this.arguments = arguments;
+ }
+
+ @Override
+ protected Object newInstance() {
+ if (arguments == null) {
+ throw new IllegalStateException("graph.complete must be called before retrieving instances.");
+ }
+
+ List<Object> actualArguments = new ArrayList<>();
+ for (Object ob : arguments) {
+ if (ob instanceof Node) {
+ actualArguments.add(((Node) ob).newOrCachedInstance());
+ } else if (ob instanceof ConfigKey) {
+ actualArguments.add(availableConfigs.get(ob));
+ } else {
+ actualArguments.add(ob);
+ }
+ }
+
+ Object instance;
+ try {
+ instance = constructor.newInstance(actualArguments.toArray());
+ } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) {
+ StackTraceElement dependencyInjectorMarker = new StackTraceElement("============= Dependency Injection =============", "newInstance", null, -1);
+
+ throw removeStackTrace(new ComponentConstructorException("Error constructing " + idAndType(), cutStackTraceAtConstructor(e.getCause(), dependencyInjectorMarker)));
+ }
+
+ return initId(instance);
+ }
+
+ private Object initId(Object component) {
+ if (component instanceof AbstractComponent) {
+ AbstractComponent abstractComponent = (AbstractComponent) component;
+ if (abstractComponent.hasInitializedId() && !abstractComponent.getId().equals(componentId())) {
+ throw new IllegalStateException(
+ "Component with id '" + componentId() + "' is trying to set its component id explicitly: '" + abstractComponent.getId() + "'. " +
+ "This is not allowed, so please remove any call to super() in your component's constructor.");
+ }
+ abstractComponent.initId(componentId());
+ }
+ return component;
+ }
+
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + Arrays.hashCode(arguments);
+ result = prime * result + ((availableConfigs == null) ? 0 : availableConfigs.hashCode());
+ result = prime * result + ((configId == null) ? 0 : configId.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ComponentNode) {
+ ComponentNode that = (ComponentNode) other;
+ return super.equals(that) && equalEdges(Arrays.asList(this.arguments), Arrays.asList(that.arguments)) && this.usedConfigs().equals(that.usedConfigs());
+ } else {
+ return false;
+ }
+ }
+
+ private List<ConfigInstance> usedConfigs() {
+ if (availableConfigs == null) {
+ throw new IllegalStateException("setAvailableConfigs must be called!");
+ }
+ List<ConfigInstance> ret = new ArrayList<>();
+ for (Object arg : arguments) {
+ if (arg instanceof ConfigKey) {
+ ret.add(availableConfigs.get(arg));
+ }
+ }
+ return ret;
+ }
+
+ protected List<Pair<Type, List<Annotation>>> getAnnotatedConstructorParams() {
+ Type[] types = constructor.getGenericParameterTypes();
+ Annotation[][] annotations = constructor.getParameterAnnotations();
+
+ List<Pair<Type, List<Annotation>>> ret = new ArrayList<>();
+
+ for (int i = 0; i < types.length; i++) {
+ ret.add(new Pair<>(types[i], Arrays.asList(annotations[i])));
+ }
+ return ret;
+ }
+
+ public void setAvailableConfigs(Map<ConfigKey<ConfigInstance>, ConfigInstance> configs) {
+ if (arguments == null) {
+ throw new IllegalStateException("graph.complete must be called before graph.setAvailableConfigs.");
+ }
+ this.availableConfigs = configs;
+ }
+
+ @Override
+ public Set<ConfigKey<ConfigInstance>> configKeys() {
+ return configParameterClasses().stream().map(par -> new ConfigKey<>(par, configId)).collect(Collectors.toSet());
+ }
+
+ @SuppressWarnings("unchecked")
+ private List<Class<ConfigInstance>> configParameterClasses() {
+ List<Class<ConfigInstance>> ret = new ArrayList<>();
+ for (Type type : constructor.getGenericParameterTypes()) {
+ if (type instanceof Class && ConfigInstance.class.isAssignableFrom((Class<?>) type)) {
+ ret.add((Class<ConfigInstance>) type);
+ }
+ }
+ return ret;
+ }
+
+ @Override
+ public String label() {
+ LinkedList<String> configNames = configKeys().stream().map(k -> k.getName() + ".def").collect(Collectors.toCollection(LinkedList::new));
+
+ configNames.addFirst(instanceType().getSimpleName());
+ configNames.addFirst(Node.packageName(instanceType()));
+
+ return "{" + String.join("|", configNames) + "}";
+ }
+
+ private static Constructor<?> bestConstructor(Class<?> clazz) {
+ Constructor<?>[] publicConstructors = clazz.getConstructors();
+
+ Constructor<?> annotated = null;
+ for (Constructor<?> ctor : publicConstructors) {
+ Annotation annotation = ctor.getAnnotation(Inject.class);
+ if (annotation != null) {
+ if (annotated == null) {
+ annotated = ctor;
+ } else {
+ throw componentConstructorException("Multiple constructor annotated with @Inject in class " + clazz.getName());
+ }
+ }
+ }
+ if (annotated != null) {
+ return annotated;
+ }
+
+ if (publicConstructors.length == 0) {
+ throw componentConstructorException("No public constructors in class " + clazz.getName());
+ } else if (publicConstructors.length == 1) {
+ return publicConstructors[0];
+ } else {
+ log.warning(String.format("Multiple public constructors found in class %s, there should only be one. "
+ + "If more than one public constructor is needed, the primary one must be annotated with @Inject.", clazz.getName()));
+ List<Pair<Constructor<?>, Integer>> withParameterCount = new ArrayList<>();
+ for (Constructor<?> ctor : publicConstructors) {
+ long count = Arrays.stream(ctor.getParameterTypes()).filter(ConfigInstance.class::isAssignableFrom).count();
+ withParameterCount.add(new Pair<>(ctor, (int) count));
+ }
+ withParameterCount.sort(Comparator.comparingInt(Pair::getSecond));
+ return withParameterCount.get(withParameterCount.size() - 1).getFirst();
+ }
+ }
+
+ private static ComponentConstructorException componentConstructorException(String message) {
+ return removeStackTrace(new ComponentConstructorException(message));
+ }
+
+ public static class ComponentConstructorException extends RuntimeException {
+ ComponentConstructorException(String message) {
+ super(message);
+ }
+
+ ComponentConstructorException(String message, Throwable cause) {
+ super(message, cause);
+ }
+ }
+
+
+ private static boolean isAbstract(Class<?> clazz) {
+ return Modifier.isAbstract(clazz.getModifiers());
+ }
+} \ No newline at end of file
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.java
new file mode 100644
index 00000000000..8af1713c84f
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.java
@@ -0,0 +1,105 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core;
+
+import com.google.inject.Key;
+import com.google.inject.util.Types;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+/**
+ * @author Tony Vaagenes
+ * @author gjoranv
+ * @author ollivir
+ */
+public class ComponentRegistryNode extends Node {
+ private static ComponentId componentRegistryNamespace = ComponentId.fromString("ComponentRegistry");
+
+ private final Class<?> componentClass;
+
+ public ComponentRegistryNode(Class<?> componentClass) {
+ super(componentId(componentClass));
+ this.componentClass = componentClass;
+ }
+
+ @Override
+ public List<Node> usedComponents() {
+ return componentsToInject;
+ }
+
+ @Override
+ protected Object newInstance() {
+ ComponentRegistry<Object> registry = new ComponentRegistry<>();
+ componentsToInject.forEach(component -> registry.register(component.componentId(), component.newOrCachedInstance()));
+
+ return registry;
+ }
+
+ @Override
+ public Key<?> instanceKey() {
+ return Key.get(Types.newParameterizedType(ComponentRegistry.class, componentClass));
+ }
+
+ @Override
+ public Class<?> instanceType() {
+ return instanceKey().getTypeLiteral().getRawType();
+ }
+
+ @Override
+ public Class<?> componentType() {
+ return instanceType();
+ }
+
+ public Class<?> componentClass() {
+ return componentClass;
+ }
+
+ @Override
+ public Set<ConfigKey<ConfigInstance>> configKeys() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = super.hashCode();
+ result = prime * result + ((componentClass == null) ? 0 : componentClass.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof ComponentRegistryNode) {
+ ComponentRegistryNode that = (ComponentRegistryNode) other;
+ return this.componentId().equals(that.componentId()) && this.instanceType().equals(that.instanceType())
+ && equalNodeEdges(this.usedComponents(), that.usedComponents());
+ } else {
+ return false;
+ }
+ }
+
+ @Override
+ public String label() {
+ return String.format("{ComponentRegistry\\<%s\\>|%s}", componentClass.getSimpleName(), Node.packageName(componentClass));
+ }
+
+ private static ComponentId componentId(Class<?> componentClass) {
+ return syntheticComponentId(componentClass.getName(), componentClass, componentRegistryNamespace);
+ }
+
+ public static boolean equalNodeEdges(List<Node> edges, List<Node> otherEdges) {
+ if (edges.size() == otherEdges.size()) {
+ List<ComponentId> left = edges.stream().map(Node::componentId).sorted().collect(Collectors.toList());
+ List<ComponentId> right = otherEdges.stream().map(Node::componentId).sorted().collect(Collectors.toList());
+ return left.equals(right);
+ } else {
+ return false;
+ }
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Exceptions.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Exceptions.java
new file mode 100644
index 00000000000..d84d771fef6
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Exceptions.java
@@ -0,0 +1,45 @@
+package com.yahoo.container.di.componentgraph.core;
+
+import java.util.Arrays;
+
+class Exceptions {
+ static <E extends Throwable> E removeStackTrace(E exception) {
+ if (preserveStackTrace()) {
+ return exception;
+ } else {
+ exception.setStackTrace(new StackTraceElement[0]);
+ return exception;
+ }
+ }
+
+ static boolean preserveStackTrace() {
+ String preserve = System.getProperty("jdisc.container.preserveStackTrace");
+ return (preserve != null && !preserve.isEmpty());
+ }
+
+ static Throwable cutStackTraceAtConstructor(Throwable throwable, StackTraceElement marker) {
+ if (throwable != null && !preserveStackTrace()) {
+ StackTraceElement[] stackTrace = throwable.getStackTrace();
+ int upTo = stackTrace.length - 1;
+
+ // take until ComponentNode is reached
+ while (upTo >= 0 && !stackTrace[upTo].getClassName().equals(ComponentNode.class.getName())) {
+ upTo--;
+ }
+
+ // then drop until <init> is reached
+ while (upTo >= 0 && !stackTrace[upTo].getMethodName().equals("<init>")) {
+ upTo--;
+ }
+ if (upTo < 0) {
+ throwable.setStackTrace(new StackTraceElement[0]);
+ } else {
+ throwable.setStackTrace(Arrays.copyOfRange(stackTrace, 0, upTo));
+ }
+
+ cutStackTraceAtConstructor(throwable.getCause(), marker);
+ }
+ return throwable;
+ }
+
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/GuiceNode.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/GuiceNode.java
new file mode 100644
index 00000000000..61d0d9bba8d
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/GuiceNode.java
@@ -0,0 +1,78 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core;
+
+import com.google.inject.Key;
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.lang.annotation.Annotation;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+import static com.yahoo.container.di.componentgraph.core.Keys.createKey;
+
+/**
+ * @author Tony Vaagenes
+ * @author gjoranv
+ * @author ollivir
+ */
+public final class GuiceNode extends Node {
+ private static final ComponentId guiceNamespace = ComponentId.fromString("Guice");
+
+ private final Object myInstance;
+ private final Annotation annotation;
+
+ public GuiceNode(Object myInstance,
+ Annotation annotation) {
+ super(componentId(myInstance));
+ this.myInstance = myInstance;
+ this.annotation = annotation;
+ }
+
+ @Override
+ public Set<ConfigKey<ConfigInstance>> configKeys() {
+ return Collections.emptySet();
+ }
+
+ @Override
+ public Key<?> instanceKey() {
+ return createKey(myInstance.getClass(), annotation);
+ }
+
+ @Override
+ public Class<?> instanceType() {
+ return myInstance.getClass();
+ }
+
+ @Override
+ public Class<?> componentType() {
+ return instanceType();
+ }
+
+
+ @Override
+ public List<Node> usedComponents() {
+ return Collections.emptyList();
+ }
+
+ @Override
+ protected Object newInstance() {
+ return myInstance;
+ }
+
+ @Override
+ public void inject(Node component) {
+ throw new UnsupportedOperationException("Illegal to inject components to a GuiceNode!");
+ }
+
+ @Override
+ public String label() {
+ return String.format("{{%s|Guice}|%s}", instanceType().getSimpleName(), Node.packageName(instanceType()));
+ }
+
+ private static ComponentId componentId(Object instance) {
+ return Node.syntheticComponentId(instance.getClass().getName(), instance, guiceNamespace);
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/JerseyNode.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/JerseyNode.java
new file mode 100644
index 00000000000..79b849bff8f
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/JerseyNode.java
@@ -0,0 +1,92 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.container.di.Osgi;
+import com.yahoo.container.di.config.JerseyBundlesConfig;
+import com.yahoo.container.di.config.RestApiContext;
+import com.yahoo.container.di.config.RestApiContext.BundleInfo;
+import com.yahoo.container.di.osgi.BundleClasses;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.wiring.BundleWiring;
+
+import java.net.URL;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+
+/**
+ * Represents an instance of RestApiContext
+ *
+ * @author gjoranv
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class JerseyNode extends ComponentNode {
+ private static final String WEB_INF_URL = "WebInfUrl";
+
+ private final Osgi osgi;
+
+ public JerseyNode(ComponentId componentId, String configId, Class<?> clazz, Osgi osgi) {
+ super(componentId, configId, clazz, null);
+ this.osgi = osgi;
+ }
+
+ @Override
+ protected RestApiContext newInstance() {
+ Object instance = super.newInstance();
+ RestApiContext restApiContext = (RestApiContext) instance;
+
+ List<JerseyBundlesConfig.Bundles> bundles = restApiContext.bundlesConfig.bundles();
+ for (JerseyBundlesConfig.Bundles bundleConfig : bundles) {
+ BundleClasses bundleClasses = osgi.getBundleClasses(ComponentSpecification.fromString(bundleConfig.spec()),
+ new HashSet<>(bundleConfig.packages()));
+
+ restApiContext.addBundle(createBundleInfo(bundleClasses.bundle(), bundleClasses.classEntries()));
+ }
+
+ componentsToInject.forEach(component -> restApiContext.addInjectableComponent(component.instanceKey(), component.componentId(),
+ component.newOrCachedInstance()));
+
+ return restApiContext;
+ }
+
+ @Override
+ public int hashCode() {
+ return super.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ return super.equals(other)
+ && (other instanceof JerseyNode && this.componentsToInject.equals(((JerseyNode) other).componentsToInject));
+ }
+
+ public static BundleInfo createBundleInfo(Bundle bundle, Collection<String> classEntries) {
+ BundleInfo bundleInfo = new BundleInfo(bundle.getSymbolicName(), bundle.getVersion(), bundle.getLocation(), webInfUrl(bundle),
+ bundle.adapt(BundleWiring.class).getClassLoader());
+
+ bundleInfo.setClassEntries(classEntries);
+ return bundleInfo;
+ }
+
+ public static Bundle getBundle(Osgi osgi, String bundleSpec) {
+ Bundle bundle = osgi.getBundle(ComponentSpecification.fromString(bundleSpec));
+ if (bundle == null) {
+ throw new IllegalArgumentException("Bundle not found: " + bundleSpec);
+ }
+ return bundle;
+ }
+
+ private static URL webInfUrl(Bundle bundle) {
+ String webInfUrlHeader = bundle.getHeaders().get(WEB_INF_URL);
+
+ if (webInfUrlHeader == null) {
+ return null;
+ } else {
+ return bundle.getEntry(webInfUrlHeader);
+ }
+ }
+
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Keys.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Keys.java
new file mode 100644
index 00000000000..005691721c4
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Keys.java
@@ -0,0 +1,37 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core;
+
+import com.google.inject.Key;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.Type;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author ollivir
+ */
+public class Keys {
+ static Key<?> createKey(Type instanceType, Annotation annotation) {
+ if (annotation == null) {
+ return Key.get(instanceType);
+ } else {
+ return Key.get(instanceType, annotation);
+ }
+ }
+
+ @SuppressWarnings("unchecked")
+ public static Map<ConfigKey<ConfigInstance>, ConfigInstance> invariantCopy(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs) {
+ Map<ConfigKey<ConfigInstance>, ConfigInstance> ret = new HashMap<>();
+ configs.forEach((k, v) -> ret.put((ConfigKey<ConfigInstance>) k, v));
+ return ret;
+ }
+
+ public static Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> covariantCopy(Map<ConfigKey<ConfigInstance>, ConfigInstance> configs) {
+ Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> ret = new HashMap<>();
+ configs.forEach((k, v) -> ret.put(k, v));
+ return ret;
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Node.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Node.java
new file mode 100644
index 00000000000..6feac7a4078
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Node.java
@@ -0,0 +1,164 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core;
+
+import com.google.inject.Key;
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.container.di.componentgraph.Provider;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Optional;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import static com.yahoo.log.LogLevel.DEBUG;
+import static com.yahoo.log.LogLevel.SPAM;
+
+/**
+ * @author Tony Vaagenes
+ * @author gjoranv
+ * @author ollivir
+ */
+public abstract class Node {
+ private final static Logger log = Logger.getLogger(Node.class.getName());
+
+ private final ComponentId componentId;
+ protected Optional<Object> instance = Optional.empty();
+ List<Node> componentsToInject = new ArrayList<>();
+
+ public Node(ComponentId componentId) {
+ this.componentId = componentId;
+ }
+
+ public abstract Key<?> instanceKey();
+
+ /**
+ * The components actually used by this node. Consist of a subset of the injected nodes + subset of the global nodes.
+ */
+ public abstract List<Node> usedComponents();
+
+ protected abstract Object newInstance();
+
+ public Object newOrCachedInstance() {
+ Object inst;
+ if (instance.isPresent()) {
+ inst = instance.get();
+ log.log(SPAM, "Reusing instance for component with ID " + componentId);
+ } else {
+ log.log(DEBUG, "Creating new instance for component with ID " + componentId);
+ inst = newInstance();
+ instance = Optional.of(inst);
+ }
+ return component(inst);
+ }
+
+ private Object component(Object instance) {
+ if (instance instanceof Provider) {
+ Provider<?> provider = (Provider<?>) instance;
+ return provider.get();
+ } else {
+ return instance;
+ }
+ }
+
+ public abstract Set<ConfigKey<ConfigInstance>> configKeys();
+
+ public void inject(Node component) {
+ componentsToInject.add(component);
+ }
+
+ public void injectAll(Collection<ComponentNode> componentNodes) {
+ componentNodes.forEach(this::inject);
+ }
+
+ public abstract Class<?> instanceType();
+
+ public abstract Class<?> componentType();
+
+ public abstract String label();
+
+ public String idAndType() {
+ String className = instanceType().getName();
+
+ if (className.equals(componentId.getName())) {
+ return "'" + componentId + "'";
+ } else {
+ return "'" + componentId + "' of type '" + className + "'";
+ }
+ }
+
+ private static boolean equalNodes(Object a, Object b) {
+ if (a instanceof Node && b instanceof Node) {
+ Node l = (Node) a;
+ Node r = (Node) b;
+ return l.componentId.equals(r.componentId);
+ } else {
+ return a.equals(b);
+ }
+ }
+
+ public static boolean equalEdges(List<?> edges1, List<?> edges2) {
+ Iterator<?> right = edges2.iterator();
+ for (Object l : edges1) {
+ if (!right.hasNext()) {
+ return false;
+ }
+ Object r = right.next();
+ if (!equalNodes(l, r)) {
+ return false;
+ }
+ }
+ return !right.hasNext();
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((componentId == null) ? 0 : componentId.hashCode());
+ result = prime * result + ((componentsToInject == null) ? 0 : componentsToInject.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object other) {
+ if (other instanceof Node) {
+ Node that = (Node) other;
+ return getClass().equals(that.getClass()) && this.componentId.equals(that.componentId)
+ && this.instanceType().equals(that.instanceType()) && equalEdges(this.usedComponents(), that.usedComponents());
+ } else {
+ return false;
+ }
+ }
+
+ public ComponentId componentId() {
+ return componentId;
+ }
+
+ public Optional<?> instance() {
+ return instance;
+ }
+
+ /**
+ * @param identityObject
+ * The identifying object that makes the Node unique
+ */
+ protected static ComponentId syntheticComponentId(String className, Object identityObject, ComponentId namespace) {
+ String name = className + "_" + System.identityHashCode(identityObject);
+ return ComponentId.fromString(name).nestInNamespace(namespace);
+ }
+
+ public static String packageName(Class<?> componentClass) {
+ String fullClassName = componentClass.getName();
+ int index = fullClassName.lastIndexOf('.');
+ if (index < 0) {
+ return "";
+ } else {
+ return fullClassName.substring(0, index);
+ }
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java b/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java
index 7b5f85778c6..bfb9a8f9160 100644
--- a/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java
+++ b/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java
@@ -1,4 +1,4 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
package com.yahoo.container.di.config;
import com.google.common.collect.ImmutableSet;
@@ -11,8 +11,6 @@ import java.net.URL;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
-import java.util.Iterator;
-import java.util.LinkedHashSet;
import java.util.List;
import java.util.Set;
diff --git a/container-di/src/main/java/com/yahoo/container/di/osgi/BundleClasses.java b/container-di/src/main/java/com/yahoo/container/di/osgi/BundleClasses.java
new file mode 100644
index 00000000000..bca3ed73d0b
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/osgi/BundleClasses.java
@@ -0,0 +1,27 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.osgi;
+
+import org.osgi.framework.Bundle;
+
+import java.util.Collection;
+
+/**
+ * @author ollivir
+ */
+public class BundleClasses {
+ private final Bundle bundle;
+ private final Collection<String> classEntries;
+
+ public BundleClasses(Bundle bundle, Collection<String> classEntries) {
+ this.bundle = bundle;
+ this.classEntries = classEntries;
+ }
+
+ public Bundle bundle() {
+ return bundle;
+ }
+
+ public Collection<String> classEntries() {
+ return classEntries;
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/osgi/OsgiUtil.java b/container-di/src/main/java/com/yahoo/container/di/osgi/OsgiUtil.java
new file mode 100644
index 00000000000..e1854155e5b
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/osgi/OsgiUtil.java
@@ -0,0 +1,168 @@
+// Copyright 2018 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.osgi;
+
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.osgi.maven.ProjectBundleClassPaths;
+import com.yahoo.osgi.maven.ProjectBundleClassPaths.BundleClasspathMapping;
+import org.osgi.framework.Bundle;
+import org.osgi.framework.wiring.BundleWiring;
+
+import java.io.File;
+import java.io.IOException;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.function.Predicate;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+
+import static com.google.common.io.Files.fileTreeTraverser;
+
+/**
+ * Tested by com.yahoo.application.container.jersey.JerseyTest
+ *
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class OsgiUtil {
+ private static final Logger log = Logger.getLogger(OsgiUtil.class.getName());
+ private static final String CLASS_FILE_TYPE_SUFFIX = ".class";
+
+ public static Collection<String> getClassEntriesInBundleClassPath(Bundle bundle, Set<String> packagesToScan) {
+ BundleWiring bundleWiring = bundle.adapt(BundleWiring.class);
+
+ if (packagesToScan.isEmpty()) {
+ return bundleWiring.listResources("/", "*" + CLASS_FILE_TYPE_SUFFIX,
+ BundleWiring.LISTRESOURCES_LOCAL | BundleWiring.LISTRESOURCES_RECURSE);
+ } else {
+ List<String> ret = new ArrayList<>();
+ for (String pkg : packagesToScan) {
+ ret.addAll(bundleWiring.listResources(packageToPath(pkg), "*" + CLASS_FILE_TYPE_SUFFIX, BundleWiring.LISTRESOURCES_LOCAL));
+ }
+ return ret;
+ }
+ }
+
+ public static Collection<String> getClassEntriesForBundleUsingProjectClassPathMappings(ClassLoader classLoader,
+ ComponentSpecification bundleSpec, Set<String> packagesToScan) {
+ return classEntriesFrom(bundleClassPathMapping(bundleSpec, classLoader).classPathElements, packagesToScan);
+ }
+
+ private static BundleClasspathMapping bundleClassPathMapping(ComponentSpecification bundleSpec, ClassLoader classLoader) {
+ ProjectBundleClassPaths projectBundleClassPaths = loadProjectBundleClassPaths(classLoader);
+
+ if (projectBundleClassPaths.mainBundle.bundleSymbolicName.equals(bundleSpec.getName())) {
+ return projectBundleClassPaths.mainBundle;
+ } else {
+ log.log(Level.WARNING,
+ "Dependencies of the bundle " + bundleSpec + " will not be scanned. Please file a feature request if you need this");
+ return matchingBundleClassPathMapping(bundleSpec, projectBundleClassPaths.providedDependencies);
+ }
+ }
+
+ public static BundleClasspathMapping matchingBundleClassPathMapping(ComponentSpecification bundleSpec,
+ Collection<BundleClasspathMapping> providedBundlesClassPathMappings) {
+ for (BundleClasspathMapping mapping : providedBundlesClassPathMappings) {
+ if (mapping.bundleSymbolicName.equals(bundleSpec.getName())) {
+ return mapping;
+ }
+ }
+ throw new RuntimeException("No such bundle: " + bundleSpec);
+ }
+
+ private static ProjectBundleClassPaths loadProjectBundleClassPaths(ClassLoader classLoader) {
+ URL classPathMappingsFileLocation = classLoader.getResource(ProjectBundleClassPaths.CLASSPATH_MAPPINGS_FILENAME);
+ if (classPathMappingsFileLocation == null) {
+ throw new RuntimeException("Couldn't find " + ProjectBundleClassPaths.CLASSPATH_MAPPINGS_FILENAME + " in the class path.");
+ }
+
+ try {
+ return ProjectBundleClassPaths.load(Paths.get(classPathMappingsFileLocation.toURI()));
+ } catch (IOException | URISyntaxException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static Collection<String> classEntriesFrom(List<String> classPathEntries, Set<String> packagesToScan) {
+ Set<String> packagePathsToScan = packagesToScan.stream().map(OsgiUtil::packageToPath).collect(Collectors.toSet());
+ List<String> ret = new ArrayList<>();
+
+ for (String entry : classPathEntries) {
+ Path path = Paths.get(entry);
+ if (Files.isDirectory(path)) {
+ ret.addAll(classEntriesInPath(path, packagePathsToScan));
+ } else if (Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")) {
+ ret.addAll(classEntriesInJar(path, packagePathsToScan));
+ } else {
+ throw new RuntimeException("Unsupported path " + path + " in the class path");
+ }
+ }
+ return ret;
+ }
+
+ private static String relativePathToClass(Path rootPath, Path pathToClass) {
+ Path relativePath = rootPath.relativize(pathToClass);
+ return relativePath.toString();
+ }
+
+ private static Collection<String> classEntriesInPath(Path rootPath, Collection<String> packagePathsToScan) {
+ Iterable<File> fileIterator;
+ if (packagePathsToScan.isEmpty()) {
+ fileIterator = fileTreeTraverser().preOrderTraversal(rootPath.toFile());
+ } else {
+ List<File> files = new ArrayList<>();
+ for (String packagePath : packagePathsToScan) {
+ for (File file : fileTreeTraverser().children(rootPath.resolve(packagePath).toFile())) {
+ files.add(file);
+ }
+ }
+ fileIterator = files;
+ }
+
+ List<String> ret = new ArrayList<>();
+ for (File file : fileIterator) {
+ if (file.isFile() && file.getName().endsWith(CLASS_FILE_TYPE_SUFFIX)) {
+ ret.add(relativePathToClass(rootPath, file.toPath()));
+ }
+ }
+ return ret;
+ }
+
+ private static String packagePath(String name) {
+ int index = name.lastIndexOf('/');
+ if (index < 0) {
+ return name;
+ } else {
+ return name.substring(0, index);
+ }
+ }
+
+ private static Collection<String> classEntriesInJar(Path jarPath, Set<String> packagePathsToScan) {
+ Predicate<String> acceptedPackage;
+ if (packagePathsToScan.isEmpty()) {
+ acceptedPackage = ign -> true;
+ } else {
+ acceptedPackage = name -> packagePathsToScan.contains(packagePath(name));
+ }
+
+ try (JarFile jarFile = new JarFile(jarPath.toFile())) {
+ return jarFile.stream().map(JarEntry::getName).filter(name -> name.endsWith(CLASS_FILE_TYPE_SUFFIX)).filter(acceptedPackage)
+ .collect(Collectors.toList());
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static String packageToPath(String packageName) {
+ return packageName.replace('.', '/');
+ }
+}