summaryrefslogtreecommitdiffstats
path: root/container-di
diff options
context:
space:
mode:
authorOlli Virtanen <olli.virtanen@oath.com>2018-06-21 16:48:59 +0200
committerOlli Virtanen <olli.virtanen@oath.com>2018-06-21 16:48:59 +0200
commita9f527be723d026ae21ed9504c55553bd5beadbb (patch)
tree1323e8bcc5a8f6698618d806f35a70a047cc656e /container-di
parent5258489bf992e8176e136362759ac079494b6f94 (diff)
Scala code in container-di converted to Java
Diffstat (limited to 'container-di')
-rw-r--r--container-di/pom.xml46
-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
-rw-r--r--container-di/src/main/scala/com/yahoo/container/bundle/MockBundle.scala94
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/CloudSubscriberFactory.scala104
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/ConfigRetriever.scala135
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/Container.scala265
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/Osgi.scala40
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentGraph.scala334
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentNode.scala213
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.scala71
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/GuiceNode.scala41
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/JerseyNode.scala92
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/Node.scala117
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/osgi/OsgiUtil.scala143
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/package.scala44
-rw-r--r--container-di/src/test/java/com/yahoo/container/di/ConfigRetrieverTest.java137
-rw-r--r--container-di/src/test/java/com/yahoo/container/di/ContainerTest.java378
-rw-r--r--container-di/src/test/java/com/yahoo/container/di/ContainerTestBase.java120
-rw-r--r--container-di/src/test/java/com/yahoo/container/di/DirConfigSource.java69
-rw-r--r--container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java674
-rw-r--r--container-di/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java68
-rw-r--r--container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.java254
-rw-r--r--container-di/src/test/java/demo/Base.java4
-rw-r--r--container-di/src/test/java/demo/ContainerTestBase.java71
-rw-r--r--container-di/src/test/java/demo/DeconstructTest.java1
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala107
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala398
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala50
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala540
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.scala66
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.scala249
46 files changed, 4064 insertions, 3226 deletions
diff --git a/container-di/pom.xml b/container-di/pom.xml
index 97db8bc9b42..96ceedad352 100644
--- a/container-di/pom.xml
+++ b/container-di/pom.xml
@@ -51,15 +51,6 @@
<scope>provided</scope>
</dependency>
<dependency>
- <groupId>org.scala-lang</groupId>
- <artifactId>scala-library</artifactId>
- </dependency>
- <dependency>
- <groupId>org.scalatest</groupId>
- <artifactId>scalatest_${scala.major-version}</artifactId>
- <scope>test</scope>
- </dependency>
- <dependency>
<groupId>com.yahoo.vespa</groupId>
<artifactId>vespajlib</artifactId>
<version>${project.version}</version>
@@ -122,45 +113,8 @@
</executions>
</plugin>
<plugin>
- <groupId>net.alchim31.maven</groupId>
- <artifactId>scala-maven-plugin</artifactId>
- <executions>
- <execution>
- <id>compile</id>
- <goals>
- <goal>compile</goal>
- </goals>
- <phase>compile</phase>
- </execution>
- <execution>
- <id>test-compile</id>
- <goals>
- <goal>testCompile</goal>
- </goals>
- <phase>test-compile</phase>
- </execution>
- <execution>
- <phase>process-resources</phase>
- <goals>
- <goal>compile</goal>
- </goals>
- </execution>
- <execution>
- <phase>process-test-resources</phase>
- <id>early-test-compile</id>
- <goals>
- <goal>testCompile</goal>
- </goals>
- </execution>
- </executions>
- </plugin>
- <plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-javadoc-plugin</artifactId>
- <configuration>
- <!-- Exclude package with known scala-java interaction issue. -->
- <excludePackageNames>com.yahoo.container.di</excludePackageNames>
- </configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
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('.', '/');
+ }
+}
diff --git a/container-di/src/main/scala/com/yahoo/container/bundle/MockBundle.scala b/container-di/src/main/scala/com/yahoo/container/bundle/MockBundle.scala
deleted file mode 100644
index 5a28d9abe2a..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/bundle/MockBundle.scala
+++ /dev/null
@@ -1,94 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.container.bundle
-
-
-import java.net.URL
-import java.util
-
-import org.osgi.framework.wiring._
-import org.osgi.framework.{ServiceReference, Version, Bundle}
-import java.io.InputStream
-import java.util.{Collections, Hashtable, Dictionary}
-import MockBundle._
-import org.osgi.resource.{Capability, Wire, Requirement}
-
-/**
- * @author gjoranv
- */
-class MockBundle extends Bundle with BundleWiring {
-
- override def getState = Bundle.ACTIVE
-
- override def start(options: Int) {}
- override def start() {}
- override def stop(options: Int) {}
- override def stop() {}
- override def update(input: InputStream) {}
- override def update() {}
- override def uninstall() {}
-
- override def getHeaders(locale: String) = getHeaders
-
- override def getSymbolicName = SymbolicName
- override def getVersion: Version = BundleVersion
- override def getLocation = getSymbolicName
- override def getBundleId: Long = 0L
-
- override def getHeaders: Dictionary[String, String] = new Hashtable[String, String]()
-
- override def getRegisteredServices = Array[ServiceReference[_]]()
- override def getServicesInUse = getRegisteredServices
-
- override def hasPermission(permission: Any) = true
-
- override def getResource(name: String) = throw new UnsupportedOperationException
- override def loadClass(name: String) = throw new UnsupportedOperationException
- override def getResources(name: String) = throw new UnsupportedOperationException
-
- override def getEntryPaths(path: String) = throw new UnsupportedOperationException
- override def getEntry(path: String) = throw new UnsupportedOperationException
- override def findEntries(path: String, filePattern: String, recurse: Boolean) = Collections.emptyEnumeration()
-
-
- override def getLastModified = 1L
-
- override def getBundleContext = throw new UnsupportedOperationException
- override def getSignerCertificates(signersType: Int) = Collections.emptyMap()
-
- override def adapt[A](`type`: Class[A]) =
- `type` match {
- case MockBundle.bundleWiringClass => this.asInstanceOf[A]
- case _ => ???
- }
-
- override def getDataFile(filename: String) = null
- override def compareTo(o: Bundle) = getBundleId compareTo o.getBundleId
-
-
- //TODO: replace with mockito
- override def findEntries(p1: String, p2: String, p3: Int): util.List[URL] = ???
- override def getRequiredResourceWires(p1: String): util.List[Wire] = ???
- override def getResourceCapabilities(p1: String): util.List[Capability] = ???
- override def isCurrent: Boolean = ???
- override def getRequiredWires(p1: String): util.List[BundleWire] = ???
- override def getCapabilities(p1: String): util.List[BundleCapability] = ???
- override def getProvidedResourceWires(p1: String): util.List[Wire] = ???
- override def getProvidedWires(p1: String): util.List[BundleWire] = ???
- override def getRevision: BundleRevision = ???
- override def getResourceRequirements(p1: String): util.List[Requirement] = ???
- override def isInUse: Boolean = ???
- override def listResources(p1: String, p2: String, p3: Int): util.Collection[String] = Collections.emptyList()
- override def getClassLoader: ClassLoader = MockBundle.getClass.getClassLoader
- override def getRequirements(p1: String): util.List[BundleRequirement] = ???
- override def getResource: BundleRevision = ???
- override def getBundle: Bundle = ???
-}
-
-object MockBundle {
- val SymbolicName = "mock-bundle"
- val BundleVersion = new Version(1, 0, 0)
-
- val bundleWiringClass = classOf[BundleWiring]
-
-
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/CloudSubscriberFactory.scala b/container-di/src/main/scala/com/yahoo/container/di/CloudSubscriberFactory.scala
deleted file mode 100644
index 0f3fab93e80..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/CloudSubscriberFactory.scala
+++ /dev/null
@@ -1,104 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.container.di
-
-import java.util.logging.{Level, Logger}
-
-import com.yahoo.config.ConfigInstance
-import com.yahoo.config.subscription.{ConfigHandle, ConfigSource, ConfigSourceSet, ConfigSubscriber}
-import com.yahoo.container.di.CloudSubscriberFactory._
-import com.yahoo.container.di.config.{Subscriber, SubscriberFactory}
-import com.yahoo.vespa.config.ConfigKey
-
-import scala.collection.JavaConverters._
-import scala.language.existentials
-
-
-/**
- * @author Tony Vaagenes
- */
-class CloudSubscriberFactory(configSource: ConfigSource) extends SubscriberFactory {
-
- private var testGeneration: Option[Long] = None
-
- private val activeSubscribers = new java.util.WeakHashMap[CloudSubscriber, Int]()
-
- override def getSubscriber(configKeys: java.util.Set[_ <: ConfigKey[_]]): Subscriber = {
- val subscriber = new CloudSubscriber(configKeys.asScala.toSet.asInstanceOf[Set[ConfigKeyT]], configSource)
-
- testGeneration.foreach(subscriber.subscriber.reload(_)) //TODO: test specific code, remove
- activeSubscribers.put(subscriber, 0)
-
- subscriber
- }
-
- //TODO: test specific code, remove
- override def reloadActiveSubscribers(generation: Long) {
- testGeneration = Some(generation)
-
- val l = activeSubscribers.keySet().asScala.toSet
- l.foreach { _.subscriber.reload(generation) }
- }
-}
-
-object CloudSubscriberFactory {
- val log = Logger.getLogger(classOf[CloudSubscriberFactory].getName)
-
- private class CloudSubscriber(keys: Set[ConfigKeyT], configSource: ConfigSource) extends Subscriber
- {
- private[CloudSubscriberFactory] val subscriber = new ConfigSubscriber(configSource)
- private val handles: Map[ConfigKeyT, ConfigHandle[_ <: ConfigInstance]] = keys.map(subscribe).toMap
-
-
- // if waitNextGeneration has not yet been called, -1 should be returned
- var generation: Long = -1
-
- // True if this reconfiguration was caused by a system-internal redeploy, not an external application change
- var internalRedeploy: Boolean = false
-
- private def subscribe(key: ConfigKeyT) = (key, subscriber.subscribe(key.getConfigClass, key.getConfigId))
-
- override def configChanged = handles.values.exists(_.isChanged)
-
- //mapValues returns a view,, so we need to force evaluation of it here to prevent deferred evaluation.
- override def config = handles.mapValues(_.getConfig).toMap.view.force.
- asInstanceOf[Map[ConfigKey[ConfigInstance], ConfigInstance]].asJava
-
- override def waitNextGeneration() = {
- require(!handles.isEmpty)
-
- /* 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. */
- var gotNextGen = false
- var numExceptions = 0
- while (!gotNextGen) {
- try{
- if (subscriber.nextGeneration())
- gotNextGen = true
- } catch {
- case e: IllegalArgumentException =>
- numExceptions += 1
- 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
- generation
- }
-
- override def close() {
- subscriber.close()
- }
- }
-
-
- class Provider extends com.google.inject.Provider[SubscriberFactory] {
- override def get() = new CloudSubscriberFactory(ConfigSourceSet.createDefault())
- }
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/ConfigRetriever.scala b/container-di/src/main/scala/com/yahoo/container/di/ConfigRetriever.scala
deleted file mode 100644
index aad9e17acb2..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/ConfigRetriever.scala
+++ /dev/null
@@ -1,135 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.container.di
-
-
-import java.util.logging.{Level, Logger}
-
-import com.yahoo.config.ConfigInstance
-import com.yahoo.container.di.ConfigRetriever._
-import com.yahoo.container.di.config.Subscriber
-import com.yahoo.log.LogLevel.DEBUG
-
-import scala.annotation.tailrec
-import scala.collection.JavaConverters._
-import scala.language.postfixOps
-
-/**
- * @author Tony Vaagenes
- * @author gjoranv
- */
-final class ConfigRetriever(bootstrapKeys: Set[ConfigKeyT],
- subscribe: Set[ConfigKeyT] => Subscriber)
-{
- require(!bootstrapKeys.isEmpty)
-
- private val bootstrapSubscriber: Subscriber = subscribe(bootstrapKeys)
-
- private var componentSubscriber: Subscriber = subscribe(Set())
- private var componentSubscriberKeys: Set[ConfigKeyT] = Set()
-
- /** Loop forever until we get config */
- @tailrec
- final def getConfigs(componentConfigKeys: Set[ConfigKeyT], leastGeneration: Long, restartOnRedeploy: Boolean = false): ConfigSnapshot = {
- require(componentConfigKeys intersect bootstrapKeys isEmpty)
- log.log(DEBUG, "getConfigs: " + componentConfigKeys)
-
- setupComponentSubscriber(componentConfigKeys ++ bootstrapKeys)
-
- getConfigsOptional(leastGeneration, restartOnRedeploy) match {
- case Some(snapshot) => resetComponentSubscriberIfBootstrap(snapshot); snapshot
- case None => getConfigs(componentConfigKeys, leastGeneration, restartOnRedeploy)
- }
- }
-
-
- /** Try to get config just once */
- final def getConfigsOnce(componentConfigKeys: Set[ConfigKeyT], leastGeneration: Long, restartOnRedeploy: Boolean = false): Option[ConfigSnapshot] = {
- require(componentConfigKeys intersect bootstrapKeys isEmpty)
- log.log(DEBUG, "getConfigsOnce: " + componentConfigKeys)
-
- setupComponentSubscriber(componentConfigKeys ++ bootstrapKeys)
-
- getConfigsOptional(leastGeneration, restartOnRedeploy) match {
- case Some(snapshot) => resetComponentSubscriberIfBootstrap(snapshot); Some(snapshot)
- case None => None;
- }
- }
-
- private def getConfigsOptional(leastGeneration: Long, restartOnRedeploy: Boolean): Option[ConfigSnapshot] = {
- val newestComponentGeneration = componentSubscriber.waitNextGeneration()
- log.log(DEBUG, s"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) {
- None
- } else if (restartOnRedeploy && ! componentSubscriber.internalRedeploy()) { // Don't reconfig - wait for restart
- None
- } else if (bootstrapSubscriber.generation < newestComponentGeneration) {
- val newestBootstrapGeneration = bootstrapSubscriber.waitNextGeneration()
- log.log(DEBUG, s"getConfigsOptional: new bootstrap generation: ${bootstrapSubscriber.generation}")
- bootstrapConfigIfChanged() orElse {
- if (newestBootstrapGeneration == newestComponentGeneration){
- log.log(DEBUG, s"Got new components configs with unchanged bootstrap configs.")
- componentsConfigIfChanged()
- } else {
- // This should not be a normal case, and hence a warning to allow investigation.
- log.warning(s"Did not get same generation for bootstrap ($newestBootstrapGeneration) and components configs ($newestComponentGeneration).")
- None
- }
- }
- } else {
- // bootstrapGen==componentGen (happens only when a new component subscriber returns first config after bootstrap)
- componentsConfigIfChanged()
- }
- }
-
- private def bootstrapConfigIfChanged(): Option[BootstrapConfigs] = configIfChanged(bootstrapSubscriber, BootstrapConfigs)
- private def componentsConfigIfChanged(): Option[ComponentsConfigs] = configIfChanged(componentSubscriber, ComponentsConfigs)
-
- private def configIfChanged[T <: ConfigSnapshot](subscriber: Subscriber,
- constructor: Map[ConfigKeyT, ConfigInstance] => T ): Option[T] = {
- if (subscriber.configChanged) Some(constructor(subscriber.config.asScala.toMap))
- else None
- }
-
- private def resetComponentSubscriberIfBootstrap(snapshot: ConfigSnapshot) {
- snapshot match {
- case BootstrapConfigs(_) => setupComponentSubscriber(Set())
- case _ =>
- }
- }
-
- private def setupComponentSubscriber(keys: Set[ConfigKeyT]) {
- if (componentSubscriberKeys != keys) {
- componentSubscriber.close()
- componentSubscriberKeys = keys
- try {
- log.log(DEBUG, s"Setting up new component subscriber for keys: $keys")
- componentSubscriber = subscribe(keys)
- } catch {
- case e: Throwable =>
- log.log(Level.WARNING, s"Failed setting up subscriptions for component configs: ${e.getMessage}")
- log.log(Level.WARNING, s"Config keys: $keys")
- throw e
- }
- }
- }
-
- def shutdown() {
- bootstrapSubscriber.close()
- componentSubscriber.close()
- }
-
- //TODO: check if these are really needed
- final def getBootstrapGeneration = bootstrapSubscriber.generation
- final def getComponentsGeneration = componentSubscriber.generation
-}
-
-
-object ConfigRetriever {
- private val log = Logger.getLogger(classOf[ConfigRetriever].getName)
-
- sealed abstract class ConfigSnapshot
- case class BootstrapConfigs(configs: Map[ConfigKeyT, ConfigInstance]) extends ConfigSnapshot
- case class ComponentsConfigs(configs: Map[ConfigKeyT, ConfigInstance]) extends ConfigSnapshot
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/Container.scala b/container-di/src/main/scala/com/yahoo/container/di/Container.scala
deleted file mode 100644
index 2a185d41a6c..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/Container.scala
+++ /dev/null
@@ -1,265 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.container.di
-
-import java.util.logging.{Level, Logger}
-import java.util.{IdentityHashMap, Random}
-
-import com.google.inject.{Guice, Injector}
-import com.yahoo.config._
-import com.yahoo.config.subscription.ConfigInterruptedException
-import com.yahoo.container.bundle.BundleInstantiationSpecification
-import com.yahoo.container.di.ConfigRetriever.{BootstrapConfigs, ComponentsConfigs}
-import com.yahoo.container.di.Container._
-import com.yahoo.container.di.componentgraph.core.ComponentNode.ComponentConstructorException
-import com.yahoo.container.di.componentgraph.core.{ComponentGraph, ComponentNode, JerseyNode}
-import com.yahoo.container.di.config.{RestApiContext, SubscriberFactory}
-import com.yahoo.container.{BundlesConfig, ComponentsConfig}
-import com.yahoo.log.LogLevel.DEBUG
-import com.yahoo.protect.Process
-import com.yahoo.vespa.config.ConfigKey
-
-import scala.collection.JavaConverters._
-import scala.concurrent.duration._
-import scala.language.postfixOps
-import scala.math.max
-
-
-/**
- *
- * @author gjoranv
- * @author Tony Vaagenes
- */
-class Container(
- subscriberFactory: SubscriberFactory,
- configId: String,
- componentDeconstructor: ComponentDeconstructor,
- osgi: Osgi = new Osgi {}
- )
-{
- val bundlesConfigKey = new ConfigKey(classOf[BundlesConfig], configId)
- val componentsConfigKey = new ConfigKey(classOf[ComponentsConfig], configId)
-
- var configurer = new ConfigRetriever(Set(bundlesConfigKey, componentsConfigKey), (keys) => subscriberFactory.getSubscriber(keys.asJava))
- var previousConfigGeneration = -1L
- var leastGeneration = -1L
-
- @throws(classOf[InterruptedException])
- def getNewComponentGraph(oldGraph: ComponentGraph = new ComponentGraph,
- fallbackInjector: GuiceInjector = Guice.createInjector(),
- restartOnRedeploy: Boolean = false): ComponentGraph = {
-
- def deconstructObsoleteComponents(oldGraph: ComponentGraph, newGraph: ComponentGraph) {
- val oldComponents = new IdentityHashMap[AnyRef, AnyRef]()
- oldGraph.allComponentsAndProviders foreach (oldComponents.put(_, null))
- newGraph.allComponentsAndProviders foreach (oldComponents.remove(_))
- oldComponents.keySet.asScala foreach (componentDeconstructor.deconstruct(_))
- }
-
- try {
- val newGraph = getConfigAndCreateGraph(oldGraph, fallbackInjector, restartOnRedeploy)
- newGraph.reuseNodes(oldGraph)
- constructComponents(newGraph)
- deconstructObsoleteComponents(oldGraph, newGraph)
- newGraph
- } catch {
- case userException: ComponentConstructorException =>
- invalidateGeneration(oldGraph.generation, userException)
- // TODO: Wrap userException in an Error when generation==0 (+ unit test that Error is thrown)
- throw userException
- case t: Throwable =>
- invalidateGeneration(oldGraph.generation, t)
- throw t
- }
- }
-
- private def invalidateGeneration(generation: Long, cause: Throwable) {
- val maxWaitToExit = 60 seconds
-
- def newGraphErrorMessage(generation: Long, cause: Throwable): String = {
- val failedFirstMessage = "Failed to set up first component graph"
- val failedNewMessage = "Failed to set up new component graph"
- val constructMessage = "due to error when constructing one of the components"
- val exitMessage = s"Exiting within $maxWaitToExit."
- val retainMessage = "Retaining previous component generation."
- generation match {
- case 0 =>
- cause match {
- case _: ComponentConstructorException => s"$failedFirstMessage $constructMessage. $exitMessage"
- case _ => s"$failedFirstMessage. $exitMessage"
- }
- case _ =>
- cause match {
- case _: ComponentConstructorException => s"$failedNewMessage $constructMessage. $retainMessage"
- case _ => s"$failedNewMessage. $retainMessage"
- }
- }
- }
-
- // TODO: move to ConfiguredApplication
- def logAndDie(message: String, cause: Throwable): Unit = {
- log.log(Level.SEVERE, message, cause)
- try {
- Thread.sleep((new Random(System.nanoTime).nextDouble * maxWaitToExit.toMillis).toLong)
- } catch {
- case _: InterruptedException => // Do nothing
- }
- Process.logAndDie("Exited for reason (repeated from above):", cause)
- }
-
- leastGeneration = max(configurer.getComponentsGeneration, configurer.getBootstrapGeneration) + 1
- cause match {
- case _: InterruptedException | _: ConfigInterruptedException => // Normal during shutdown, do not log anything.
- case _ => log.log(Level.WARNING, newGraphErrorMessage(generation, cause), cause)
- }
- }
-
- final def getConfigAndCreateGraph(graph: ComponentGraph = new ComponentGraph,
- fallbackInjector: Injector,
- restartOnRedeploy: Boolean): ComponentGraph = {
-
- val snapshot = configurer.getConfigs(graph.configKeys, leastGeneration, restartOnRedeploy)
-
- log.log(DEBUG,
- """createNewGraph:
- |graph.configKeys = %s
- |graph.generation = %s
- |snapshot = %s"""
- .format(graph.configKeys, graph.generation, snapshot).stripMargin)
-
- val preventTailRecursion =
- snapshot match {
- case BootstrapConfigs(configs) =>
- // TODO: remove require when proven unnecessary
- require(getBootstrapGeneration > previousConfigGeneration,
- """Got bootstrap configs out of sequence for old config generation %d.
- |Previous config generation is %d""".format(getBootstrapGeneration, previousConfigGeneration))
- log.log(DEBUG,
- """Got new bootstrap generation
- |bootstrap generation = %d
- |components generation: %d
- |previous generation: %d"""
- .format(getBootstrapGeneration, getComponentsGeneration, previousConfigGeneration).stripMargin)
- installBundles(configs)
- getConfigAndCreateGraph(
- createComponentsGraph(configs, getBootstrapGeneration,fallbackInjector), fallbackInjector, restartOnRedeploy)
- case ComponentsConfigs(configs) =>
- log.log(DEBUG,
- """Got components configs,
- |bootstrap generation = %d
- |components generation: %d
- |previous generation: %d"""
- .format(getBootstrapGeneration, getComponentsGeneration, previousConfigGeneration).stripMargin)
- createAndConfigureComponentsGraph(configs, fallbackInjector)
- }
-
- preventTailRecursion
- }
-
-
- def getBootstrapGeneration: Long = {
- configurer.getBootstrapGeneration
- }
-
- def getComponentsGeneration: Long = {
- configurer.getComponentsGeneration
- }
-
- private def createAndConfigureComponentsGraph[T](componentsConfigs: Map[ConfigKeyT, ConfigInstance],
- fallbackInjector: Injector): ComponentGraph = {
-
- val componentGraph = createComponentsGraph(componentsConfigs, getComponentsGeneration, fallbackInjector)
- componentGraph.setAvailableConfigs(componentsConfigs)
- componentGraph
- }
-
- def injectNodes(config: ComponentsConfig, graph: ComponentGraph) {
- for {
- component <- config.components().asScala
- inject <- component.inject().asScala
- } {
- def getNode = ComponentGraph.getNode(graph, _: String)
-
- //TODO: Support inject.name()
- getNode(component.id()).inject(getNode(inject.id()))
- }
-
- }
-
- def installBundles(configsIncludingBootstrapConfigs: Map[ConfigKeyT, ConfigInstance]) {
- val bundlesConfig = getConfig(bundlesConfigKey, configsIncludingBootstrapConfigs)
- osgi.useBundles(bundlesConfig.bundle())
- }
-
- private def createComponentsGraph[T](
- configsIncludingBootstrapConfigs: Map[ConfigKeyT, ConfigInstance],
- generation: Long,
- fallbackInjector: Injector): ComponentGraph = {
-
- previousConfigGeneration = generation
-
- val graph = new ComponentGraph(generation)
-
- val 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)
- graph
- }
-
- def addNodes[T](componentsConfig: ComponentsConfig, graph: ComponentGraph) {
- def isRestApiContext(clazz: Class[_]) = classOf[RestApiContext].isAssignableFrom(clazz)
- def asRestApiContext(clazz: Class[_]) = clazz.asInstanceOf[Class[RestApiContext]]
-
- for (config : ComponentsConfig.Components <- componentsConfig.components.asScala) {
- val specification = bundleInstatiationSpecification(config)
- val componentClass = osgi.resolveClass(specification)
-
- val componentNode =
- if (isRestApiContext(componentClass))
- new JerseyNode(specification.id, config.configId(), asRestApiContext(componentClass), osgi)
- else
- new ComponentNode(specification.id, config.configId(), componentClass)
-
- graph.add(componentNode)
- }
- }
-
- private def constructComponents(graph: ComponentGraph) {
- graph.nodes foreach (_.newOrCachedInstance())
- }
-
- def shutdown(graph: ComponentGraph, deconstructor: ComponentDeconstructor) {
- shutdownConfigurer()
- if (graph != null)
- deconstructAllComponents(graph, deconstructor)
- }
-
- def shutdownConfigurer() {
- configurer.shutdown()
- }
-
- // Reload config manually, when subscribing to non-configserver sources
- def reloadConfig(generation: Long) {
- subscriberFactory.reloadActiveSubscribers(generation)
- }
-
- def deconstructAllComponents(graph: ComponentGraph, deconstructor: ComponentDeconstructor) {
- graph.allComponentsAndProviders foreach(deconstructor.deconstruct(_))
- }
-
-}
-
-object Container {
- val log = Logger.getLogger(classOf[Container].getName)
-
- def getConfig[T <: ConfigInstance](key: ConfigKey[T], configs: Map[ConfigKeyT, ConfigInstance]) : T = {
- key.getConfigClass.cast(configs.getOrElse(key.asInstanceOf[ConfigKeyT], sys.error("Missing config " + key)))
- }
-
- def bundleInstatiationSpecification(config: ComponentsConfig.Components) =
- BundleInstantiationSpecification.getFromStrings(config.id(), config.classId(), config.bundle())
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/Osgi.scala b/container-di/src/main/scala/com/yahoo/container/di/Osgi.scala
deleted file mode 100644
index 3407eceae3e..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/Osgi.scala
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright 2017 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.FileReference
-import com.yahoo.container.bundle.{MockBundle, BundleInstantiationSpecification}
-import com.yahoo.container.di.Osgi.BundleClasses
-import org.osgi.framework.Bundle
-import com.yahoo.component.ComponentSpecification
-
-/**
- *
- * @author gjoranv
- * @author Tony Vaagenes
- */
-trait Osgi {
-
- def getBundleClasses(bundle: ComponentSpecification, packagesToScan: Set[String]): BundleClasses = {
- BundleClasses(new MockBundle, List())
- }
-
- def useBundles(bundles: java.util.Collection[FileReference]) {
- println("useBundles " + bundles.toArray.mkString(", "))
- }
-
- def resolveClass(spec: BundleInstantiationSpecification): Class[AnyRef] = {
- println("resolving class " + spec.classId)
- Class.forName(spec.classId.getName).asInstanceOf[Class[AnyRef]]
- }
-
- def getBundle(spec: ComponentSpecification): Bundle = {
- println("resolving bundle " + spec)
- new MockBundle()
- }
-
-}
-
-object Osgi {
- type RelativePath = String //e.g. "com/yahoo/MyClass.class"
- case class BundleClasses(bundle: Bundle, classEntries: Iterable[RelativePath])
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentGraph.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentGraph.scala
deleted file mode 100644
index 92f489798c9..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentGraph.scala
+++ /dev/null
@@ -1,334 +0,0 @@
-// Copyright 2017 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 java.util.logging.Logger
-
-import com.yahoo.component.provider.ComponentRegistry
-import com.yahoo.config.ConfigInstance
-
-import java.lang.annotation.{Annotation => JavaAnnotation}
-import java.lang.IllegalStateException
-import com.yahoo.log.LogLevel
-
-import collection.mutable
-import annotation.tailrec
-
-import com.yahoo.container.di.{ConfigKeyT, GuiceInjector}
-import com.yahoo.container.di.componentgraph.Provider
-import com.yahoo.vespa.config.ConfigKey
-import com.google.inject.{Guice, ConfigurationException, Key, BindingAnnotation}
-import net.jcip.annotations.NotThreadSafe
-
-import com.yahoo.component.{AbstractComponent, ComponentId}
-import java.lang.reflect.{TypeVariable, WildcardType, Method, ParameterizedType, Type}
-import com.yahoo.container.di.removeStackTrace
-import scala.util.Try
-import scala.Some
-
-import scala.language.existentials
-
-/**
- * @author Tony Vaagenes
- * @author gjoranv
- */
-@NotThreadSafe
-class ComponentGraph(val generation: Long = 0) {
-
- import ComponentGraph._
-
- private var nodesById = Map[ComponentId, Node]()
-
- private[di] def size = nodesById.size
-
- def nodes = nodesById.values
-
- def add(component: Node) {
- require(!nodesById.isDefinedAt(component.componentId), "Multiple components with the same id " + component.componentId)
- nodesById += component.componentId -> component
- }
-
- def lookupGlobalComponent(key: Key[_]): Option[Node] = {
- require(key.getTypeLiteral.getType.isInstanceOf[Class[_]], "Type not supported " + key.getTypeLiteral)
- val clazz = key.getTypeLiteral.getRawType
-
-
- val components = matchingComponentNodes(nodesById.values, key)
-
- def singleNonProviderComponentOrThrow: Option[Node] = {
- val nonProviderComponents = components filter (c => !classOf[Provider[_]].isAssignableFrom(c.instanceType))
- nonProviderComponents.size match {
- case 0 => throw new IllegalStateException(s"Multiple global component providers for class '${clazz.getName}' found")
- case 1 => Some(nonProviderComponents.head.asInstanceOf[Node])
- case _ => throw new IllegalStateException(s"Multiple global components with class '${clazz.getName}' found")
- }
- }
-
- components.size match {
- case 0 => None
- case 1 => Some(components.head.asInstanceOf[Node])
- case _ => singleNonProviderComponentOrThrow
- }
- }
-
- def getInstance[T](clazz: Class[T]) : T = {
- getInstance(Key.get(clazz))
- }
-
- def getInstance[T](key: Key[T]) : T = {
- lookupGlobalComponent(key).
- // TODO: Combine exception handling with lookupGlobalComponent.
- getOrElse(throw new IllegalStateException("No global component with key '%s' ".format(key.toString))).
- newOrCachedInstance().asInstanceOf[T]
- }
-
- private def componentNodes: Traversable[ComponentNode] =
- nodesOfType(nodesById.values, classOf[ComponentNode])
-
- private def componentRegistryNodes: Traversable[ComponentRegistryNode] =
- nodesOfType(nodesById.values, classOf[ComponentRegistryNode])
-
- private def osgiComponentsOfClass(clazz: Class[_]): Traversable[ComponentNode] = {
- componentNodes.filter(node => clazz.isAssignableFrom(node.componentType))
- }
-
- def complete(fallbackInjector: GuiceInjector = Guice.createInjector()) {
- def detectCycles = topologicalSort(nodesById.values.toList)
-
- componentNodes foreach {completeNode(_, fallbackInjector)}
- componentRegistryNodes foreach completeComponentRegistryNode
- detectCycles
- }
-
- def configKeys: Set[ConfigKeyT] = {
- nodesById.values.flatMap(_.configKeys).toSet
- }
-
- def setAvailableConfigs(configs: Map[ConfigKeyT, ConfigInstance]) {
- componentNodes foreach { _.setAvailableConfigs(configs) }
- }
-
- def reuseNodes(old: ComponentGraph) {
- def copyInstancesIfNodeEqual() {
- val commonComponentIds = nodesById.keySet & old.nodesById.keySet
- for (id <- commonComponentIds) {
- if (nodesById(id) == old.nodesById(id)) {
- nodesById(id).instance = old.nodesById(id).instance
- }
- }
- }
- def resetInstancesWithModifiedDependencies() {
- for {
- node <- topologicalSort(nodesById.values.toList)
- usedComponent <- node.usedComponents
- } {
- if (usedComponent.instance == None) {
- node.instance = None
- }
- }
- }
-
- copyInstancesIfNodeEqual()
- resetInstancesWithModifiedDependencies()
- }
-
- def allComponentsAndProviders = nodes map {_.instance.get}
-
- private def completeComponentRegistryNode(registry: ComponentRegistryNode) {
- registry.injectAll(osgiComponentsOfClass(registry.componentClass))
- }
-
- private def completeNode(node: ComponentNode, fallbackInjector: GuiceInjector) {
- try {
- val arguments = node.getAnnotatedConstructorParams.map(handleParameter(node, fallbackInjector, _))
-
- node.setArguments(arguments)
- } catch {
- case e : Exception => throw removeStackTrace(new RuntimeException(s"When resolving dependencies of ${node.idAndType}", e))
- }
- }
-
- private def handleParameter(node : Node,
- fallbackInjector: GuiceInjector,
- annotatedParameterType: (Type, Array[JavaAnnotation])): AnyRef =
- {
- def isConfigParameter(clazz : Class[_]) = classOf[ConfigInstance].isAssignableFrom(clazz)
- def isComponentRegistry(t : Type) = t == classOf[ComponentRegistry[_]]
-
- val (parameterType, annotations) = annotatedParameterType
-
- (parameterType match {
- case componentIdClass: Class[_] if componentIdClass == classOf[ComponentId] => node.componentId
- case configClass : Class[_] if isConfigParameter(configClass) => handleConfigParameter(node.asInstanceOf[ComponentNode], configClass)
- case registry : ParameterizedType if isComponentRegistry(registry.getRawType) => getComponentRegistry(registry.getActualTypeArguments.head)
- case clazz : Class[_] => handleComponentParameter(node, fallbackInjector, clazz, annotations)
- case other: ParameterizedType => sys.error(s"Injection of parameterized type $other is not supported.")
- case other => sys.error(s"Injection of type $other is not supported.")
- }).asInstanceOf[AnyRef]
- }
-
-
- def newComponentRegistryNode(componentClass: Class[AnyRef]): ComponentRegistryNode = {
- val registry = new ComponentRegistryNode(componentClass)
- add(registry) //TODO: don't mutate nodes here.
- registry
- }
-
- private def getComponentRegistry(componentType : Type) : ComponentRegistryNode = {
- val componentClass = componentType match {
- case wildCardType: WildcardType =>
- assert(wildCardType.getLowerBounds.isEmpty)
- assert(wildCardType.getUpperBounds.size == 1)
- wildCardType.getUpperBounds.head.asInstanceOf[Class[AnyRef]]
- case clazz: Class[_] => clazz
- case typeVariable: TypeVariable[_] =>
- throw new RuntimeException("Can't create ComponentRegistry of unknown type variable " + typeVariable)
- }
-
- componentRegistryNodes.find(_.componentClass == componentType).
- getOrElse(newComponentRegistryNode(componentClass.asInstanceOf[Class[AnyRef]]))
- }
-
- def handleConfigParameter(node : ComponentNode, clazz: Class[_]) : ConfigKeyT = {
- new ConfigKey(clazz.asInstanceOf[Class[ConfigInstance]], node.configId)
- }
-
- def getKey(clazz: Class[_], bindingAnnotation: Option[JavaAnnotation]) =
- bindingAnnotation.map(Key.get(clazz, _)).getOrElse(Key.get(clazz))
-
- private def handleComponentParameter(node: Node,
- fallbackInjector: GuiceInjector,
- clazz: Class[_],
- annotations: Array[JavaAnnotation]) : Node = {
-
- val bindingAnnotations = annotations.filter(isBindingAnnotation)
- val key = getKey(clazz, bindingAnnotations.headOption)
-
- def matchingGuiceNode(key: Key[_], instance: AnyRef): Option[GuiceNode] = {
- matchingNodes(nodesById.values, classOf[GuiceNode], key).
- filter(node => node.newOrCachedInstance eq instance). // TODO: assert that there is only one (after filter)
- headOption
- }
-
- def lookupOrCreateGlobalComponent: Node = {
- lookupGlobalComponent(key).getOrElse {
- val instance =
- try {
- log.log(LogLevel.DEBUG, "Trying the fallback injector to create" + messageForNoGlobalComponent(clazz, node))
- fallbackInjector.getInstance(key).asInstanceOf[AnyRef]
- } catch {
- case e: ConfigurationException =>
- throw removeStackTrace(new IllegalStateException(
- if (messageForMultipleClassLoaders(clazz).isEmpty)
- "No global" + messageForNoGlobalComponent(clazz, node)
- else
- messageForMultipleClassLoaders(clazz)))
-
- }
- matchingGuiceNode(key, instance).getOrElse {
- val node = new GuiceNode(instance, key.getAnnotation)
- add(node)
- node
- }
- }
- }
-
- if (bindingAnnotations.size > 1)
- sys.error("More than one binding annotation used in class '%s'".format(node.instanceType))
-
- val injectedNodesOfCorrectType = matchingComponentNodes(node.componentsToInject, key)
- injectedNodesOfCorrectType.size match {
- case 0 => lookupOrCreateGlobalComponent
- case 1 => injectedNodesOfCorrectType.head.asInstanceOf[Node]
- case _ => sys.error("Multiple components of type '%s' injected into component '%s'".format(clazz.getName, node.instanceType)) //TODO: !className for last parameter
- }
- }
-
-}
-
-object ComponentGraph {
- val log = Logger.getLogger(classOf[ComponentGraph].getName)
-
- def messageForNoGlobalComponent(clazz: Class[_], node: Node) =
- s" component of class ${clazz.getName} to inject into component ${node.idAndType}."
-
- def messageForMultipleClassLoaders(clazz: Class[_]): String = {
- val 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"
-
- (for {
- resolvedClass <- Try {Class.forName(clazz.getName, false, this.getClass.getClassLoader)}
- if resolvedClass != clazz
- } yield errMsg)
- .getOrElse("")
- }
-
- // For unit testing
- def getNode(graph: ComponentGraph, componentId: String): Node = {
- graph.nodesById(new ComponentId(componentId))
- }
-
- private def nodesOfType[T <: Node](nodes: Traversable[Node], clazz : Class[T]) : Traversable[T] = {
- nodes.collect {
- case node if clazz.isInstance(node) => clazz.cast(node)
- }
- }
-
- private def matchingComponentNodes(nodes: Traversable[Node], key: Key[_]) : Traversable[ComponentNode] = {
- matchingNodes(nodes, classOf[ComponentNode], key)
- }
-
- // Finds all nodes with a given nodeType and instance with given key
- private def matchingNodes[T <: Node](nodes: Traversable[Node], nodeType: Class[T], key: Key[_]) : Traversable[T] = {
- val clazz = key.getTypeLiteral.getRawType
- val annotation = key.getAnnotation
-
- val filteredByClass = nodesOfType(nodes, nodeType) filter { node => clazz.isAssignableFrom(node.componentType) }
- val filteredByClassAndAnnotation = filteredByClass filter { node => annotation == node.instanceKey.getAnnotation }
-
- if (filteredByClass.size == 1) filteredByClass
- else if (filteredByClassAndAnnotation.size > 0) filteredByClassAndAnnotation
- else filteredByClass
- }
-
- // Returns true if annotation is a BindingAnnotation, e.g. com.google.inject.name.Named
- def isBindingAnnotation(annotation: JavaAnnotation) : Boolean = {
- def isBindingAnnotation(clazz: Class[_]) : Boolean = {
- val clazzOrSuperIsBindingAnnotation =
- (clazz.getAnnotation(classOf[BindingAnnotation]) != null) ||
- Option(clazz.getSuperclass).map(isBindingAnnotation).getOrElse(false)
-
- (clazzOrSuperIsBindingAnnotation /: clazz.getInterfaces.map(isBindingAnnotation))(_ || _)
- }
- isBindingAnnotation(annotation.getClass)
- }
-
- /**
- * 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
- */
- def topologicalSort(nodes: List[Node]): List[Node] = {
- val numIncoming = mutable.Map[ComponentId, Int]().withDefaultValue(0)
-
- def forEachUsedComponent(nodes: Traversable[Node])(f: Node => Unit) {
- nodes.foreach(_.usedComponents.foreach(f))
- }
-
- def partitionByNoIncoming(nodes: List[Node]) =
- nodes.partition(node => numIncoming(node.componentId) == 0)
-
- @tailrec
- def sort(sorted: List[Node], unsorted: List[Node]) : List[Node] = {
- if (unsorted.isEmpty) {
- sorted
- } else {
- val (ready, notReady) = partitionByNoIncoming(unsorted)
- require(!ready.isEmpty, "There's a cycle in the graph.") //TODO: return cycle
- forEachUsedComponent(ready) { injectedNode => numIncoming(injectedNode.componentId) -= 1}
- sort(ready ::: sorted, notReady)
- }
- }
-
- forEachUsedComponent(nodes) { injectedNode => numIncoming(injectedNode.componentId) += 1 }
- sort(List(), nodes)
- }
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentNode.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentNode.scala
deleted file mode 100644
index cc1745d6e35..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentNode.scala
+++ /dev/null
@@ -1,213 +0,0 @@
-// Copyright 2017 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 java.lang.reflect.{Constructor, InvocationTargetException, Modifier, ParameterizedType, Type}
-import java.util.logging.Logger
-
-import com.google.inject.Inject
-import com.yahoo.component.{AbstractComponent, ComponentId}
-import com.yahoo.config.ConfigInstance
-import com.yahoo.container.di.componentgraph.Provider
-import com.yahoo.container.di.componentgraph.core.ComponentNode._
-import com.yahoo.container.di.componentgraph.core.Node.equalEdges
-import com.yahoo.container.di.{ConfigKeyT, JavaAnnotation, createKey, makeClassCovariant, preserveStackTrace, removeStackTrace}
-import com.yahoo.vespa.config.ConfigKey
-
-import scala.language.postfixOps
-
-/**
- * @author Tony Vaagenes
- * @author gjoranv
- */
-class ComponentNode(componentId: ComponentId,
- val configId: String,
- clazz: Class[_ <: AnyRef],
- val XXX_key: JavaAnnotation = null) // TODO expose key, not javaAnnotation
- extends Node(componentId)
-{
- require(!isAbstract(clazz), "Can't instantiate abstract class " + clazz.getName)
-
- var arguments : Array[AnyRef] = _
-
- val constructor: Constructor[AnyRef] = bestConstructor(clazz)
-
- var availableConfigs: Map[ConfigKeyT, ConfigInstance] = null
-
- override val instanceKey = createKey(clazz, XXX_key)
-
- override val instanceType = clazz
-
- override def usedComponents: List[Node] = {
- require(arguments != null, "Arguments must be set first.")
- arguments.collect{case node: Node => node}.toList
- }
-
- override val componentType: Class[AnyRef] = {
- def allSuperClasses(clazz: Class[_], coll : List[Class[_]]) : List[Class[_]] = {
- if (clazz == null) coll
- else allSuperClasses(clazz.getSuperclass, clazz :: coll)
- }
-
- def allGenericInterfaces(clazz : Class[_]) = allSuperClasses(clazz, List()) flatMap (_.getGenericInterfaces)
-
- def isProvider = classOf[Provider[_]].isAssignableFrom(clazz)
- def providerComponentType = (allGenericInterfaces(clazz).collect {
- case t: ParameterizedType if t.getRawType == classOf[Provider[_]] => t.getActualTypeArguments.head
- }).head
-
- if (isProvider) providerComponentType.asInstanceOf[Class[AnyRef]] //TODO: Test what happens if you ask for something that isn't a class, e.g. a parametrized type.
- else clazz.asInstanceOf[Class[AnyRef]]
- }
-
- def setArguments(arguments: Array[AnyRef]) {
- this.arguments = arguments
- }
-
- def cutStackTraceAtConstructor(throwable: Throwable): Throwable = {
- def takeUntilComponentNode(elements: Array[StackTraceElement]) =
- elements.takeWhile(_.getClassName != classOf[ComponentNode].getName)
-
- def dropToInitAtEnd(elements: Array[StackTraceElement]) =
- elements.reverse.dropWhile(_.getMethodName != "<init>").reverse
-
- val modifyStackTrace = takeUntilComponentNode _ andThen dropToInitAtEnd
-
- val dependencyInjectorStackTraceMarker = new StackTraceElement("============= Dependency Injection =============", "newInstance", null, -1)
-
- if (throwable != null && !preserveStackTrace) {
- throwable.setStackTrace(modifyStackTrace(throwable.getStackTrace) :+
- dependencyInjectorStackTraceMarker)
-
- cutStackTraceAtConstructor(throwable.getCause)
- }
- throwable
- }
-
- override protected def newInstance() : AnyRef = {
- assert (arguments != null, "graph.complete must be called before retrieving instances.")
-
- val actualArguments = arguments.map {
- case node: Node => node.newOrCachedInstance()
- case config: ConfigKeyT => availableConfigs(config.asInstanceOf[ConfigKeyT])
- case other => other
- }
-
- val instance =
- try {
- constructor.newInstance(actualArguments: _*)
- } catch {
- case e: InvocationTargetException =>
- throw removeStackTrace(ErrorOrComponentConstructorException(cutStackTraceAtConstructor(e.getCause), s"Error constructing $idAndType"))
- }
-
- initId(instance)
- }
-
- private def ErrorOrComponentConstructorException(cause: Throwable, message: String) : Throwable = {
- if (cause != null && cause.isInstanceOf[Error]) // don't convert Errors to RuntimeExceptions
- new Error(message, cause)
- else
- new ComponentConstructorException(message, cause)
- }
-
- private def initId(component: AnyRef) = {
- def checkAndSetId(c: AbstractComponent) {
- if (c.hasInitializedId && c.getId != componentId )
- throw new IllegalStateException(
- s"Component with id '$componentId' is trying to set its component id explicitly: '${c.getId}'. " +
- "This is not allowed, so please remove any call to super() in your component's constructor.")
-
- c.initId(componentId)
- }
-
- component match {
- case component: AbstractComponent => checkAndSetId(component)
- case other => ()
- }
- component
- }
-
- override def equals(other: Any) = {
- other match {
- case that: ComponentNode =>
- super.equals(that) &&
- equalEdges(arguments.toList, that.arguments.toList) &&
- usedConfigs == that.usedConfigs
- }
- }
-
- private def usedConfigs = {
- require(availableConfigs != null, "setAvailableConfigs must be called!")
- ( arguments collect {case c: ConfigKeyT => c} map (availableConfigs) ).toList
- }
-
- def getAnnotatedConstructorParams: Array[(Type, Array[JavaAnnotation])] = {
- constructor.getGenericParameterTypes zip constructor.getParameterAnnotations
- }
-
- def setAvailableConfigs(configs: Map[ConfigKeyT, ConfigInstance]) {
- require (arguments != null, "graph.complete must be called before graph.setAvailableConfigs.")
- availableConfigs = configs
- }
-
- override def configKeys = {
- configParameterClasses.map(new ConfigKey(_, configId)).toSet
- }
-
-
- private def configParameterClasses: Array[Class[ConfigInstance]] = {
- constructor.getGenericParameterTypes.collect {
- case clazz: Class[_] if classOf[ConfigInstance].isAssignableFrom(clazz) => clazz.asInstanceOf[Class[ConfigInstance]]
- }
- }
-
- override def label = {
- val configNames = configKeys.map(_.getName + ".def").toList
-
- (List(instanceType.getSimpleName, Node.packageName(instanceType)) ::: configNames).
- mkString("{", "|", "}")
- }
-
-}
-
-object ComponentNode {
- val log = Logger.getLogger(classOf[ComponentNode].getName)
-
- private def bestConstructor(clazz: Class[AnyRef]) = {
- val publicConstructors = clazz.getConstructors.asInstanceOf[Array[Constructor[AnyRef]]]
-
- def constructorAnnotatedWithInject = {
- publicConstructors filter {_.getAnnotation(classOf[Inject]) != null} match {
- case Array() => None
- case Array(single) => Some(single)
- case _ => throwComponentConstructorException("Multiple constructors annotated with inject in class " + clazz.getName)
- }
- }
-
- def constructorWithMostConfigParameters = {
- def isConfigInstance(clazz: Class[_]) = classOf[ConfigInstance].isAssignableFrom(clazz)
-
- publicConstructors match {
- case Array() => throwComponentConstructorException("No public constructors in class " + clazz.getName)
- case Array(single) => single
- case _ =>
- log.warning("Multiple public constructors found in class %s, there should only be one. ".format(clazz.getName) +
- "If more than one public constructor is needed, the primary one must be annotated with @Inject.")
- publicConstructors.
- sortBy(_.getParameterTypes.filter(isConfigInstance).size).
- last
- }
- }
-
- constructorAnnotatedWithInject getOrElse constructorWithMostConfigParameters
- }
-
- private def throwComponentConstructorException(message: String) =
- throw removeStackTrace(new ComponentConstructorException(message))
-
- class ComponentConstructorException(message: String, cause: Throwable) extends RuntimeException(message, cause) {
- def this(message: String) = this(message, null)
- }
-
- def isAbstract(clazz: Class[_ <: AnyRef]) = Modifier.isAbstract(clazz.getModifiers)
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.scala
deleted file mode 100644
index 9c7e2ba322f..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.scala
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2017 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.provider.ComponentRegistry
-import com.yahoo.component.{ComponentId, Component}
-import ComponentRegistryNode._
-import com.google.inject.Key
-import com.google.inject.util.Types
-import Node.syntheticComponentId
-
-/**
- * @author Tony Vaagenes
- * @author gjoranv
- */
-class ComponentRegistryNode(val componentClass : Class[AnyRef])
- extends Node(componentId(componentClass)) {
-
- def usedComponents = componentsToInject
-
- protected def newInstance() = {
- val registry = new ComponentRegistry[AnyRef]
-
- componentsToInject foreach { component =>
- registry.register(component.componentId, component.newOrCachedInstance())
- }
-
- registry
- }
-
- override val instanceKey =
- Key.get(Types.newParameterizedType(classOf[ComponentRegistry[_]], componentClass)).asInstanceOf[Key[AnyRef]]
-
- override val instanceType: Class[AnyRef] = instanceKey.getTypeLiteral.getRawType.asInstanceOf[Class[AnyRef]]
- override val componentType: Class[AnyRef] = instanceType
-
- override def configKeys = Set()
-
- override def equals(other: Any) = {
- other match {
- case that: ComponentRegistryNode =>
- componentId == that.componentId && // includes componentClass
- instanceType == that.instanceType &&
- equalEdges(usedComponents, that.usedComponents)
- case _ => false
- }
- }
-
- override def label =
- "{ComponentRegistry\\<%s\\>|%s}".format(componentClass.getSimpleName, Node.packageName(componentClass))
-}
-
-object ComponentRegistryNode {
- val componentRegistryNamespace = ComponentId.fromString("ComponentRegistry")
-
- def componentId(componentClass: Class[_]) = {
- syntheticComponentId(componentClass.getName, componentClass, componentRegistryNamespace)
- }
-
- def equalEdges(edges: List[Node], otherEdges: List[Node]): Boolean = {
- def compareEdges = {
- (sortByComponentId(edges) zip sortByComponentId(otherEdges)).
- forall(equalEdge)
- }
-
- def sortByComponentId(in: List[Node]) = in.sortBy(_.componentId)
- def equalEdge(edgePair: (Node, Node)): Boolean = edgePair._1.componentId == edgePair._2.componentId
-
- edges.size == otherEdges.size &&
- compareEdges
- }
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/GuiceNode.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/GuiceNode.scala
deleted file mode 100644
index 26fb0e0d3d8..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/GuiceNode.scala
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright 2017 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.container.di.{JavaAnnotation, createKey}
-import com.yahoo.component.ComponentId
-import Node.syntheticComponentId
-import GuiceNode._
-
-/**
- * @author Tony Vaagenes
- * @author gjoranv
- */
-final class GuiceNode(myInstance: AnyRef,
- annotation: JavaAnnotation) extends Node(componentId(myInstance)) {
-
- override def configKeys = Set()
-
- override val instanceKey = createKey(myInstance.getClass, annotation)
- override val instanceType = myInstance.getClass
- override val componentType = instanceType
-
-
- override def usedComponents = List()
-
- override protected def newInstance() = myInstance
-
- override def inject(component: Node) {
- throw new UnsupportedOperationException("Illegal to inject components to a GuiceNode!")
- }
-
- override def label =
- "{{%s|Guice}|%s}".format(instanceType.getSimpleName, Node.packageName(instanceType))
-}
-
-object GuiceNode {
- val guiceNamespace = ComponentId.fromString("Guice")
-
- def componentId(instance: AnyRef) = {
- syntheticComponentId(instance.getClass.getName, instance, guiceNamespace)
- }
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/JerseyNode.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/JerseyNode.scala
deleted file mode 100644
index 68353c47124..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/JerseyNode.scala
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright 2017 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 java.net.URL
-
-import com.yahoo.component.{ComponentId, ComponentSpecification}
-import com.yahoo.container.di.Osgi
-import com.yahoo.container.di.Osgi.RelativePath
-import com.yahoo.container.di.componentgraph.core.JerseyNode._
-import com.yahoo.container.di.config.RestApiContext
-import com.yahoo.container.di.config.RestApiContext.BundleInfo
-import org.osgi.framework.Bundle
-import org.osgi.framework.wiring.BundleWiring
-
-import scala.collection.JavaConverters._
-
-/**
- * Represents an instance of RestApiContext
- *
- * @author gjoranv
- * @author Tony Vaagenes
- */
-class JerseyNode(componentId: ComponentId,
- override val configId: String,
- clazz: Class[_ <: RestApiContext],
- osgi: Osgi)
- extends ComponentNode(componentId, configId, clazz) {
-
-
- override protected def newInstance(): RestApiContext = {
- val instance = super.newInstance()
- val restApiContext = instance.asInstanceOf[RestApiContext]
-
- val bundles = restApiContext.bundlesConfig.bundles().asScala
- bundles foreach ( bundleConfig => {
- val bundleClasses = osgi.getBundleClasses(
- ComponentSpecification.fromString(bundleConfig.spec()),
- bundleConfig.packages().asScala.toSet)
-
- restApiContext.addBundle(
- createBundleInfo(bundleClasses.bundle, bundleClasses.classEntries))
- })
-
- componentsToInject foreach (
- component =>
- restApiContext.addInjectableComponent(component.instanceKey, component.componentId, component.newOrCachedInstance()))
-
- restApiContext
- }
-
- override def equals(other: Any): Boolean = {
- super.equals(other) && (other match {
- case that: JerseyNode => componentsToInject == that.componentsToInject
- case _ => false
- })
- }
-
-}
-
-private[core]
-object JerseyNode {
- val WebInfUrl = "WebInfUrl"
-
- def createBundleInfo(bundle: Bundle, classEntries: Iterable[RelativePath]): BundleInfo = {
-
- val bundleInfo = new BundleInfo(bundle.getSymbolicName,
- bundle.getVersion,
- bundle.getLocation,
- webInfUrl(bundle),
- bundle.adapt(classOf[BundleWiring]).getClassLoader)
-
- bundleInfo.setClassEntries(classEntries.asJavaCollection)
- bundleInfo
- }
-
-
- private def getBundle(osgi: Osgi, bundleSpec: String): Bundle = {
-
- val bundle = osgi.getBundle(ComponentSpecification.fromString(bundleSpec))
- if (bundle == null)
- throw new IllegalArgumentException("Bundle not found: " + bundleSpec)
- bundle
- }
-
- private def webInfUrl(bundle: Bundle): URL = {
- val strWebInfUrl = bundle.getHeaders.get(WebInfUrl)
-
- if (strWebInfUrl == null) null
- else bundle.getEntry(strWebInfUrl)
- }
-
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/Node.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/Node.scala
deleted file mode 100644
index d2476904e39..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/Node.scala
+++ /dev/null
@@ -1,117 +0,0 @@
-// Copyright 2017 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 java.util.logging.Logger
-
-import com.google.inject.Key
-import com.yahoo.component.ComponentId
-import com.yahoo.container.di.ConfigKeyT
-import com.yahoo.container.di.componentgraph.Provider
-import com.yahoo.container.di.componentgraph.core.Node._
-import com.yahoo.log.LogLevel.{DEBUG, SPAM}
-
-/**
- * @author Tony Vaagenes
- * @author gjoranv
- */
-abstract class Node(val componentId: ComponentId) {
-
- def instanceKey: Key[AnyRef]
-
- var instance : Option[AnyRef] = None
-
- var componentsToInject = List[Node]()
-
- /**
- * The components actually used by this node.
- * Consist of a subset of the injected nodes + subset of the global nodes.
- */
- def usedComponents: List[Node]
-
- protected def newInstance() : AnyRef
-
- def newOrCachedInstance() : AnyRef = {
- val inst = if (instance.isEmpty) {
- log.log(DEBUG, s"Creating new instance for component with ID $componentId")
- instance = Some(newInstance())
- instance.get
- } else {
- log.log(SPAM, s"Reusing instance for component with ID $componentId")
- instance.get
- }
- component(inst)
- }
-
- private def component(instance: AnyRef) = instance match {
- case provider: Provider[_] => provider.get().asInstanceOf[AnyRef]
- case other => other
- }
-
- def configKeys: Set[ConfigKeyT]
-
- def inject(component: Node) {
- componentsToInject ::= component
- }
-
- def injectAll(componentNodes: Traversable[ComponentNode]) {
- componentNodes.foreach(inject(_))
- }
-
- def instanceType: Class[_ <: AnyRef]
- def componentType: Class[_ <: AnyRef]
-
- override def equals(other: Any) = {
- other match {
- case that: Node =>
- getClass == that.getClass &&
- componentId == that.componentId &&
- instanceType == that.instanceType &&
- equalEdges(usedComponents, that.usedComponents)
- case _ => false
- }
- }
-
- def label: String
-
- def idAndType = {
- val className = instanceType.getName
-
- if (className == componentId.getName) s"'$componentId'"
- else s"'$componentId' of type '$className'"
- }
-
-}
-
-object Node {
- private val log = Logger.getLogger(classOf[Node].getName)
-
- def equalEdges(edges1: List[AnyRef], edges2: List[AnyRef]): Boolean = {
- def compare(objects: (AnyRef, AnyRef)): Boolean = {
- objects match {
- case (edge1: Node, edge2: Node) => equalEdge(edge1, edge2)
- case (o1, o2) => o1 == o2
- }
- }
-
- def equalEdge(e1: Node, e2: Node) = e1.componentId == e2.componentId
-
- (edges1 zip edges2).forall(compare)
- }
-
- /**
- * @param identityObject The identifying object that makes the Node unique
- */
- private[componentgraph]
- def syntheticComponentId(className: String, identityObject: AnyRef, namespace: ComponentId) = {
- val name = className + "_" + System.identityHashCode(identityObject)
- ComponentId.fromString(name).nestInNamespace(namespace)
- }
-
-
- def packageName(componentClass: Class[_]) = {
- def nullIfNotFound(index : Int) = if (index == -1) 0 else index
-
- val fullClassName = componentClass.getName
- fullClassName.substring(0, nullIfNotFound(fullClassName.lastIndexOf(".")))
- }
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/osgi/OsgiUtil.scala b/container-di/src/main/scala/com/yahoo/container/di/osgi/OsgiUtil.scala
deleted file mode 100644
index 3769eed6d2d..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/osgi/OsgiUtil.scala
+++ /dev/null
@@ -1,143 +0,0 @@
-// Copyright 2017 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 java.nio.file.{Files, Path, Paths}
-import java.util.function.Predicate
-import java.util.jar.{JarEntry, JarFile}
-import java.util.logging.{Level, Logger}
-import java.util.stream.Collectors
-
-import com.google.common.io.Files.fileTreeTraverser
-import com.yahoo.component.ComponentSpecification
-import com.yahoo.container.di.Osgi.RelativePath
-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 scala.collection.JavaConverters._
-
-/**
- * Tested by com.yahoo.application.container.jersey.JerseyTest
- * @author Tony Vaagenes
- */
-object OsgiUtil {
- private val log = Logger.getLogger(getClass.getName)
- private val classFileTypeSuffix = ".class"
-
- def getClassEntriesInBundleClassPath(bundle: Bundle, packagesToScan: Set[String]) = {
- val bundleWiring = bundle.adapt(classOf[BundleWiring])
-
- def listClasses(path: String, recurse: Boolean): Iterable[RelativePath] = {
- val options =
- if (recurse) BundleWiring.LISTRESOURCES_LOCAL | BundleWiring.LISTRESOURCES_RECURSE
- else BundleWiring.LISTRESOURCES_LOCAL
-
- bundleWiring.listResources(path, "*" + classFileTypeSuffix, options).asScala
- }
-
- if (packagesToScan.isEmpty) listClasses("/", recurse = true)
- else packagesToScan flatMap { packageName => listClasses(packageToPath(packageName), recurse = false) }
- }
-
- def getClassEntriesForBundleUsingProjectClassPathMappings(classLoader: ClassLoader,
- bundleSpec: ComponentSpecification,
- packagesToScan: Set[String]) = {
- classEntriesFrom(
- bundleClassPathMapping(bundleSpec, classLoader).classPathElements.asScala.toList,
- packagesToScan)
- }
-
- private def bundleClassPathMapping(bundleSpec: ComponentSpecification,
- classLoader: ClassLoader): BundleClasspathMapping = {
-
- val projectBundleClassPaths = loadProjectBundleClassPaths(classLoader)
-
- if (projectBundleClassPaths.mainBundle.bundleSymbolicName == bundleSpec.getName) {
- projectBundleClassPaths.mainBundle
- } else {
- log.log(Level.WARNING, s"Dependencies of the bundle $bundleSpec will not be scanned. Please file a feature request if you need this" )
- matchingBundleClassPathMapping(bundleSpec, projectBundleClassPaths.providedDependencies.asScala.toList)
- }
- }
-
- def matchingBundleClassPathMapping(bundleSpec: ComponentSpecification,
- providedBundlesClassPathMappings: List[BundleClasspathMapping]): BundleClasspathMapping = {
- providedBundlesClassPathMappings.
- find(_.bundleSymbolicName == bundleSpec.getName).
- getOrElse(throw new RuntimeException("No such bundle: " + bundleSpec))
- }
-
- private def loadProjectBundleClassPaths(classLoader: ClassLoader): ProjectBundleClassPaths = {
- val classPathMappingsFileLocation = classLoader.getResource(ProjectBundleClassPaths.CLASSPATH_MAPPINGS_FILENAME)
- if (classPathMappingsFileLocation == null)
- throw new RuntimeException(s"Couldn't find ${ProjectBundleClassPaths.CLASSPATH_MAPPINGS_FILENAME} in the class path.")
-
- ProjectBundleClassPaths.load(Paths.get(classPathMappingsFileLocation.toURI))
- }
-
- private def classEntriesFrom(classPathEntries: List[String], packagesToScan: Set[String]): Iterable[RelativePath] = {
- val packagePathsToScan = packagesToScan map packageToPath
-
- classPathEntries.flatMap { entry =>
- val path = Paths.get(entry)
- if (Files.isDirectory(path)) classEntriesInPath(path, packagePathsToScan)
- else if (Files.isRegularFile(path) && path.getFileName.toString.endsWith(".jar")) classEntriesInJar(path, packagePathsToScan)
- else throw new RuntimeException("Unsupported path " + path + " in the class path")
- }
- }
-
- private def classEntriesInPath(rootPath: Path, packagePathsToScan: Traversable[String]): Traversable[RelativePath] = {
- def relativePathToClass(pathToClass: Path): RelativePath = {
- val relativePath = rootPath.relativize(pathToClass)
- relativePath.toString
- }
-
- val fileIterator =
- if (packagePathsToScan.isEmpty) fileTreeTraverser().preOrderTraversal(rootPath.toFile).asScala
- else packagePathsToScan.view flatMap { packagePath => fileTreeTraverser().children(rootPath.resolve(packagePath).toFile).asScala }
-
- for {
- file <- fileIterator
- if file.isFile
- if file.getName.endsWith(classFileTypeSuffix)
- } yield relativePathToClass(file.toPath)
- }
-
-
- private def classEntriesInJar(jarPath: Path, packagePathsToScan: Set[String]): Traversable[RelativePath] = {
- def packagePath(name: String) = {
- name.lastIndexOf('/') match {
- case -1 => name
- case n => name.substring(0, n)
- }
- }
-
- val acceptedPackage: Predicate[String] =
- if (packagePathsToScan.isEmpty) (name: String) => true
- else (name: String) => packagePathsToScan(packagePath(name))
-
- var jarFile: JarFile = null
- try {
- jarFile = new JarFile(jarPath.toFile)
- jarFile.stream().
- map[String] { entry: JarEntry => entry.getName}.
- filter { name: String => name.endsWith(classFileTypeSuffix)}.
- filter(acceptedPackage).
- collect(Collectors.toList()).
- asScala
- } finally {
- if (jarFile != null) jarFile.close()
- }
- }
-
- def packageToPath(packageName: String) = packageName.replaceAllLiterally(".", "/")
-
- implicit class JavaPredicate[T](f: T => Boolean) extends Predicate[T] {
- override def test(t: T): Boolean = f(t)
- }
-
- implicit class JavaFunction[T, R](f: T => R) extends java.util.function.Function[T, R] {
- override def apply(t: T): R = f(t)
- }
-}
diff --git a/container-di/src/main/scala/com/yahoo/container/di/package.scala b/container-di/src/main/scala/com/yahoo/container/di/package.scala
deleted file mode 100644
index cccb0242e8b..00000000000
--- a/container-di/src/main/scala/com/yahoo/container/di/package.scala
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.container
-
-import java.lang.reflect.Type
-
-import com.google.inject.Key
-import com.yahoo.config.ConfigInstance
-import com.yahoo.vespa.config.ConfigKey
-
-import scala.language.implicitConversions
-
-/**
- *
- * @author gjoranv
- * @author Tony Vaagenes
- */
-package object di {
- type ConfigKeyT = ConfigKey[_ <: ConfigInstance]
- type GuiceInjector = com.google.inject.Injector
- type JavaAnnotation = java.lang.annotation.Annotation
-
- def createKey(instanceType: Type, annotation: JavaAnnotation) = {
- {if (annotation == null)
- Key.get(instanceType)
- else
- Key.get(instanceType, annotation)
- }.asInstanceOf[Key[AnyRef]]
- }
-
- implicit def makeClassCovariant[SUB, SUPER >: SUB](clazz: Class[SUB]) : Class[SUPER] = {
- clazz.asInstanceOf[Class[SUPER]]
- }
-
- def removeStackTrace(exception: Throwable): Throwable = {
- if (preserveStackTrace) exception
- else {
- exception.setStackTrace(Array())
- exception
- }
- }
-
- //For debug purposes only
- val preserveStackTrace: Boolean = Option(System.getProperty("jdisc.container.preserveStackTrace")).filterNot(_.isEmpty).isDefined
-}
diff --git a/container-di/src/test/java/com/yahoo/container/di/ConfigRetrieverTest.java b/container-di/src/test/java/com/yahoo/container/di/ConfigRetrieverTest.java
new file mode 100644
index 00000000000..e6b0309981a
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/container/di/ConfigRetrieverTest.java
@@ -0,0 +1,137 @@
+// 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.test.Bootstrap1Config;
+import com.yahoo.config.test.Bootstrap2Config;
+import com.yahoo.config.test.TestConfig;
+import com.yahoo.container.di.ConfigRetriever.BootstrapConfigs;
+import com.yahoo.container.di.ConfigRetriever.ComponentsConfigs;
+import com.yahoo.container.di.ConfigRetriever.ConfigSnapshot;
+import com.yahoo.vespa.config.ConfigKey;
+import org.hamcrest.Matchers;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Ignore;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+
+import static org.hamcrest.CoreMatchers.instanceOf;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.fail;
+
+/**
+ *
+ * @author gjoranv
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class ConfigRetrieverTest {
+
+ private DirConfigSource dirConfigSource = null;
+
+ @Before
+ public void setup() {
+ dirConfigSource = new DirConfigSource("ConfigRetrieverTest-");
+ }
+
+ @After
+ public void cleanup() {
+ dirConfigSource.cleanup();
+ }
+
+ @Test
+ public void require_that_bootstrap_configs_come_first() {
+ writeConfigs();
+ ConfigRetriever retriever = createConfigRetriever();
+ ConfigSnapshot bootstrapConfigs = retriever.getConfigs(Collections.emptySet(), 0, false);
+
+ assertThat(bootstrapConfigs, Matchers.instanceOf(BootstrapConfigs.class));
+ }
+
+ @Test
+ @SuppressWarnings("unused")
+ public void require_that_components_comes_after_bootstrap() {
+ writeConfigs();
+ ConfigRetriever retriever = createConfigRetriever();
+ ConfigSnapshot bootstrapConfigs = retriever.getConfigs(Collections.emptySet(), 0, false);
+
+ ConfigKey<? extends ConfigInstance> testConfigKey = new ConfigKey<>(TestConfig.class, dirConfigSource.configId());
+ ConfigSnapshot componentsConfigs = retriever.getConfigs(Collections.singleton(testConfigKey), 0);
+
+ if (componentsConfigs instanceof ComponentsConfigs) {
+ assertThat(componentsConfigs.size(), is(3));
+ } else {
+ fail("ComponentsConfigs has unexpected type: " + componentsConfigs);
+ }
+ }
+
+ @Test
+ @SuppressWarnings("unused")
+ public void require_no_reconfig_when_restart_on_redeploy() {
+ // TODO
+ writeConfigs();
+ ConfigRetriever retriever = createConfigRetriever();
+ ConfigSnapshot bootstrapConfigs = retriever.getConfigs(Collections.emptySet(), 0, false);
+
+ ConfigKey<? extends ConfigInstance> testConfigKey = new ConfigKey<>(TestConfig.class, dirConfigSource.configId());
+ Optional<ConfigSnapshot> componentsConfigs = retriever.getConfigsOnce(Collections.singleton(testConfigKey), 0, true);
+
+ if (componentsConfigs.isPresent()) {
+ fail("Expected no configs");
+ }
+ }
+
+ @Rule
+ public ExpectedException expectedException = ExpectedException.none();
+
+ @Ignore
+ @SuppressWarnings("unused")
+ public void require_exception_upon_modified_components_keys_without_bootstrap() {
+ expectedException.expect(IllegalArgumentException.class);
+
+ writeConfigs();
+ ConfigRetriever retriever = createConfigRetriever();
+ ConfigKey<? extends ConfigInstance> testConfigKey = new ConfigKey<>(TestConfig.class, dirConfigSource.configId());
+ ConfigSnapshot bootstrapConfigs = retriever.getConfigs(Collections.emptySet(), 0, false);
+ ConfigSnapshot componentsConfigs = retriever.getConfigs(Collections.singleton(testConfigKey), 0, false);
+ Set<ConfigKey<? extends ConfigInstance>> keys = new HashSet<>();
+ keys.add(testConfigKey);
+ keys.add(new ConfigKey<>(TestConfig.class, ""));
+ retriever.getConfigs(keys, 0);
+ }
+
+ @Test
+ public void require_that_empty_components_keys_after_bootstrap_returns_components_configs() {
+ writeConfigs();
+ ConfigRetriever retriever = createConfigRetriever();
+ assertThat(retriever.getConfigs(Collections.emptySet(), 0), instanceOf(BootstrapConfigs.class));
+ assertThat(retriever.getConfigs(Collections.emptySet(), 0), instanceOf(ComponentsConfigs.class));
+ }
+
+ public void writeConfigs() {
+ writeConfig("bootstrap1", "dummy \"ignored\"");
+ writeConfig("bootstrap2", "dummy \"ignored\"");
+ writeConfig("test", "stringVal \"ignored\"");
+ }
+
+ private ConfigRetriever createConfigRetriever() {
+ String configId = dirConfigSource.configId();
+ CloudSubscriberFactory subscriber = new CloudSubscriberFactory(dirConfigSource.configSource());
+ Set<ConfigKey<? extends ConfigInstance>> keys = new HashSet<>();
+ keys.add(new ConfigKey<>(Bootstrap1Config.class, configId));
+ keys.add(new ConfigKey<>(Bootstrap2Config.class, configId));
+ return new ConfigRetriever(keys, keySet -> subscriber.getSubscriber(keySet));
+ }
+
+ private void writeConfig(String name, String contents) {
+ dirConfigSource.writeConfig(name, contents);
+ }
+}
diff --git a/container-di/src/test/java/com/yahoo/container/di/ContainerTest.java b/container-di/src/test/java/com/yahoo/container/di/ContainerTest.java
new file mode 100644
index 00000000000..7e01505dc03
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/container/di/ContainerTest.java
@@ -0,0 +1,378 @@
+// 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.AbstractComponent;
+import com.yahoo.config.di.IntConfig;
+import com.yahoo.config.test.TestConfig;
+import com.yahoo.container.bundle.MockBundle;
+import com.yahoo.container.di.componentgraph.Provider;
+import com.yahoo.container.di.componentgraph.core.ComponentGraph;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent2;
+import com.yahoo.container.di.componentgraph.core.ComponentNode.ComponentConstructorException;
+import com.yahoo.container.di.componentgraph.core.Node;
+import com.yahoo.container.di.config.RestApiContext;
+import org.junit.Ignore;
+import org.junit.Test;
+
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertSame;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author Tony Vaagenes
+ * @author gjoranv
+ * @author ollivir
+ */
+public class ContainerTest extends ContainerTestBase {
+
+ @Test
+ public void components_can_be_created_from_config() {
+ writeBootstrapConfigs();
+ dirConfigSource.writeConfig("test", "stringVal \"myString\"");
+
+ Container container = newContainer(dirConfigSource);
+
+ ComponentTakingConfig component = createComponentTakingConfig(container.getNewComponentGraph());
+ assertThat(component.config.stringVal(), is("myString"));
+
+ container.shutdownConfigurer();
+ }
+
+ @Test
+ public void components_are_reconfigured_after_config_update_without_bootstrap_configs() {
+ writeBootstrapConfigs();
+ dirConfigSource.writeConfig("test", "stringVal \"original\"");
+
+ Container container = newContainer(dirConfigSource);
+
+ ComponentGraph componentGraph = container.getNewComponentGraph();
+ ComponentTakingConfig component = createComponentTakingConfig(componentGraph);
+
+ assertThat(component.config.stringVal(), is("original"));
+
+ // Reconfigure
+ dirConfigSource.writeConfig("test", "stringVal \"reconfigured\"");
+ container.reloadConfig(2);
+
+ ComponentGraph newComponentGraph = container.getNewComponentGraph(componentGraph);
+ ComponentTakingConfig component2 = createComponentTakingConfig(newComponentGraph);
+ assertThat(component2.config.stringVal(), is("reconfigured"));
+
+ container.shutdownConfigurer();
+ }
+
+ @Test
+ public void graph_is_updated_after_bootstrap_update() {
+ dirConfigSource.writeConfig("test", "stringVal \"original\"");
+ writeBootstrapConfigs("id1");
+
+ Container container = newContainer(dirConfigSource);
+
+ ComponentGraph graph = container.getNewComponentGraph();
+ ComponentTakingConfig component = createComponentTakingConfig(graph);
+ assertThat(component.getId().toString(), is("id1"));
+
+ writeBootstrapConfigs(
+ new ComponentEntry("id1", ComponentTakingConfig.class),
+ new ComponentEntry("id2", ComponentTakingConfig.class));
+
+ container.reloadConfig(2);
+ ComponentGraph newGraph = container.getNewComponentGraph(graph);
+
+ assertThat(ComponentGraph.getNode(newGraph, "id1"), notNullValue(Node.class));
+ assertThat(ComponentGraph.getNode(newGraph, "id2"), notNullValue(Node.class));
+
+ container.shutdownConfigurer();
+ }
+
+ //@Test TODO
+ public void deconstructor_is_given_guice_components() {
+ }
+
+ @Test
+ public void osgi_component_is_deconstructed_when_not_reused() {
+ writeBootstrapConfigs("id1", DestructableComponent.class);
+
+ Container container = newContainer(dirConfigSource);
+
+ ComponentGraph oldGraph = container.getNewComponentGraph();
+ DestructableComponent componentToDestruct = oldGraph.getInstance(DestructableComponent.class);
+
+ writeBootstrapConfigs("id2", DestructableComponent.class);
+ container.reloadConfig(2);
+ container.getNewComponentGraph(oldGraph);
+ assertTrue(componentToDestruct.deconstructed);
+ }
+
+ @Ignore // because logAndDie is impossible(?) to verify programmatically
+ @Test
+ public void manually_verify_what_happens_when_first_graph_contains_component_that_throws_exception_in_ctor() {
+ writeBootstrapConfigs("thrower", ComponentThrowingExceptionInConstructor.class);
+ Container container = newContainer(dirConfigSource);
+ try {
+ container.getNewComponentGraph();
+ fail("Expected to log and die.");
+ } catch (Throwable t) {
+ fail("Expected to log and die");
+ }
+ }
+
+ @Test
+ public void previous_graph_is_retained_when_new_graph_contains_component_that_throws_exception_in_ctor() {
+ ComponentEntry simpleComponentEntry = new ComponentEntry("simpleComponent", SimpleComponent.class);
+
+ writeBootstrapConfigs(simpleComponentEntry);
+ Container container = newContainer(dirConfigSource);
+ ComponentGraph currentGraph = container.getNewComponentGraph();
+
+ SimpleComponent simpleComponent = currentGraph.getInstance(SimpleComponent.class);
+
+ writeBootstrapConfigs("thrower", ComponentThrowingExceptionInConstructor.class);
+ container.reloadConfig(2);
+ try {
+ currentGraph = container.getNewComponentGraph(currentGraph);
+ fail("Expected exception");
+ } catch (ComponentConstructorException ignored) {
+ // Expected, do nothing
+ } catch (Throwable t) {
+ fail("Expected ComponentConstructorException");
+ }
+ assertEquals(1, currentGraph.generation());
+
+ // Also verify that next reconfig is successful
+ ComponentEntry componentTakingConfigEntry = new ComponentEntry("componentTakingConfig", ComponentTakingConfig.class);
+ dirConfigSource.writeConfig("test", "stringVal \"myString\"");
+ writeBootstrapConfigs(simpleComponentEntry, componentTakingConfigEntry);
+ container.reloadConfig(3);
+ currentGraph = container.getNewComponentGraph(currentGraph);
+
+ assertEquals(3, currentGraph.generation());
+ assertSame(simpleComponent, currentGraph.getInstance(SimpleComponent.class));
+ assertNotNull(currentGraph.getInstance(ComponentTakingConfig.class));
+ }
+
+ @Test
+ public void previous_graph_is_retained_when_new_graph_throws_exception_for_missing_config() {
+ ComponentEntry simpleComponentEntry = new ComponentEntry("simpleComponent", SimpleComponent.class);
+
+ writeBootstrapConfigs(simpleComponentEntry);
+ Container container = newContainer(dirConfigSource);
+ ComponentGraph currentGraph = container.getNewComponentGraph();
+
+ currentGraph.getInstance(SimpleComponent.class);
+
+ writeBootstrapConfigs("thrower", ComponentThrowingExceptionForMissingConfig.class);
+ dirConfigSource.writeConfig("test", "stringVal \"myString\"");
+ container.reloadConfig(2);
+ try {
+ currentGraph = container.getNewComponentGraph(currentGraph);
+ fail("Expected exception");
+ } catch (IllegalArgumentException ignored) {
+ // Expected, do nothing
+ } catch (Throwable t) {
+ fail("Expected IllegalArgumentException");
+ }
+ assertEquals(1, currentGraph.generation());
+ }
+
+ @Test
+ public void runOnce_hangs_waiting_for_valid_config_after_invalid_config() throws InterruptedException, ExecutionException, TimeoutException {
+ dirConfigSource.writeConfig("test", "stringVal \"original\"");
+ writeBootstrapConfigs("myId", ComponentTakingConfig.class);
+
+ Container container = newContainer(dirConfigSource);
+ final ComponentGraph currentGraph = container.getNewComponentGraph();
+
+ writeBootstrapConfigs("thrower", ComponentThrowingExceptionForMissingConfig.class);
+ container.reloadConfig(2);
+
+ try {
+ container.getNewComponentGraph(currentGraph);
+ fail("expected exception");
+ } catch (Exception ignored) {
+ }
+ ExecutorService exec = Executors.newFixedThreadPool(1);
+ Future<ComponentGraph> newGraph = exec.submit(() -> container.getNewComponentGraph(currentGraph));
+
+ try {
+ newGraph.get(1, TimeUnit.SECONDS);
+ fail("Expected waiting for new config.");
+ } catch (Exception ignored) {
+ // expect to time out
+ }
+
+ writeBootstrapConfigs("myId2", ComponentTakingConfig.class);
+ container.reloadConfig(3);
+
+ assertNotNull(newGraph.get(5, TimeUnit.MINUTES));
+ }
+
+
+ @Test
+ public void bundle_info_is_set_on_rest_api_context() {
+ Class<RestApiContext> clazz = RestApiContext.class;
+
+ writeBootstrapConfigs("restApiContext", clazz);
+ dirConfigSource.writeConfig("jersey-bundles", "bundles[0].spec \"mock-entry-to-enforce-a-MockBundle\"");
+ dirConfigSource.writeConfig("jersey-injection", "inject[0]");
+
+ Container container = newContainer(dirConfigSource);
+ ComponentGraph componentGraph = container.getNewComponentGraph();
+
+ RestApiContext restApiContext = componentGraph.getInstance(clazz);
+ assertNotNull(restApiContext);
+
+ assertThat(restApiContext.getBundles().size(), is(1));
+ assertThat(restApiContext.getBundles().get(0).symbolicName, is(MockBundle.SymbolicName));
+ assertThat(restApiContext.getBundles().get(0).version, is(MockBundle.BundleVersion));
+
+ container.shutdownConfigurer();
+ }
+
+ @Test
+ public void restApiContext_has_all_components_injected() {
+ Class<RestApiContext> restApiClass = RestApiContext.class;
+ Class<SimpleComponent> injectedClass = SimpleComponent.class;
+ String injectedComponentId = "injectedComponent";
+ Class<SimpleComponent2> anotherComponentClass = SimpleComponent2.class;
+ String anotherComponentId = "anotherComponent";
+
+ String componentsConfig =
+ new ComponentEntry(injectedComponentId, injectedClass).asConfig(0) + "\n" +
+ new ComponentEntry(anotherComponentId, anotherComponentClass).asConfig(1) + "\n" +
+ new ComponentEntry("restApiContext", restApiClass).asConfig(2) + "\n" +
+ "components[2].inject[0].id " + injectedComponentId + "\n" +
+ "components[2].inject[1].id " + anotherComponentId + "\n";
+
+ String injectionConfig = "inject[1]\n" +//
+ "inject[0].instance " + injectedComponentId + "\n" +//
+ "inject[0].forClass \"" + injectedClass.getName() + "\"\n";
+
+ dirConfigSource.writeConfig("components", componentsConfig);
+ dirConfigSource.writeConfig("bundles", "");
+ dirConfigSource.writeConfig("jersey-bundles", "bundles[0].spec \"mock-entry-to-enforce-a-MockBundle\"");
+ dirConfigSource.writeConfig("jersey-injection", injectionConfig);
+
+ Container container = newContainer(dirConfigSource);
+ ComponentGraph componentGraph = container.getNewComponentGraph();
+
+ RestApiContext restApiContext = componentGraph.getInstance(restApiClass);
+
+ assertFalse(restApiContext.getInjectableComponents().isEmpty());
+ assertThat(restApiContext.getInjectableComponents().size(), is(2));
+
+ container.shutdownConfigurer();
+ }
+
+ @Test
+ public void providers_are_destructed() {
+ writeBootstrapConfigs("id1", DestructableProvider.class);
+
+ ComponentDeconstructor deconstructor = new ComponentDeconstructor() {
+ @Override
+ public void deconstruct(Object component) {
+ if (component instanceof AbstractComponent) {
+ ((AbstractComponent) component).deconstruct();
+ ;
+ } else if (component instanceof Provider) {
+ ((Provider<?>) component).deconstruct();
+ }
+ }
+ };
+
+ Container container = newContainer(dirConfigSource, deconstructor);
+
+ ComponentGraph oldGraph = container.getNewComponentGraph();
+ DestructableEntity destructableEntity = oldGraph.getInstance(DestructableEntity.class);
+
+ writeBootstrapConfigs("id2", DestructableProvider.class);
+ container.reloadConfig(2);
+ container.getNewComponentGraph(oldGraph);
+
+ assertTrue(destructableEntity.deconstructed);
+ }
+
+ static class DestructableEntity {
+ private boolean deconstructed = false;
+ }
+
+ public static class DestructableProvider implements Provider<DestructableEntity> {
+ DestructableEntity instance = new DestructableEntity();
+
+ public DestructableEntity get() {
+ return instance;
+ }
+
+ public void deconstruct() {
+ assertFalse(instance.deconstructed);
+ instance.deconstructed = true;
+ }
+ }
+
+ public static class ComponentTakingConfig extends AbstractComponent {
+ private final TestConfig config;
+
+ public ComponentTakingConfig(TestConfig config) {
+ assertNotNull(config);
+ this.config = config;
+ }
+ }
+
+ public static class ComponentThrowingExceptionInConstructor {
+ public ComponentThrowingExceptionInConstructor() {
+ throw new RuntimeException("This component fails upon construction.");
+ }
+ }
+
+ public static class ComponentThrowingExceptionForMissingConfig extends AbstractComponent {
+ public ComponentThrowingExceptionForMissingConfig(IntConfig intConfig) {
+ fail("This component should never be created. Only used for tests where 'int' config is missing.");
+ }
+ }
+
+ public static class DestructableComponent extends AbstractComponent {
+ private boolean deconstructed = false;
+
+ @Override
+ public void deconstruct() {
+ deconstructed = true;
+ }
+ }
+
+ public static class TestDeconstructor implements ComponentDeconstructor {
+ @Override
+ public void deconstruct(Object component) {
+ if (component instanceof DestructableComponent) {
+ DestructableComponent vespaComponent = (DestructableComponent) component;
+ vespaComponent.deconstruct();
+ }
+ }
+ }
+
+ private static Container newContainer(DirConfigSource dirConfigSource,
+ ComponentDeconstructor deconstructor) {
+ return new Container(new CloudSubscriberFactory(dirConfigSource.configSource), dirConfigSource.configId(), deconstructor);
+ }
+
+ private static Container newContainer(DirConfigSource dirConfigSource) {
+ return newContainer(dirConfigSource, new TestDeconstructor());
+ }
+
+ private ComponentTakingConfig createComponentTakingConfig(ComponentGraph componentGraph) {
+ return componentGraph.getInstance(ComponentTakingConfig.class);
+ }
+}
diff --git a/container-di/src/test/java/com/yahoo/container/di/ContainerTestBase.java b/container-di/src/test/java/com/yahoo/container/di/ContainerTestBase.java
new file mode 100644
index 00000000000..79cb080dfa4
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/container/di/ContainerTestBase.java
@@ -0,0 +1,120 @@
+// 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.yahoo.component.ComponentSpecification;
+import com.yahoo.config.FileReference;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.container.di.CloudSubscriberFactory;
+import com.yahoo.container.di.ContainerTest.ComponentTakingConfig;
+import com.yahoo.container.di.componentgraph.core.ComponentGraph;
+import com.yahoo.container.di.osgi.BundleClasses;
+import org.junit.After;
+import org.junit.Before;
+import org.osgi.framework.Bundle;
+
+import java.util.Collection;
+import java.util.Set;
+
+/**
+ * @author Tony Vaagenes
+ * @author gjoranv
+ * @author ollivir
+ */
+public class ContainerTestBase {
+ private ComponentGraph componentGraph;
+ protected DirConfigSource dirConfigSource = null;
+
+ @Before
+ public void setup() {
+ dirConfigSource = new DirConfigSource("ContainerTest-");
+ }
+
+ @After
+ public void cleanup() {
+ dirConfigSource.cleanup();
+ }
+
+ @Before
+ public void createGraph() {
+ componentGraph = new ComponentGraph(0);
+ }
+
+ public void complete() {
+ try {
+ Container container = new Container(new CloudSubscriberFactory(dirConfigSource.configSource()), dirConfigSource.configId(),
+ new ContainerTest.TestDeconstructor(), new Osgi() {
+ @SuppressWarnings("unchecked")
+ @Override
+ public Class<Object> resolveClass(BundleInstantiationSpecification spec) {
+ try {
+ return (Class<Object>) Class.forName(spec.classId.getName());
+ } catch (ClassNotFoundException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ @Override
+ public BundleClasses getBundleClasses(ComponentSpecification bundle, Set<String> packagesToScan) {
+ throw new UnsupportedOperationException("getBundleClasses not supported");
+ }
+
+ @Override
+ public void useBundles(Collection<FileReference> bundles) {
+ }
+
+ @Override
+ public Bundle getBundle(ComponentSpecification spec) {
+ throw new UnsupportedOperationException("getBundle not supported.");
+ }
+ });
+ componentGraph = container.getNewComponentGraph(componentGraph, Guice.createInjector(), false);
+ } catch (Exception e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ public <T> T getInstance(Class<T> componentClass) {
+ return componentGraph.getInstance(componentClass);
+ }
+
+ protected void writeBootstrapConfigs(ComponentEntry... componentEntries) {
+ dirConfigSource.writeConfig("bundles", "");
+ StringBuilder components = new StringBuilder();
+ for (int i = 0; i < componentEntries.length; i++) {
+ components.append(componentEntries[i].asConfig(i));
+ components.append('\n');
+ }
+ dirConfigSource.writeConfig("components", String.format("components[%s]\n%s", componentEntries.length, components));
+ }
+
+ protected void writeBootstrapConfigs(String componentId, Class<?> classId) {
+ writeBootstrapConfigs(new ComponentEntry(componentId, classId));
+ }
+
+ protected void writeBootstrapConfigs(String componentId) {
+ writeBootstrapConfigs(componentId, ComponentTakingConfig.class);
+ }
+
+ protected void writeBootstrapConfigs() {
+ writeBootstrapConfigs(ComponentTakingConfig.class.getName(), ComponentTakingConfig.class);
+ }
+
+ protected class ComponentEntry {
+ private final String componentId;
+ private final Class<?> classId;
+
+ ComponentEntry(String componentId, Class<?> classId) {
+ this.componentId = componentId;
+ this.classId = classId;
+ }
+
+ String asConfig(int position) {
+ return "<config>\n" + //
+ "components[" + position + "].id \"" + componentId + "\"\n" + //
+ "components[" + position + "].classId \"" + classId.getName() + "\"\n" + //
+ "components[" + position + "].configId \"" + dirConfigSource.configId() + "\"\n" + //
+ "</config>";
+ }
+ }
+}
diff --git a/container-di/src/test/java/com/yahoo/container/di/DirConfigSource.java b/container-di/src/test/java/com/yahoo/container/di/DirConfigSource.java
new file mode 100644
index 00000000000..ec937a1a4ef
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/container/di/DirConfigSource.java
@@ -0,0 +1,69 @@
+// 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.subscription.ConfigSource;
+import com.yahoo.config.subscription.ConfigSourceSet;
+import org.junit.rules.TemporaryFolder;
+
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Random;
+
+/**
+ * @author Tony Vaagenes
+ * @author gjoranv
+ * @author ollivir
+ */
+public class DirConfigSource {
+ private final TemporaryFolder tempFolder = createTemporaryFolder();
+ public final ConfigSource configSource;
+
+ public DirConfigSource(String testSourcePrefix) {
+ this.configSource = new ConfigSourceSet(testSourcePrefix + new Random().nextLong());
+ }
+
+ public void writeConfig(String name, String contents) {
+ File file = new File(tempFolder.getRoot(), name + ".cfg");
+ if (!file.exists()) {
+ try {
+ file.createNewFile();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ printFile(file, contents + "\n");
+ }
+
+ public String configId() {
+ return "dir:" + tempFolder.getRoot().getPath();
+ }
+
+ public ConfigSource configSource() {
+ return configSource;
+ }
+
+ public void cleanup() {
+ tempFolder.delete();
+ }
+
+ private static void printFile(File f, String content) {
+ try (OutputStream out = new FileOutputStream(f)) {
+ out.write(content.getBytes("UTF-8"));
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+ private static TemporaryFolder createTemporaryFolder() {
+ TemporaryFolder folder = new TemporaryFolder();
+ try {
+ folder.create();
+ } catch (IOException e) {
+ throw new RuntimeException(e);
+ }
+ return folder;
+ }
+}
diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java
new file mode 100644
index 00000000000..1bf0894a745
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.java
@@ -0,0 +1,674 @@
+// 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.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import com.yahoo.collections.Pair;
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.config.subscription.ConfigGetter;
+import com.yahoo.config.test.Test2Config;
+import com.yahoo.config.test.TestConfig;
+import com.yahoo.container.di.Osgi;
+import com.yahoo.container.di.componentgraph.Provider;
+import com.yahoo.container.di.config.JerseyBundlesConfig;
+import com.yahoo.container.di.config.JerseyInjectionConfig;
+import com.yahoo.container.di.config.RestApiContext;
+import com.yahoo.vespa.config.ConfigKey;
+import org.junit.Test;
+
+import java.lang.annotation.Annotation;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Set;
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.function.Supplier;
+
+import static com.yahoo.container.di.componentgraph.core.ComponentGraph.isBindingAnnotation;
+import static org.hamcrest.CoreMatchers.containsString;
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.notNullValue;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+/**
+ * @author gjoranv
+ * @author tonytv
+ * @author ollivir
+ */
+public class ComponentGraphTest {
+ public static class ConfigMap extends HashMap<ConfigKey<? extends ConfigInstance>, ConfigInstance> {
+ public ConfigMap() {
+ super();
+ }
+
+ public <T extends ConfigInstance> ConfigMap add(Class<T> clazz, String configId) {
+ ConfigKey<T> key = new ConfigKey<>(clazz, configId);
+ put(key, ConfigGetter.getConfig(key.getConfigClass(), key.getConfigId()));
+ return this;
+ }
+
+ public static <T extends ConfigInstance> ConfigMap newMap(Class<T> clazz, String configId) {
+ ConfigMap ret = new ConfigMap();
+ ret.add(clazz, configId);
+ return ret;
+ }
+ }
+
+ @Test
+ public void component_taking_config_can_be_instantiated() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ String configId = "raw:stringVal \"test-value\"";
+ Node componentNode = mockComponentNode(ComponentTakingConfig.class, configId);
+
+ componentGraph.add(componentNode);
+ componentGraph.complete();
+ componentGraph.setAvailableConfigs(ConfigMap.newMap(TestConfig.class, configId));
+
+ ComponentTakingConfig instance = componentGraph.getInstance(ComponentTakingConfig.class);
+ assertNotNull(instance);
+ assertThat(instance.config.stringVal(), is("test-value"));
+ }
+
+ @Test
+ public void component_can_be_injected_into_another_component() {
+ Node injectedComponent = mockComponentNode(SimpleComponent.class);
+ Node targetComponent = mockComponentNode(ComponentTakingComponent.class);
+ targetComponent.inject(injectedComponent);
+
+ Node destroyGlobalLookupComponent = mockComponentNode(SimpleComponent.class);
+
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(injectedComponent);
+ componentGraph.add(targetComponent);
+ componentGraph.add(destroyGlobalLookupComponent);
+ componentGraph.complete();
+
+ ComponentTakingComponent instance = componentGraph.getInstance(ComponentTakingComponent.class);
+ assertNotNull(instance);
+ }
+
+ @Test
+ public void all_components_of_a_type_can_be_injected() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(SimpleComponent.class));
+ componentGraph.add(mockComponentNode(SimpleComponent.class));
+ componentGraph.add(mockComponentNode(SimpleDerivedComponent.class));
+ componentGraph.add(mockComponentNode(ComponentTakingAllSimpleComponents.class));
+ componentGraph.complete();
+
+ ComponentTakingAllSimpleComponents instance = componentGraph.getInstance(ComponentTakingAllSimpleComponents.class);
+ assertThat(instance.simpleComponents.allComponents().size(), is(3));
+ }
+
+ @Test
+ public void empty_component_registry_can_be_injected() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(ComponentTakingAllSimpleComponents.class));
+ componentGraph.complete();
+
+ ComponentTakingAllSimpleComponents instance = componentGraph.getInstance(ComponentTakingAllSimpleComponents.class);
+ assertThat(instance.simpleComponents.allComponents().size(), is(0));
+ }
+
+ @Test
+ public void component_registry_with_wildcard_upper_bound_can_be_injected() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(SimpleComponent.class));
+ componentGraph.add(mockComponentNode(SimpleDerivedComponent.class));
+ componentGraph.add(mockComponentNode(ComponentTakingAllSimpleComponentsUpperBound.class));
+ componentGraph.complete();
+
+ ComponentTakingAllSimpleComponentsUpperBound instance = componentGraph
+ .getInstance(ComponentTakingAllSimpleComponentsUpperBound.class);
+ assertThat(instance.simpleComponents.allComponents().size(), is(2));
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void require_exception_when_injecting_registry_with_unknown_type_variable() {
+ @SuppressWarnings("rawtypes")
+ Class<ComponentTakingAllComponentsWithTypeVariable> clazz = ComponentTakingAllComponentsWithTypeVariable.class;
+
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(SimpleComponent.class));
+ componentGraph.add(mockComponentNode(SimpleDerivedComponent.class));
+ componentGraph.add(mockComponentNode(clazz));
+ componentGraph.complete();
+
+ componentGraph.getInstance(clazz);
+ }
+
+ @Test
+ public void components_are_shared() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(SimpleComponent.class));
+ componentGraph.complete();
+
+ SimpleComponent instance1 = componentGraph.getInstance(SimpleComponent.class);
+ SimpleComponent instance2 = componentGraph.getInstance(SimpleComponent.class);
+ assertThat(instance1, sameInstance(instance2));
+ }
+
+ @Test
+ public void singleton_components_can_be_injected() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ String configId = "raw:stringVal \"test-value\"";
+
+ componentGraph.add(mockComponentNode(ComponentTakingComponent.class));
+ componentGraph.add(mockComponentNode(ComponentTakingConfig.class, configId));
+ componentGraph.add(mockComponentNode(SimpleComponent2.class));
+ componentGraph.complete();
+ componentGraph.setAvailableConfigs(ConfigMap.newMap(TestConfig.class, configId));
+
+ ComponentTakingComponent instance = componentGraph.getInstance(ComponentTakingComponent.class);
+ ComponentTakingConfig injected = (ComponentTakingConfig) instance.injectedComponent;
+ assertThat(injected.config.stringVal(), is("test-value"));
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void require_error_when_multiple_components_match_a_singleton_dependency() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(SimpleDerivedComponent.class));
+ componentGraph.add(mockComponentNode(SimpleComponent.class));
+ componentGraph.add(mockComponentNode(ComponentTakingComponent.class));
+ componentGraph.complete();
+ }
+
+ @Test
+ public void named_component_can_be_injected() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(SimpleComponent.class));
+ componentGraph.add(mockComponentNode(SimpleComponent.class, Names.named("named-test")));
+ componentGraph.add(mockComponentNode(ComponentTakingNamedComponent.class));
+ componentGraph.complete();
+ }
+
+ @Test
+ public void config_keys_can_be_retrieved() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(ComponentTakingConfig.class, "raw:stringVal \"component1\""));
+ componentGraph.add(mockComponentNode(ComponentTakingConfig.class, "raw:stringVal \"component2\""));
+ componentGraph.add(new ComponentRegistryNode(ComponentTakingConfig.class));
+ componentGraph.complete();
+
+ Set<ConfigKey<? extends ConfigInstance>> configKeys = componentGraph.configKeys();
+ assertThat(configKeys.size(), is(2));
+
+ configKeys.forEach(key -> {
+ assertThat(key.getConfigClass(), equalTo(TestConfig.class));
+ assertThat(key.getConfigId(), containsString("component"));
+ });
+ }
+
+ @Test
+ public void providers_can_be_instantiated() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(ExecutorProvider.class));
+ componentGraph.complete();
+
+ assertNotNull(componentGraph.getInstance(Executor.class));
+ }
+
+ @Test
+ public void providers_can_be_inherited() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(DerivedExecutorProvider.class));
+ componentGraph.complete();
+
+ assertNotNull(componentGraph.getInstance(Executor.class));
+ }
+
+ @Test
+ public void providers_can_deliver_a_new_instance_for_each_component() {
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNode(NewIntProvider.class));
+ componentGraph.complete();
+
+ Integer instance1 = componentGraph.getInstance(Integer.class);
+ Integer instance2 = componentGraph.getInstance(Integer.class);
+ assertThat(instance1, not(equalTo(instance2)));
+ }
+
+ @Test
+ public void providers_can_be_injected_explicitly() {
+ ComponentGraph componentGraph = new ComponentGraph();
+
+ Node componentTakingExecutor = mockComponentNode(ComponentTakingExecutor.class);
+ Node executorProvider = mockComponentNode(ExecutorProvider.class);
+ componentTakingExecutor.inject(executorProvider);
+
+ componentGraph.add(executorProvider);
+ componentGraph.add(mockComponentNode(ExecutorProvider.class));
+
+ componentGraph.add(componentTakingExecutor);
+
+ componentGraph.complete();
+ assertNotNull(componentGraph.getInstance(ComponentTakingExecutor.class));
+ }
+
+ @Test
+ public void global_providers_can_be_injected() {
+ ComponentGraph componentGraph = new ComponentGraph();
+
+ componentGraph.add(mockComponentNode(ComponentTakingExecutor.class));
+ componentGraph.add(mockComponentNode(ExecutorProvider.class));
+ componentGraph.add(mockComponentNode(IntProvider.class));
+ componentGraph.complete();
+
+ assertNotNull(componentGraph.getInstance(ComponentTakingExecutor.class));
+ }
+
+ @Test(expected = RuntimeException.class)
+ public void throw_if_multiple_global_providers_exist() {
+ ComponentGraph componentGraph = new ComponentGraph();
+
+ componentGraph.add(mockComponentNode(ExecutorProvider.class));
+ componentGraph.add(mockComponentNode(ExecutorProvider.class));
+ componentGraph.add(mockComponentNode(ComponentTakingExecutor.class));
+ componentGraph.complete();
+ }
+
+ @Test
+ public void provider_is_not_used_when_component_of_provided_class_exists() {
+ ComponentGraph componentGraph = new ComponentGraph();
+
+ componentGraph.add(mockComponentNode(SimpleComponent.class));
+ componentGraph.add(mockComponentNode(SimpleComponentProviderThatThrows.class));
+ componentGraph.add(mockComponentNode(ComponentTakingComponent.class));
+ componentGraph.complete();
+
+ SimpleComponent injectedComponent = componentGraph.getInstance(ComponentTakingComponent.class).injectedComponent;
+ assertNotNull(injectedComponent);
+ }
+
+ //TODO: move
+ @Test
+ public void check_if_annotation_is_a_binding_annotation() {
+ assertTrue(isBindingAnnotation(Names.named("name")));
+ assertFalse(isBindingAnnotation(Named.class.getAnnotations()[0]));
+ }
+
+ @Test
+ public void cycles_gives_exception() {
+ ComponentGraph componentGraph = new ComponentGraph();
+
+ Node node1 = mockComponentNode(ComponentCausingCycle.class);
+ Node node2 = mockComponentNode(ComponentCausingCycle.class);
+
+ node1.inject(node2);
+ node2.inject(node1);
+
+ componentGraph.add(node1);
+ componentGraph.add(node2);
+
+ try {
+ componentGraph.complete();
+ fail("Cycle exception expected.");
+ } catch (Throwable e) {
+ assertThat(e.getMessage(), containsString("cycle"));
+ }
+ }
+
+ @Test(expected = IllegalArgumentException.class)
+ public void abstract_classes_are_rejected() {
+ new ComponentNode(ComponentId.fromString("Test"), "", AbstractClass.class);
+ }
+
+ @Test
+ public void inject_constructor_is_preferred() {
+ assertThatComponentCanBeCreated(ComponentWithInjectConstructor.class);
+ }
+
+ @Test
+ public void constructor_with_most_parameters_is_preferred() {
+ assertThatComponentCanBeCreated(ComponentWithMultipleConstructors.class);
+ }
+
+ public void assertThatComponentCanBeCreated(Class<?> clazz) {
+ ComponentGraph componentGraph = new ComponentGraph();
+ String configId = "raw:stringVal \"dummy\"";
+
+ componentGraph.add(mockComponentNode(clazz, configId));
+ componentGraph.complete();
+
+ componentGraph.setAvailableConfigs(ConfigMap.newMap(TestConfig.class, configId).add(Test2Config.class, configId));
+
+ assertNotNull(componentGraph.getInstance(clazz));
+ }
+
+ @Test
+ public void require_fallback_to_child_injector() {
+ ComponentGraph componentGraph = new ComponentGraph();
+
+ componentGraph.add(mockComponentNode(ComponentTakingExecutor.class));
+
+ componentGraph.complete(singletonExecutorInjector);
+ assertNotNull(componentGraph.getInstance(ComponentTakingExecutor.class));
+ }
+
+ @Test
+ public void child_injector_can_inject_multiple_instances_for_same_key() {
+ Pair<Integer, Pair<Executor, Executor>> graph = buildGraphWithChildInjector(Executors::newSingleThreadExecutor);
+ int graphSize = graph.getFirst();
+ Executor executorA = graph.getSecond().getFirst();
+ Executor executorB = graph.getSecond().getSecond();
+
+ assertThat(graphSize, is(4));
+ assertThat(executorA, not(sameInstance(executorB)));
+ }
+
+ @Test
+ public void components_injected_via_child_injector_can_be_shared() {
+ Executor commonExecutor = Executors.newSingleThreadExecutor();
+ Pair<Integer, Pair<Executor, Executor>> graph = buildGraphWithChildInjector(() -> commonExecutor);
+ int graphSize = graph.getFirst();
+ Executor executorA = graph.getSecond().getFirst();
+ Executor executorB = graph.getSecond().getSecond();
+
+ assertThat(graphSize, is(3));
+ assertThat(executorA, sameInstance(executorB));
+ }
+
+ private Pair<Integer, Pair<Executor, Executor>> buildGraphWithChildInjector(Supplier<Executor> executorProvider) {
+ Injector childInjector = Guice.createInjector(new AbstractModule() {
+ @Override
+ public void configure() {
+ bind(Executor.class).toProvider(executorProvider::get);
+ }
+ });
+
+ ComponentGraph componentGraph = new ComponentGraph();
+
+ Key<ComponentTakingExecutor> keyA = Key.get(ComponentTakingExecutor.class, Names.named("A"));
+ Key<ComponentTakingExecutor> keyB = Key.get(ComponentTakingExecutor.class, Names.named("B"));
+
+ componentGraph.add(mockComponentNode(keyA));
+ componentGraph.add(mockComponentNode(keyB));
+
+ componentGraph.complete(childInjector);
+
+ return new Pair<>(componentGraph.size(),
+ new Pair<>(componentGraph.getInstance(keyA).executor, componentGraph.getInstance(keyB).executor));
+ }
+
+ @Test
+ public void providers_can_be_reused() {
+
+ ComponentGraph oldGraph = createReusingGraph();
+ Executor executor = oldGraph.getInstance(Executor.class);
+
+ ComponentGraph newGraph = createReusingGraph();
+ newGraph.reuseNodes(oldGraph);
+
+ Executor newExecutor = newGraph.getInstance(Executor.class);
+ assertThat(executor, sameInstance(newExecutor));
+ }
+
+ private ComponentGraph createReusingGraph() {
+ ComponentGraph graph = new ComponentGraph();
+ graph.add(mockComponentNodeWithId(ExecutorProvider.class, "dummyId"));
+ graph.complete();
+ graph.setAvailableConfigs(Collections.emptyMap());
+ return graph;
+ }
+
+ @Test
+ public void component_id_can_be_injected() {
+ String componentId = "myId:1.2@namespace";
+
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(mockComponentNodeWithId(ComponentTakingComponentId.class, componentId));
+ componentGraph.complete();
+
+ assertThat(componentGraph.getInstance(ComponentTakingComponentId.class).componentId, is(ComponentId.fromString(componentId)));
+ }
+
+ @Test
+ public void rest_api_context_can_be_instantiated() {
+ String configId = "raw:\"\"";
+
+ Class<RestApiContext> clazz = RestApiContext.class;
+ JerseyNode jerseyNode = new JerseyNode(uniqueComponentId(clazz.getName()), configId, clazz, new Osgi() {
+ });
+
+ ComponentGraph componentGraph = new ComponentGraph();
+ componentGraph.add(jerseyNode);
+ componentGraph.complete();
+
+ componentGraph
+ .setAvailableConfigs(ConfigMap.newMap(JerseyBundlesConfig.class, configId).add(JerseyInjectionConfig.class, configId));
+
+ RestApiContext restApiContext = componentGraph.getInstance(clazz);
+ assertNotNull(restApiContext);
+ assertThat(restApiContext.getBundles().size(), is(0));
+ }
+
+ //Note that all Components must be defined in a static context,
+ //otherwise their constructor will take the outer class as the first parameter.
+ private static int counter = 0;
+
+ public static class SimpleComponent extends AbstractComponent {
+ }
+
+ public static class SimpleComponent2 extends AbstractComponent {
+ }
+
+ public static class SimpleDerivedComponent extends SimpleComponent {
+ }
+
+ public static class ComponentTakingConfig extends SimpleComponent {
+ private final TestConfig config;
+
+ public ComponentTakingConfig(TestConfig config) {
+ assertThat(config, notNullValue());
+ this.config = config;
+ }
+ }
+
+ public static class ComponentTakingComponent extends AbstractComponent {
+ private final SimpleComponent injectedComponent;
+
+ public ComponentTakingComponent(SimpleComponent injectedComponent) {
+ assertThat(injectedComponent, notNullValue());
+ this.injectedComponent = injectedComponent;
+ }
+ }
+
+ @SuppressWarnings("unused")
+ public static class ComponentTakingConfigAndComponent extends AbstractComponent {
+ private final TestConfig config;
+ private final SimpleComponent simpleComponent;
+
+ public ComponentTakingConfigAndComponent(TestConfig config, SimpleComponent injectedComponent) {
+ assertThat(config, notNullValue());
+ assertThat(injectedComponent, notNullValue());
+ this.config = config;
+ this.simpleComponent = injectedComponent;
+ }
+ }
+
+ public static class ComponentTakingAllSimpleComponents extends AbstractComponent {
+ public final ComponentRegistry<SimpleComponent> simpleComponents;
+
+ public ComponentTakingAllSimpleComponents(ComponentRegistry<SimpleComponent> simpleComponents) {
+ assertThat(simpleComponents, notNullValue());
+ this.simpleComponents = simpleComponents;
+ }
+ }
+
+ public static class ComponentTakingAllSimpleComponentsUpperBound extends AbstractComponent {
+ private final ComponentRegistry<? extends SimpleComponent> simpleComponents;
+
+ public ComponentTakingAllSimpleComponentsUpperBound(ComponentRegistry<? extends SimpleComponent> simpleComponents) {
+ assertThat(simpleComponents, notNullValue());
+ this.simpleComponents = simpleComponents;
+ }
+ }
+
+ public static class ComponentTakingAllComponentsWithTypeVariable<COMPONENT extends AbstractComponent> extends AbstractComponent {
+ public ComponentTakingAllComponentsWithTypeVariable(ComponentRegistry<COMPONENT> simpleComponents) {
+ assertThat(simpleComponents, notNullValue());
+ }
+ }
+
+ public static class ComponentTakingNamedComponent extends AbstractComponent {
+ public ComponentTakingNamedComponent(@Named("named-test") SimpleComponent injectedComponent) {
+ assertThat(injectedComponent, notNullValue());
+ }
+ }
+
+ public static class ComponentCausingCycle extends AbstractComponent {
+ public ComponentCausingCycle(ComponentCausingCycle component) {
+ }
+ }
+
+ public static class SimpleComponentProviderThatThrows implements Provider<SimpleComponent> {
+ public SimpleComponent get() {
+ throw new AssertionError("Should never be called.");
+ }
+
+ public void deconstruct() {
+ }
+ }
+
+ public static class ExecutorProvider implements Provider<Executor> {
+ private Executor executor = Executors.newSingleThreadExecutor();
+
+ public Executor get() {
+ return executor;
+ }
+
+ public void deconstruct() {
+ /*TODO */ }
+ }
+
+ public static class DerivedExecutorProvider extends ExecutorProvider {
+ }
+
+ public static class IntProvider implements Provider<Integer> {
+ public Integer get() {
+ throw new AssertionError("Should never be called.");
+ }
+
+ public void deconstruct() {
+ }
+ }
+
+ public static class NewIntProvider implements Provider<Integer> {
+ int i = 0;
+
+ public Integer get() {
+ i++;
+ return i;
+ }
+
+ public void deconstruct() {
+ }
+ }
+
+ public static class ComponentTakingExecutor extends AbstractComponent {
+ private final Executor executor;
+
+ public ComponentTakingExecutor(Executor executor) {
+ assertThat(executor, notNullValue());
+ this.executor = executor;
+ }
+ }
+
+ public static class ComponentWithInjectConstructor {
+ public ComponentWithInjectConstructor(TestConfig c, Test2Config c2) {
+ throw new RuntimeException("Should not be called");
+ }
+
+ @Inject
+ public ComponentWithInjectConstructor(Test2Config c) {
+ }
+ }
+
+ public static class ComponentWithMultipleConstructors {
+ private ComponentWithMultipleConstructors(int dummy) {
+ }
+
+ public ComponentWithMultipleConstructors() {
+ this(0);
+ throw new RuntimeException("Should not be called");
+ }
+
+ public ComponentWithMultipleConstructors(TestConfig c, Test2Config c2) {
+ this(0);
+ }
+
+ public ComponentWithMultipleConstructors(Test2Config c) {
+ this();
+ }
+ }
+
+ public static class ComponentTakingComponentId {
+ private final ComponentId componentId;
+
+ public ComponentTakingComponentId(ComponentId componentId) {
+ this.componentId = componentId;
+ }
+ }
+
+ public static ComponentId uniqueComponentId(String className) {
+ counter += 1;
+ return ComponentId.fromString(className + counter);
+ }
+
+ public static Node mockComponentNode(Key<?> key) {
+ return mockComponentNode(key.getTypeLiteral().getRawType(), "", key.getAnnotation());
+ }
+
+ public static Node mockComponentNode(Class<?> clazz, String configId, Annotation key) {
+ return new ComponentNode(uniqueComponentId(clazz.getName()), configId, clazz, key);
+ }
+
+ public static Node mockComponentNode(Class<?> clazz, String configId) {
+ return new ComponentNode(uniqueComponentId(clazz.getName()), configId, clazz, null);
+ }
+
+ public static Node mockComponentNode(Class<?> clazz, Annotation key) {
+ return new ComponentNode(uniqueComponentId(clazz.getName()), "", clazz, key);
+ }
+
+ public static Node mockComponentNode(Class<?> clazz) {
+ return new ComponentNode(uniqueComponentId(clazz.getName()), "", clazz, null);
+ }
+
+ public static Node mockComponentNodeWithId(Class<?> clazz, String componentId, String configId /*= ""*/, Annotation key /*= null*/) {
+ return new ComponentNode(ComponentId.fromString(componentId), configId, clazz, key);
+ }
+
+ public static Node mockComponentNodeWithId(Class<?> clazz, String componentId, String configId /*= ""*/) {
+ return new ComponentNode(ComponentId.fromString(componentId), configId, clazz, null);
+ }
+
+ public static Node mockComponentNodeWithId(Class<?> clazz, String componentId) {
+ return new ComponentNode(ComponentId.fromString(componentId), "", clazz, null);
+ }
+
+ public static Injector singletonExecutorInjector = Guice.createInjector(new AbstractModule() {
+ @Override
+ public void configure() {
+ bind(Executor.class).toInstance(Executors.newSingleThreadExecutor());
+ }
+ });
+
+ public static abstract class AbstractClass {
+ }
+}
diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java
new file mode 100644
index 00000000000..f30f9260830
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.java
@@ -0,0 +1,68 @@
+// 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.container.bundle.MockBundle;
+import com.yahoo.container.di.config.RestApiContext;
+import com.yahoo.container.di.osgi.OsgiUtil;
+import org.junit.Test;
+import org.osgi.framework.wiring.BundleWiring;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.Matchers.contains;
+import static org.hamcrest.Matchers.containsInAnyOrder;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ * @author ollivir
+ */
+
+public class JerseyNodeTest {
+ private MockBundle bundle;
+ private List<String> bundleClasses;
+ private final Map<String, String> resources;
+
+ public JerseyNodeTest() {
+ resources = new HashMap<>();
+ resources.put("com/foo", "com/foo/Foo.class");
+ resources.put("com/bar", "com/bar/Bar.class");
+ bundle = new MockBundle() {
+ @Override
+ public Collection<String> listResources(String path, String ignored, int options) {
+ if ((options & BundleWiring.LISTRESOURCES_RECURSE) != 0 && path.equals("/")) {
+ return resources.values();
+ } else {
+ return Collections.singleton(resources.get(path));
+ }
+ }
+ };
+ bundleClasses = new ArrayList<>(resources.values());
+ }
+
+ @Test
+ public void all_bundle_entries_are_returned_when_no_packages_are_given() {
+ Collection<String> entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Collections.emptySet());
+ assertThat(entries, containsInAnyOrder(bundleClasses.toArray()));
+ }
+
+ @Test
+ public void only_bundle_entries_from_the_given_packages_are_returned() {
+ Collection<String> entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Collections.singleton("com.foo"));
+ assertThat(entries, contains(resources.get("com/foo")));
+ }
+
+ @Test
+ public void bundle_info_is_initialized() {
+ RestApiContext.BundleInfo bundleInfo = JerseyNode.createBundleInfo(bundle, Collections.emptyList());
+ assertThat(bundleInfo.symbolicName, is(bundle.getSymbolicName()));
+ assertThat(bundleInfo.version, is(bundle.getVersion()));
+ assertThat(bundleInfo.fileLocation, is(bundle.getLocation()));
+ }
+}
diff --git a/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.java b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.java
new file mode 100644
index 00000000000..e61e90cd718
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.java
@@ -0,0 +1,254 @@
+// 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.provider.ComponentRegistry;
+import com.yahoo.config.subscription.ConfigGetter;
+import com.yahoo.config.test.TestConfig;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingAllSimpleComponents;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingComponent;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingConfig;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingConfigAndComponent;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ComponentTakingExecutor;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.ExecutorProvider;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent;
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.SimpleComponent2;
+import com.yahoo.vespa.config.ConfigKey;
+import org.junit.Test;
+
+import java.util.Collections;
+import java.util.concurrent.Executor;
+import java.util.function.Function;
+import java.util.function.Supplier;
+
+import static org.hamcrest.CoreMatchers.equalTo;
+import static org.hamcrest.CoreMatchers.is;
+import static org.hamcrest.CoreMatchers.not;
+import static org.hamcrest.CoreMatchers.sameInstance;
+import static org.junit.Assert.assertThat;
+
+/**
+ * @author gjoranv
+ * @author Tony Vaagenes
+ * @author ollivir
+ */
+public class ReuseComponentsTest {
+ @Test
+ public void require_that_component_is_reused_when_componentNode_is_unmodified() {
+ reuseAndTest(SimpleComponent.class, SimpleComponent.class);
+ reuseAndTest(ExecutorProvider.class, Executor.class);
+ }
+
+ private <T> void reuseAndTest(Class<?> classToRegister, Class<T> classToLookup) {
+ ComponentGraph graph = buildGraphAndSetNoConfigs(classToRegister);
+ T instance = getComponent(graph, classToLookup);
+
+ ComponentGraph newGraph = buildGraphAndSetNoConfigs(classToRegister);
+ newGraph.reuseNodes(graph);
+ T instance2 = getComponent(newGraph, classToLookup);
+
+ assertThat(instance2, sameInstance(instance));
+ }
+
+ @Test(expected = IllegalStateException.class)
+ public void require_that_component_is_not_reused_when_class_is_changed() {
+ ComponentGraph graph = buildGraphAndSetNoConfigs(SimpleComponent.class);
+ SimpleComponent instance = getComponent(graph, SimpleComponent.class);
+
+ ComponentGraph newGraph = buildGraphAndSetNoConfigs(SimpleComponent2.class);
+ newGraph.reuseNodes(graph);
+ SimpleComponent2 instance2 = getComponent(newGraph, SimpleComponent2.class);
+
+ assertThat(instance2.getId(), is(instance.getId()));
+ @SuppressWarnings("unused")
+ SimpleComponent throwsException = getComponent(newGraph, SimpleComponent.class);
+ }
+
+ @Test
+ public void require_that_component_is_not_reused_when_config_is_changed() {
+ Class<ComponentTakingConfig> componentClass = ComponentTakingConfig.class;
+
+ ComponentGraph graph = buildGraph(componentClass);
+ graph.setAvailableConfigs(Collections.singletonMap(new ConfigKey<>(TestConfig.class, "component"),
+ ConfigGetter.getConfig(TestConfig.class, "raw: stringVal \"oldConfig\"")));
+ ComponentTakingConfig instance = getComponent(graph, componentClass);
+
+ ComponentGraph newGraph = buildGraph(componentClass);
+ newGraph.setAvailableConfigs(Collections.singletonMap(new ConfigKey<>(TestConfig.class, "component"),
+ ConfigGetter.getConfig(TestConfig.class, "raw: stringVal \"newConfig\"")));
+ newGraph.reuseNodes(graph);
+ ComponentTakingConfig instance2 = getComponent(newGraph, componentClass);
+
+ assertThat(instance2, not(sameInstance(instance)));
+ }
+
+ @Test
+ public void require_that_component_is_not_reused_when_injected_component_is_changed() {
+ Function<String, ComponentGraph> buildGraph = config -> {
+ ComponentGraph graph = new ComponentGraph();
+
+ ComponentNode rootComponent = mockComponentNode(ComponentTakingComponent.class, "root_component");
+
+ String configId = "componentTakingConfigId";
+ ComponentNode injectedComponent = mockComponentNode(ComponentTakingConfig.class, "injected_component", configId);
+
+ rootComponent.inject(injectedComponent);
+
+ graph.add(rootComponent);
+ graph.add(injectedComponent);
+
+ graph.complete();
+ graph.setAvailableConfigs(Collections.singletonMap(new ConfigKey<>(TestConfig.class, configId),
+ ConfigGetter.getConfig(TestConfig.class, "raw: stringVal \"" + config + "\"")));
+
+ return graph;
+ };
+
+ ComponentGraph oldGraph = buildGraph.apply("oldGraph");
+ ComponentTakingComponent oldInstance = getComponent(oldGraph, ComponentTakingComponent.class);
+
+ ComponentGraph newGraph = buildGraph.apply("newGraph");
+ newGraph.reuseNodes(oldGraph);
+ ComponentTakingComponent newInstance = getComponent(newGraph, ComponentTakingComponent.class);
+
+ assertThat(newInstance, not(sameInstance(oldInstance)));
+ }
+
+ @Test
+ public void require_that_component_is_not_reused_when_injected_component_registry_has_one_component_removed() {
+ Function<Boolean, ComponentGraph> buildGraph = useBothInjectedComponents -> {
+ ComponentGraph graph = new ComponentGraph();
+ graph.add(mockComponentNode(ComponentTakingAllSimpleComponents.class, "root_component"));
+
+ /* Below if-else has code duplication, but explicit ordering of the two components
+ * was necessary to reproduce erroneous behaviour in ComponentGraph.reuseNodes that
+ * occurred before ComponentRegistryNode got its own 'equals' implementation.
+ */
+ if (useBothInjectedComponents) {
+ graph.add(mockComponentNode(SimpleComponent.class, "injected_component2"));
+ graph.add(mockComponentNode(SimpleComponent.class, "injected_component1"));
+ } else {
+ graph.add(mockComponentNode(SimpleComponent.class, "injected_component1"));
+ }
+
+ graph.complete();
+ graph.setAvailableConfigs(Collections.emptyMap());
+ return graph;
+ };
+
+ ComponentGraph oldGraph = buildGraph.apply(true);
+ ComponentRegistry<SimpleComponent> oldSimpleComponentRegistry = getComponent(oldGraph, ComponentTakingAllSimpleComponents.class).simpleComponents;
+
+ ComponentGraph newGraph = buildGraph.apply(false);
+ newGraph.reuseNodes(oldGraph);
+ ComponentRegistry<SimpleComponent> newSimpleComponentRegistry = getComponent(newGraph, ComponentTakingAllSimpleComponents.class).simpleComponents;
+
+ assertThat(newSimpleComponentRegistry, not(sameInstance(oldSimpleComponentRegistry)));
+ }
+
+ @Test
+ public void require_that_injected_component_is_reused_even_when_dependent_component_is_changed() {
+ Function<String, ComponentGraph> buildGraph = config -> {
+ ComponentGraph graph = new ComponentGraph();
+
+ String configId = "componentTakingConfigAndComponent";
+ ComponentNode rootComponent = mockComponentNode(ComponentTakingConfigAndComponent.class, "root_component", configId);
+
+ ComponentNode injectedComponent = mockComponentNode(SimpleComponent.class, "injected_component");
+
+ rootComponent.inject(injectedComponent);
+
+ graph.add(rootComponent);
+ graph.add(injectedComponent);
+
+ graph.complete();
+ graph.setAvailableConfigs(Collections.singletonMap(new ConfigKey<>(TestConfig.class, configId),
+ ConfigGetter.getConfig(TestConfig.class, "raw: stringVal \"" + config + "\"")));
+
+ return graph;
+ };
+
+ ComponentGraph oldGraph = buildGraph.apply("oldGraph");
+ SimpleComponent oldInjectedComponent = getComponent(oldGraph, SimpleComponent.class);
+ ComponentTakingConfigAndComponent oldDependentComponent = getComponent(oldGraph, ComponentTakingConfigAndComponent.class);
+
+ ComponentGraph newGraph = buildGraph.apply("newGraph");
+ newGraph.reuseNodes(oldGraph);
+ SimpleComponent newInjectedComponent = getComponent(newGraph, SimpleComponent.class);
+ ComponentTakingConfigAndComponent newDependentComponent = getComponent(newGraph, ComponentTakingConfigAndComponent.class);
+
+ assertThat(newDependentComponent, not(sameInstance(oldDependentComponent)));
+ assertThat(newInjectedComponent, sameInstance(oldInjectedComponent));
+ }
+
+ @Test
+ public void require_that_node_depending_on_guice_node_is_reused() {
+ Supplier<ComponentGraph> makeGraph = () -> {
+ ComponentGraph graph = new ComponentGraph();
+ graph.add(mockComponentNode(ComponentTakingExecutor.class, "dummyId"));
+ graph.complete(ComponentGraphTest.singletonExecutorInjector);
+ graph.setAvailableConfigs(Collections.emptyMap());
+ return graph;
+ };
+
+ Function<ComponentGraph, ComponentTakingExecutor> componentRetriever = graph -> getComponent(graph, ComponentTakingExecutor.class);
+
+ ComponentGraph oldGraph = makeGraph.get();
+ componentRetriever.apply(oldGraph); // Ensure creation of GuiceNode
+ ComponentGraph newGraph = makeGraph.get();
+ newGraph.reuseNodes(oldGraph);
+ assertThat(componentRetriever.apply(oldGraph), sameInstance(componentRetriever.apply(newGraph)));
+ }
+
+ @Test
+ public void require_that_node_equals_only_checks_first_level_components_to_inject() {
+ Function<String, Node> createNodeWithInjectedNodeWithInjectedNode = indirectlyInjectedComponentId -> {
+ ComponentNode targetComponent = mockComponentNode(SimpleComponent.class, "target");
+ ComponentNode directlyInjectedComponent = mockComponentNode(SimpleComponent.class, "directlyInjected");
+ ComponentNode indirectlyInjectedComponent = mockComponentNode(SimpleComponent.class, indirectlyInjectedComponentId);
+ directlyInjectedComponent.inject(indirectlyInjectedComponent);
+ targetComponent.inject(directlyInjectedComponent);
+
+ completeNode(targetComponent);
+ completeNode(directlyInjectedComponent);
+ completeNode(indirectlyInjectedComponent);
+
+ return targetComponent;
+ };
+
+ Node targetNode1 = createNodeWithInjectedNodeWithInjectedNode.apply("indirectlyInjected_1");
+ Node targetNode2 = createNodeWithInjectedNodeWithInjectedNode.apply("indirectlyInjected_2");
+ assertThat(targetNode1, equalTo(targetNode2));
+ }
+
+ private void completeNode(ComponentNode node) {
+ node.setArguments(new Object[0]);
+ node.setAvailableConfigs(Collections.emptyMap());
+ }
+
+ private ComponentGraph buildGraph(Class<?> componentClass) {
+ String commonComponentId = "component";
+ ComponentGraph g = new ComponentGraph();
+ g.add(mockComponentNode(componentClass, commonComponentId, commonComponentId));
+ g.complete();
+ return g;
+ }
+
+ private ComponentGraph buildGraphAndSetNoConfigs(Class<?> componentClass) {
+ ComponentGraph g = buildGraph(componentClass);
+ g.setAvailableConfigs(Collections.emptyMap());
+ return g;
+ }
+
+ private static ComponentNode mockComponentNode(Class<?> clazz, String componentId, String configId) {
+ return new ComponentNode(new ComponentId(componentId), configId, clazz);
+ }
+
+ private static ComponentNode mockComponentNode(Class<?> clazz, String componentId) {
+ return mockComponentNode(clazz, componentId, "");
+ }
+
+ private static <T> T getComponent(ComponentGraph graph, Class<T> clazz) {
+ return graph.getInstance(clazz);
+ }
+}
diff --git a/container-di/src/test/java/demo/Base.java b/container-di/src/test/java/demo/Base.java
index b702bdcaddd..fbe779636e0 100644
--- a/container-di/src/test/java/demo/Base.java
+++ b/container-di/src/test/java/demo/Base.java
@@ -5,7 +5,6 @@ import com.google.inject.Guice;
import com.google.inject.Injector;
import com.yahoo.component.ComponentId;
import com.yahoo.config.ConfigInstance;
-import com.yahoo.container.di.ContainerTest;
import com.yahoo.container.di.componentgraph.core.ComponentGraph;
import com.yahoo.container.di.componentgraph.core.ComponentNode;
import com.yahoo.container.di.componentgraph.core.Node;
@@ -48,10 +47,9 @@ public class Base {
return componentGraph.getInstance(componentClass);
}
- @SuppressWarnings("unchecked")
public void complete() {
componentGraph.complete(injector);
- componentGraph.setAvailableConfigs(ContainerTest.convertMap(configs));
+ componentGraph.setAvailableConfigs(configs);
}
public void setInjector(Injector injector) {
diff --git a/container-di/src/test/java/demo/ContainerTestBase.java b/container-di/src/test/java/demo/ContainerTestBase.java
deleted file mode 100644
index 9c2415c3514..00000000000
--- a/container-di/src/test/java/demo/ContainerTestBase.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package demo;
-
-import com.google.inject.Guice;
-import com.yahoo.component.ComponentSpecification;
-import com.yahoo.container.bundle.BundleInstantiationSpecification;
-import com.yahoo.config.FileReference;
-import com.yahoo.container.di.CloudSubscriberFactory;
-import com.yahoo.container.di.Container;
-import com.yahoo.container.di.ContainerTest;
-import com.yahoo.container.di.Osgi;
-import com.yahoo.container.di.componentgraph.core.ComponentGraph;
-import org.junit.Before;
-import org.osgi.framework.Bundle;
-import scala.collection.immutable.Set;
-
-import java.util.Collection;
-
-/**
- * @author tonytv
- * @author gjoranv
- */
-public class ContainerTestBase extends ContainerTest {
- private ComponentGraph componentGraph;
-
- @Before
- public void createGraph() {
- componentGraph = new ComponentGraph(0);
- }
-
- public void complete() {
- try {
- Container container = new Container(
- new CloudSubscriberFactory(dirConfigSource().configSource()),
- dirConfigSource().configId(),
- new ContainerTest.TestDeconstructor(),
- new Osgi() {
- @SuppressWarnings("unchecked")
- @Override
- public Class<Object> resolveClass(BundleInstantiationSpecification spec) {
- try {
- return (Class<Object>) Class.forName(spec.classId.getName());
- } catch (ClassNotFoundException e) {
- throw new RuntimeException(e);
- }
- }
-
- @Override
- public BundleClasses getBundleClasses(ComponentSpecification bundle,
- Set<String> packagesToScan) {
- throw new UnsupportedOperationException("getBundleClasses not supported");
- }
-
- @Override
- public void useBundles(Collection<FileReference> bundles) {}
-
- @Override
- public Bundle getBundle(ComponentSpecification spec) {
- throw new UnsupportedOperationException("getBundle not supported.");
- }
- });
- componentGraph = container.getNewComponentGraph(componentGraph, Guice.createInjector(), false);
- } catch (Exception e) {
- throw new RuntimeException(e);
- }
- }
-
- public <T> T getInstance(Class<T> componentClass) {
- return componentGraph.getInstance(componentClass);
- }
-}
diff --git a/container-di/src/test/java/demo/DeconstructTest.java b/container-di/src/test/java/demo/DeconstructTest.java
index 1ec2fe17054..fd4848c5fa3 100644
--- a/container-di/src/test/java/demo/DeconstructTest.java
+++ b/container-di/src/test/java/demo/DeconstructTest.java
@@ -2,6 +2,7 @@
package demo;
import com.yahoo.container.di.ContainerTest;
+import com.yahoo.container.di.ContainerTestBase;
import org.junit.Test;
import static org.junit.Assert.assertTrue;
diff --git a/container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala b/container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala
deleted file mode 100644
index 7f1d9a73a82..00000000000
--- a/container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala
+++ /dev/null
@@ -1,107 +0,0 @@
-// Copyright 2017 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.test.{Bootstrap1Config, Bootstrap2Config, TestConfig}
-import com.yahoo.container.di.ConfigRetriever.{BootstrapConfigs, ComponentsConfigs}
-import com.yahoo.vespa.config.ConfigKey
-import org.hamcrest.CoreMatchers.{is, instanceOf => hamcrestInstanceOf}
-import org.hamcrest.Matcher
-import org.junit.Assert._
-import org.junit.{After, Before, Ignore, Test}
-
-import scala.reflect.ClassTag
-import scala.collection.JavaConverters._
-
-/**
- *
- * @author gjoranv
- * @author tonytv
- */
-class ConfigRetrieverTest {
-
- var dirConfigSource: DirConfigSource = null
-
- @Before def setup() {
- dirConfigSource = new DirConfigSource("ConfigRetrieverTest-")
- }
-
- @After def cleanup() { dirConfigSource.cleanup() }
-
- @Test
- def require_that_bootstrap_configs_come_first() {
- writeConfigs()
- val retriever = createConfigRetriever()
- val bootstrapConfigs = retriever.getConfigs(Set(), 0)
-
- assertThat(bootstrapConfigs, instanceOf[BootstrapConfigs])
- }
-
- @Test
- def require_that_components_comes_after_bootstrap() {
- writeConfigs()
- val retriever = createConfigRetriever()
- val bootstrapConfigs = retriever.getConfigs(Set(), 0)
-
- val testConfigKey = new ConfigKey(classOf[TestConfig], dirConfigSource.configId)
- val componentsConfigs = retriever.getConfigs(Set(testConfigKey), 0)
-
- componentsConfigs match {
- case ComponentsConfigs(configs) => assertThat(configs.size, is(3))
- case _ => fail("ComponentsConfigs has unexpected type: " + componentsConfigs)
- }
- }
-
- @Test
- def require_no_reconfig_when_restart_on_redeploy() {
- // TODO
- writeConfigs()
- val retriever = createConfigRetriever()
- val bootstrapConfigs = retriever.getConfigs(Set(), 0)
-
- val testConfigKey = new ConfigKey(classOf[TestConfig], dirConfigSource.configId)
- val componentsConfigs = retriever.getConfigsOnce(Set(testConfigKey), 0, true)
-
- componentsConfigs match {
- case Some(snapshot) => fail("Expected no configs")
- case _ => // ok
- }
- }
-
- @Test(expected = classOf[IllegalArgumentException])
- @Ignore
- def require_exception_upon_modified_components_keys_without_bootstrap() {
- writeConfigs()
- val retriever = createConfigRetriever()
- val testConfigKey = new ConfigKey(classOf[TestConfig], dirConfigSource.configId)
- val bootstrapConfigs = retriever.getConfigs(Set(), 0)
- val componentsConfigs = retriever.getConfigs(Set(testConfigKey), 0)
- retriever.getConfigs(Set(testConfigKey, new ConfigKey(classOf[TestConfig],"")), 0)
- }
-
- @Test
- def require_that_empty_components_keys_after_bootstrap_returns_components_configs() {
- writeConfigs()
- val retriever = createConfigRetriever()
- assertThat(retriever.getConfigs(Set(), 0), instanceOf[BootstrapConfigs])
- assertThat(retriever.getConfigs(Set(), 0), instanceOf[ComponentsConfigs])
- }
-
- def writeConfigs() {
- writeConfig("bootstrap1", """dummy "ignored" """")
- writeConfig("bootstrap2", """dummy "ignored" """")
- writeConfig("test", """stringVal "ignored" """")
- }
-
- def createConfigRetriever() = {
- val configId = dirConfigSource.configId
- val subscriber = new CloudSubscriberFactory(dirConfigSource.configSource)
- new ConfigRetriever(
- Set(new ConfigKey(classOf[Bootstrap1Config], configId),
- new ConfigKey(classOf[Bootstrap2Config], configId)),
- (keys) => subscriber.getSubscriber(keys.asJava))
- }
-
- def writeConfig = dirConfigSource.writeConfig _
-
- def instanceOf[T: ClassTag] = hamcrestInstanceOf(implicitly[ClassTag[T]].runtimeClass): Matcher[AnyRef]
-}
diff --git a/container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala b/container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala
deleted file mode 100644
index 9f07acc7dc9..00000000000
--- a/container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala
+++ /dev/null
@@ -1,398 +0,0 @@
-// Copyright 2017 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.AbstractComponent
-import com.yahoo.config.di.IntConfig
-import com.yahoo.config.test.TestConfig
-import com.yahoo.container.bundle.MockBundle
-import com.yahoo.container.di.ContainerTest._
-import com.yahoo.container.di.componentgraph.Provider
-import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.{SimpleComponent, SimpleComponent2}
-import com.yahoo.container.di.componentgraph.core.ComponentNode.ComponentConstructorException
-import com.yahoo.container.di.componentgraph.core.{ComponentGraph, Node}
-import com.yahoo.container.di.config.RestApiContext
-import org.hamcrest.CoreMatchers._
-import org.junit.Assert._
-import org.junit.{After, Before, Ignore, Test}
-
-import scala.collection.JavaConverters._
-import scala.concurrent.ExecutionContext.Implicits.global
-import scala.concurrent.duration._
-import scala.concurrent.{Await, Future}
-import scala.language.{existentials, postfixOps}
-import scala.util.Try
-
-/**
- * @author tonytv
- * @author gjoranv
- */
-class ContainerTest {
- var dirConfigSource: DirConfigSource = _
-
- @Before def setup() {
- dirConfigSource = new DirConfigSource("ContainerTest-")
- }
-
- @After def cleanup() {
- dirConfigSource.cleanup()
- }
-
- @Test
- def components_can_be_created_from_config() {
- writeBootstrapConfigs()
- dirConfigSource.writeConfig("test", """stringVal "myString" """)
-
- val container = newContainer(dirConfigSource)
-
- val component = createComponentTakingConfig(container.getNewComponentGraph())
- assertThat(component.config.stringVal(), is("myString"))
-
- container.shutdownConfigurer()
- }
-
- @Test
- def components_are_reconfigured_after_config_update_without_bootstrap_configs() {
- writeBootstrapConfigs()
- dirConfigSource.writeConfig("test", """stringVal "original" """)
-
- val container = newContainer(dirConfigSource)
-
- val componentGraph = container.getNewComponentGraph()
- val component = createComponentTakingConfig(componentGraph)
-
- assertThat(component.config.stringVal(), is("original"))
-
- // Reconfigure
- dirConfigSource.writeConfig("test", """stringVal "reconfigured" """)
- container.reloadConfig(2)
-
- val newComponentGraph = container.getNewComponentGraph(componentGraph)
- val component2 = createComponentTakingConfig(newComponentGraph)
- assertThat(component2.config.stringVal(), is("reconfigured"))
-
- container.shutdownConfigurer()
- }
-
- @Test
- def graph_is_updated_after_bootstrap_update() {
- dirConfigSource.writeConfig("test", """stringVal "original" """)
- writeBootstrapConfigs("id1")
-
- val container = newContainer(dirConfigSource)
-
- val graph = container.getNewComponentGraph()
- val component = createComponentTakingConfig(graph)
- assertThat(component.getId.toString, is("id1"))
-
- writeBootstrapConfigsWithMultipleComponents(Array(
- ("id1", classOf[ComponentTakingConfig]),
- ("id2", classOf[ComponentTakingConfig])))
-
- container.reloadConfig(2)
- val newGraph = container.getNewComponentGraph(graph)
-
- assertThat(ComponentGraph.getNode(newGraph, "id1"), notNullValue(classOf[Node]))
- assertThat(ComponentGraph.getNode(newGraph, "id2"), notNullValue(classOf[Node]))
-
- container.shutdownConfigurer()
- }
-
- //@Test TODO
- def deconstructor_is_given_guice_components() {
- }
-
- @Test
- def osgi_component_is_deconstructed_when_not_reused() {
- writeBootstrapConfigs("id1", classOf[DestructableComponent])
-
- val container = newContainer(dirConfigSource)
-
- val oldGraph = container.getNewComponentGraph()
- val componentToDestruct = oldGraph.getInstance(classOf[DestructableComponent])
-
- writeBootstrapConfigs("id2", classOf[DestructableComponent])
- container.reloadConfig(2)
- container.getNewComponentGraph(oldGraph)
- assertTrue(componentToDestruct.deconstructed)
- }
-
- @Ignore // because logAndDie is impossible(?) to verify programmatically
- @Test
- def manually_verify_what_happens_when_first_graph_contains_component_that_throws_exception_in_ctor() {
- writeBootstrapConfigs("thrower", classOf[ComponentThrowingExceptionInConstructor])
- val container = newContainer(dirConfigSource)
- var currentGraph: ComponentGraph = null
- try {
- currentGraph = container.getNewComponentGraph()
- fail("Expected to log and die.")
- } catch {
- case _: Throwable => fail("Expected to log and die")
- }
- }
-
- @Test
- def previous_graph_is_retained_when_new_graph_contains_component_that_throws_exception_in_ctor() {
- val simpleComponentEntry = ComponentEntry("simpleComponent", classOf[SimpleComponent])
-
- writeBootstrapConfigs(Array(simpleComponentEntry))
- val container = newContainer(dirConfigSource)
- var currentGraph = container.getNewComponentGraph()
-
- val simpleComponent = currentGraph.getInstance(classOf[SimpleComponent])
-
- writeBootstrapConfigs("thrower", classOf[ComponentThrowingExceptionInConstructor])
- container.reloadConfig(2)
- try {
- currentGraph = container.getNewComponentGraph(currentGraph)
- fail("Expected exception")
- } catch {
- case _: ComponentConstructorException => // Expected, do nothing
- case _: Throwable => fail("Expected ComponentConstructorException")
- }
- assertEquals(1, currentGraph.generation)
-
- // Also verify that next reconfig is successful
- val componentTakingConfigEntry = ComponentEntry("componentTakingConfig", classOf[ComponentTakingConfig])
- dirConfigSource.writeConfig("test", """stringVal "myString" """)
- writeBootstrapConfigs(Array(simpleComponentEntry, componentTakingConfigEntry))
- container.reloadConfig(3)
- currentGraph = container.getNewComponentGraph(currentGraph)
-
- assertEquals(3, currentGraph.generation)
- assertSame(simpleComponent, currentGraph.getInstance(classOf[SimpleComponent]))
- assertNotNull(currentGraph.getInstance(classOf[ComponentTakingConfig]))
- }
-
- @Test
- def previous_graph_is_retained_when_new_graph_throws_exception_for_missing_config() {
- val simpleComponentEntry = ComponentEntry("simpleComponent", classOf[SimpleComponent])
-
- writeBootstrapConfigs(Array(simpleComponentEntry))
- val container = newContainer(dirConfigSource)
- var currentGraph = container.getNewComponentGraph()
-
- val simpleComponent = currentGraph.getInstance(classOf[SimpleComponent])
-
- writeBootstrapConfigs("thrower", classOf[ComponentThrowingExceptionForMissingConfig])
- dirConfigSource.writeConfig("test", """stringVal "myString" """)
- container.reloadConfig(2)
- try {
- currentGraph = container.getNewComponentGraph(currentGraph)
- fail("Expected exception")
- } catch {
- case _: IllegalArgumentException => // Expected, do nothing
- case _: Throwable => fail("Expected IllegalArgumentException")
- }
- assertEquals(1, currentGraph.generation)
- }
-
- @Test
- def runOnce_hangs_waiting_for_valid_config_after_invalid_config() {
- dirConfigSource.writeConfig("test", """stringVal "original" """)
- writeBootstrapConfigs("myId", classOf[ComponentTakingConfig])
-
- val container = newContainer(dirConfigSource)
- var currentGraph = container.getNewComponentGraph()
-
- writeBootstrapConfigs("thrower", classOf[ComponentThrowingExceptionForMissingConfig])
- container.reloadConfig(2)
-
- try {
- currentGraph = container.getNewComponentGraph(currentGraph)
- fail("expected exception")
- } catch {
- case e: Exception =>
- }
-
- val newGraph = Future {
- currentGraph = container.getNewComponentGraph(currentGraph)
- currentGraph
- }
-
- Try {
- Await.ready(newGraph, 1 second)
- } foreach { x => fail("Expected waiting for new config.") }
-
-
- writeBootstrapConfigs("myId2", classOf[ComponentTakingConfig])
- container.reloadConfig(3)
-
- assertNotNull(Await.result(newGraph, 5 minutes))
- }
-
-
- @Test
- def bundle_info_is_set_on_rest_api_context() {
- val clazz = classOf[RestApiContext]
-
- writeBootstrapConfigs("restApiContext", clazz)
- dirConfigSource.writeConfig("jersey-bundles", """bundles[0].spec "mock-entry-to-enforce-a-MockBundle" """)
- dirConfigSource.writeConfig("jersey-injection", """inject[0]" """)
-
- val container = newContainer(dirConfigSource)
- val componentGraph = container.getNewComponentGraph()
-
- val restApiContext = componentGraph.getInstance(clazz)
- assertNotNull(restApiContext)
-
- assertThat(restApiContext.getBundles.size, is(1))
- assertThat(restApiContext.getBundles.get(0).symbolicName, is(MockBundle.SymbolicName))
- assertThat(restApiContext.getBundles.get(0).version, is(MockBundle.BundleVersion))
-
- container.shutdownConfigurer()
- }
-
- @Test
- def restApiContext_has_all_components_injected() {
- new JerseyInjectionTest {
- assertFalse(restApiContext.getInjectableComponents.isEmpty)
- assertThat(restApiContext.getInjectableComponents.size(), is(2))
-
- container.shutdownConfigurer()
- }
- }
-
- // TODO: reuse injectedComponent as a named component when we support that
- trait JerseyInjectionTest {
- val restApiClass = classOf[RestApiContext]
- val injectedClass = classOf[SimpleComponent]
- val injectedComponentId = "injectedComponent"
- val anotherComponentClass = classOf[SimpleComponent2]
- val anotherComponentId = "anotherComponent"
-
- val componentsConfig: String =
- ComponentEntry(injectedComponentId, injectedClass).asConfig(0) + "\n" +
- ComponentEntry(anotherComponentId, anotherComponentClass).asConfig(1) + "\n" +
- ComponentEntry("restApiContext", restApiClass).asConfig(2) + "\n" +
- s"components[2].inject[0].id $injectedComponentId\n" +
- s"components[2].inject[1].id $anotherComponentId\n"
-
- val injectionConfig = s"""inject[1]
- |inject[0].instance $injectedComponentId
- |inject[0].forClass "${injectedClass.getName}"
- """.stripMargin
-
- dirConfigSource.writeConfig("components", componentsConfig)
- dirConfigSource.writeConfig("bundles", "")
- dirConfigSource.writeConfig("jersey-bundles", """bundles[0].spec "mock-entry-to-enforce-a-MockBundle" """)
- dirConfigSource.writeConfig("jersey-injection", injectionConfig)
-
- val container = newContainer(dirConfigSource)
- val componentGraph = container.getNewComponentGraph()
-
- val restApiContext = componentGraph.getInstance(restApiClass)
- }
-
- case class ComponentEntry(componentId: String, classId: Class[_]) {
- def asConfig(position: Int): String = {
- <config>
- |components[{position}].id "{componentId}"
- |components[{position}].classId "{classId.getName}"
- |components[{position}].configId "{dirConfigSource.configId}"
- </config>.text.stripMargin.trim
- }
- }
-
- def writeBootstrapConfigs(componentEntries: Array[ComponentEntry]) {
- dirConfigSource.writeConfig("bundles", "")
- dirConfigSource.writeConfig("components", """
- components[%s]
- %s
- """.format(componentEntries.length,
- componentEntries.zipWithIndex.map{ case (entry, index) => entry.asConfig(index) }.mkString("\n")))
- }
-
- def writeBootstrapConfigs(componentId: String = classOf[ComponentTakingConfig].getName,
- classId: Class[_] = classOf[ComponentTakingConfig]) {
-
- writeBootstrapConfigs(Array(ComponentEntry(componentId, classId)))
- }
-
- def writeBootstrapConfigsWithMultipleComponents(idAndClass: Array[(String, Class[_])]) {
- writeBootstrapConfigs(idAndClass.map{case(id, classId) => ComponentEntry(id, classId)})
- }
-
-
- @Test
- def providers_are_destructed() {
- writeBootstrapConfigs("id1", classOf[DestructableProvider])
-
- val deconstructor = new ComponentDeconstructor {
- def deconstruct(component: AnyRef) {
- component match {
- case c : AbstractComponent => c.deconstruct()
- case p : Provider[_] => p.deconstruct()
- }
- }
- }
-
- val container = newContainer(dirConfigSource, deconstructor)
-
- val oldGraph = container.getNewComponentGraph()
- val destructableEntity = oldGraph.getInstance(classOf[DestructableEntity])
-
- writeBootstrapConfigs("id2", classOf[DestructableProvider])
- container.reloadConfig(2)
- container.getNewComponentGraph(oldGraph)
-
- assertTrue(destructableEntity.deconstructed)
- }
-}
-
-
-object ContainerTest {
- class DestructableEntity {
- var deconstructed = false
- }
-
- class DestructableProvider extends Provider[DestructableEntity] {
- val instance = new DestructableEntity
-
- def get() = instance
-
- def deconstruct() {
- require(! instance.deconstructed)
- instance.deconstructed = true
- }
- }
-
- class ComponentTakingConfig(val config: TestConfig) extends AbstractComponent {
- require(config != null)
- }
-
- class ComponentThrowingExceptionInConstructor() {
- throw new RuntimeException("This component fails upon construction.")
- }
-
- class ComponentThrowingExceptionForMissingConfig(intConfig: IntConfig) extends AbstractComponent {
- fail("This component should never be created. Only used for tests where 'int' config is missing.")
- }
-
- class DestructableComponent extends AbstractComponent {
- var deconstructed = false
- override def deconstruct() {
- deconstructed = true
- }
- }
-
- class TestDeconstructor extends ComponentDeconstructor {
- def deconstruct(component: AnyRef) {
- component match {
- case vespaComponent: DestructableComponent => vespaComponent.deconstruct()
- case _ =>
- }
- }
- }
-
- private def newContainer(dirConfigSource: DirConfigSource,
- deconstructor: ComponentDeconstructor = new TestDeconstructor()):
- Container = {
- new Container(new CloudSubscriberFactory(dirConfigSource.configSource), dirConfigSource.configId, deconstructor)
- }
-
- def createComponentTakingConfig(componentGraph: ComponentGraph): ComponentTakingConfig = {
- componentGraph.getInstance(classOf[ComponentTakingConfig])
- }
-
- def convertMap[K, V](map: java.util.Map[K, V]): Map[K, V] = map.asScala.toMap
-}
diff --git a/container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala b/container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala
deleted file mode 100644
index 4f80b25a247..00000000000
--- a/container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala
+++ /dev/null
@@ -1,50 +0,0 @@
-// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
-package com.yahoo.container.di
-
-import java.io.{FileOutputStream, OutputStream, File}
-import DirConfigSource._
-import java.util.Random
-import org.junit.rules.TemporaryFolder
-import com.yahoo.config.subscription.{ConfigSource, ConfigSourceSet}
-
-/**
- * @author tonytv
- * @author gjoranv
- */
-class DirConfigSource(val testSourcePrefix: String) {
-
- private val tempFolder = createTemporaryFolder()
-
- val configSource : ConfigSource = new ConfigSourceSet(testSourcePrefix + new Random().nextLong)
-
- def writeConfig(name: String, contents: String) {
- val file = new File(tempFolder.getRoot, name + ".cfg")
- if (!file.exists())
- file.createNewFile()
-
- printFile(file, contents + "\n")
- }
-
- def configId = "dir:" + tempFolder.getRoot.getPath
-
- def cleanup() {
- tempFolder.delete()
- }
-
-}
-
-private object DirConfigSource {
-
- def printFile(f: File, content: String) {
- var out: OutputStream = new FileOutputStream(f)
- out.write(content.getBytes("UTF-8"))
- out.close()
- }
-
- def createTemporaryFolder() = {
- val folder = new TemporaryFolder
- folder.create()
- folder
- }
-
-}
diff --git a/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala
deleted file mode 100644
index 05194cb911b..00000000000
--- a/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala
+++ /dev/null
@@ -1,540 +0,0 @@
-// Copyright 2017 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 java.util.concurrent.{Executor, Executors}
-
-import com.google.inject.name.{Named, Names}
-import com.google.inject.{AbstractModule, Guice, Inject, Key, Provider => GuiceProvider}
-import com.yahoo.component.provider.ComponentRegistry
-import com.yahoo.component.{AbstractComponent, ComponentId}
-import com.yahoo.config.ConfigInstance
-import com.yahoo.config.subscription.ConfigGetter
-import com.yahoo.config.test.{Test2Config, TestConfig}
-import com.yahoo.container.di._
-import com.yahoo.container.di.componentgraph.Provider
-import com.yahoo.container.di.config.{JerseyBundlesConfig, JerseyInjectionConfig, RestApiContext}
-import com.yahoo.vespa.config.ConfigKey
-import org.hamcrest.CoreMatchers.{containsString, equalTo, is, not, sameInstance}
-import org.hamcrest.Matcher
-import org.junit.Assert._
-import org.junit.Test
-
-import scala.language.implicitConversions
-
-/**
- * @author gjoranv
- * @author tonytv
- */
-class ComponentGraphTest {
- import ComponentGraphTest._
-
- private def keyAndConfig[T <: ConfigInstance](clazz: Class[T], configId: String): (ConfigKey[T], T) = {
- val key = new ConfigKey(clazz, configId)
- key -> ConfigGetter.getConfig(key.getConfigClass, key.getConfigId.toString)
- }
-
- @Test
- def component_taking_config_can_be_instantiated() {
- val componentGraph = new ComponentGraph
- val configId = "raw:stringVal \"test-value\""
- val componentNode = mockComponentNode(classOf[ComponentTakingConfig], configId)
-
- componentGraph.add(componentNode)
- componentGraph.complete()
- componentGraph.setAvailableConfigs(Map(keyAndConfig(classOf[TestConfig], configId)))
-
- val instance = componentGraph.getInstance(classOf[ComponentTakingConfig])
- assertNotNull(instance)
- assertThat(instance.config.stringVal(), is("test-value"))
- }
-
- @Test
- def component_can_be_injected_into_another_component() {
- val injectedComponent = mockComponentNode(classOf[SimpleComponent])
- val targetComponent = mockComponentNode(classOf[ComponentTakingComponent])
- targetComponent.inject(injectedComponent)
-
- val destroyGlobalLookupComponent = mockComponentNode(classOf[SimpleComponent])
-
- val componentGraph = new ComponentGraph
- componentGraph.add(injectedComponent)
- componentGraph.add(targetComponent)
- componentGraph.add(destroyGlobalLookupComponent)
- componentGraph.complete()
-
-
- val instance = componentGraph.getInstance(classOf[ComponentTakingComponent])
- assertNotNull(instance)
- }
-
- @Test
- def all_components_of_a_type_can_be_injected() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
- componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
- componentGraph.add(mockComponentNode(classOf[SimpleDerivedComponent]))
- componentGraph.add(mockComponentNode(classOf[ComponentTakingAllSimpleComponents]))
- componentGraph.complete()
-
- val instance = componentGraph.getInstance(classOf[ComponentTakingAllSimpleComponents])
- assertThat(instance.simpleComponents.allComponents().size(), is(3))
- }
-
- @Test
- def empty_component_registry_can_be_injected() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[ComponentTakingAllSimpleComponents]))
- componentGraph.complete()
-
- val instance = componentGraph.getInstance(classOf[ComponentTakingAllSimpleComponents])
- assertThat(instance.simpleComponents.allComponents().size(), is(0))
- }
-
- @Test
- def component_registry_with_wildcard_upper_bound_can_be_injected() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
- componentGraph.add(mockComponentNode(classOf[SimpleDerivedComponent]))
- componentGraph.add(mockComponentNode(classOf[ComponentTakingAllSimpleComponentsUpperBound]))
- componentGraph.complete()
-
- val instance = componentGraph.getInstance(classOf[ComponentTakingAllSimpleComponentsUpperBound])
- assertThat(instance.simpleComponents.allComponents().size(), is(2))
- }
-
- @Test(expected = classOf[RuntimeException])
- def require_exception_when_injecting_registry_with_unknown_type_variable() {
- val clazz = classOf[ComponentTakingAllComponentsWithTypeVariable[_]]
-
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
- componentGraph.add(mockComponentNode(classOf[SimpleDerivedComponent]))
- componentGraph.add(mockComponentNode(clazz))
- componentGraph.complete()
-
- componentGraph.getInstance(clazz)
- }
-
- @Test
- def components_are_shared() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
- componentGraph.complete()
-
- val instance1 = componentGraph.getInstance(classOf[SimpleComponent])
- val instance2 = componentGraph.getInstance(classOf[SimpleComponent])
- assertThat(instance1, sameInstance(instance2))
- }
-
- @Test
- def singleton_components_can_be_injected() {
- val componentGraph = new ComponentGraph
- val configId = """raw:stringVal "test-value" """
-
- componentGraph.add(mockComponentNode(classOf[ComponentTakingComponent]))
- componentGraph.add(mockComponentNode(classOf[ComponentTakingConfig], configId))
- componentGraph.add(mockComponentNode(classOf[SimpleComponent2]))
- componentGraph.complete()
- componentGraph.setAvailableConfigs(Map(keyAndConfig(classOf[TestConfig], configId)))
-
- val instance = componentGraph.getInstance(classOf[ComponentTakingComponent])
- assertThat(instance.injectedComponent.asInstanceOf[ComponentTakingConfig].config.stringVal(), is("test-value"))
- }
-
- @Test(expected = classOf[RuntimeException])
- def require_error_when_multiple_components_match_a_singleton_dependency() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[SimpleDerivedComponent]))
- componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
- componentGraph.add(mockComponentNode(classOf[ComponentTakingComponent]))
- componentGraph.complete()
- }
-
- @Test
- def named_component_can_be_injected() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
- componentGraph.add(mockComponentNode(classOf[SimpleComponent], key = Names.named("named-test")))
- componentGraph.add(mockComponentNode(classOf[ComponentTakingNamedComponent]))
- componentGraph.complete()
- }
-
- @Test
- def config_keys_can_be_retrieved() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[ComponentTakingConfig], configId = """raw:stringVal "component1" """""))
- componentGraph.add(mockComponentNode(classOf[ComponentTakingConfig], configId = """raw:stringVal "component2" """""))
- componentGraph.add(new ComponentRegistryNode(classOf[ComponentTakingConfig]))
- componentGraph.complete()
-
- val configKeys = componentGraph.configKeys
- assertThat(configKeys.size, is(2))
-
- configKeys.foreach{ key =>
- assertThat(key.getConfigClass, equalTo(classOf[TestConfig]))
- assertThat(key.getConfigId.toString, containsString("component"))
- }
- }
-
- @Test
- def providers_can_be_instantiated() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
- componentGraph.complete()
-
- assertNotNull(componentGraph.getInstance(classOf[Executor]))
- }
-
- @Test
- def providers_can_be_inherited() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[DerivedExecutorProvider]))
- componentGraph.complete()
-
- assertNotNull(componentGraph.getInstance(classOf[Executor]))
- }
-
- @Test
- def providers_can_deliver_a_new_instance_for_each_component() {
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNode(classOf[NewIntProvider]))
- componentGraph.complete()
-
- val instance1 = componentGraph.getInstance(classOf[Int])
- val instance2 = componentGraph.getInstance(classOf[Int])
- assertThat(instance1, not(equalTo(instance2)))
- }
-
- @Test
- def providers_can_be_injected_explicitly() {
- val componentGraph = new ComponentGraph
-
- val componentTakingExecutor = mockComponentNode(classOf[ComponentTakingExecutor])
- val executorProvider = mockComponentNode(classOf[ExecutorProvider])
- componentTakingExecutor.inject(executorProvider)
-
- componentGraph.add(executorProvider)
- componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
-
- componentGraph.add(componentTakingExecutor)
-
- componentGraph.complete()
- assertNotNull(componentGraph.getInstance(classOf[ComponentTakingExecutor]))
- }
-
- @Test
- def global_providers_can_be_injected() {
- val componentGraph = new ComponentGraph
-
- componentGraph.add(mockComponentNode(classOf[ComponentTakingExecutor]))
- componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
- componentGraph.add(mockComponentNode(classOf[IntProvider]))
- componentGraph.complete()
-
- assertNotNull(componentGraph.getInstance(classOf[ComponentTakingExecutor]))
- }
-
- @Test(expected = classOf[RuntimeException])
- def throw_if_multiple_global_providers_exist(): Unit = {
- val componentGraph = new ComponentGraph
-
- componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
- componentGraph.add(mockComponentNode(classOf[ExecutorProvider]))
- componentGraph.add(mockComponentNode(classOf[ComponentTakingExecutor]))
- componentGraph.complete()
- }
-
- @Test
- def provider_is_not_used_when_component_of_provided_class_exists() {
- val componentGraph = new ComponentGraph
-
- componentGraph.add(mockComponentNode(classOf[SimpleComponent]))
- componentGraph.add(mockComponentNode(classOf[SimpleComponentProviderThatThrows]))
- componentGraph.add(mockComponentNode(classOf[ComponentTakingComponent]))
- componentGraph.complete()
-
- val injectedComponent = componentGraph.getInstance(classOf[ComponentTakingComponent]).injectedComponent
- assertNotNull(injectedComponent)
- }
-
- //TODO: move
- @Test
- def check_if_annotation_is_a_binding_annotation() {
- import ComponentGraph.isBindingAnnotation
-
- assertTrue(isBindingAnnotation(Names.named("name")))
- assertFalse(isBindingAnnotation(classOf[Named].getAnnotations.head))
- }
-
- @Test
- def cycles_gives_exception() {
- val componentGraph = new ComponentGraph
-
- def mockNode = mockComponentNode(classOf[ComponentCausingCycle])
-
- val node1 = mockNode
- val node2 = mockNode
-
- node1.inject(node2)
- node2.inject(node1)
-
- componentGraph.add(node1)
- componentGraph.add(node2)
-
- try {
- componentGraph.complete()
- fail("Cycle exception expected.")
- } catch {
- case e : Throwable => assertThat(e.getMessage, containsString("cycle"))
- }
- }
-
- @Test(expected = classOf[IllegalArgumentException])
- def abstract_classes_are_rejected() {
- new ComponentNode(ComponentId.fromString("Test"), "", classOf[AbstractClass])
- }
-
- @Test
- def inject_constructor_is_preferred() {
- assertThatComponentCanBeCreated(classOf[ComponentWithInjectConstructor])
- }
-
- @Test
- def constructor_with_most_parameters_is_preferred() {
- assertThatComponentCanBeCreated(classOf[ComponentWithMultipleConstructors])
- }
-
- def assertThatComponentCanBeCreated(clazz: Class[AnyRef]) {
- val componentGraph = new ComponentGraph
- val configId = """raw:stringVal "dummy" """"
-
- componentGraph.add(mockComponentNode(clazz, configId))
- componentGraph.complete()
-
- componentGraph.setAvailableConfigs(Map(
- keyAndConfig(classOf[TestConfig], configId),
- keyAndConfig(classOf[Test2Config], configId)))
-
- assertNotNull(componentGraph.getInstance(clazz))
- }
-
- @Test
- def require_fallback_to_child_injector() {
- val componentGraph = new ComponentGraph
-
- componentGraph.add(mockComponentNode(classOf[ComponentTakingExecutor]))
-
- componentGraph.complete(singletonExecutorInjector)
- assertNotNull(componentGraph.getInstance(classOf[ComponentTakingExecutor]))
- }
-
- @Test
- def child_injector_can_inject_multiple_instances_for_same_key() {
- def executorProvider() = Executors.newSingleThreadExecutor()
-
- val (graphSize, executorA, executorB) = buildGraphWithChildInjector(() => executorProvider())
-
- assertThat(graphSize, is(4))
- assertThat(executorA, not(sameInstance(executorB)))
- }
-
- @Test
- def components_injected_via_child_injector_can_be_shared() {
- val commonExecutor = Executors.newSingleThreadExecutor()
- val (graphSize, executorA, executorB) = buildGraphWithChildInjector(() => commonExecutor)
-
- assertThat(graphSize, is(3))
- assertThat(executorA, sameInstance(executorB))
- }
-
- def buildGraphWithChildInjector(executorProvider: () => Executor) = {
- val childInjector = Guice.createInjector(new AbstractModule {
- override def configure() {
- bind(classOf[Executor]).toProvider(new GuiceProvider[Executor] {
- def get() = executorProvider()
- })
- }
- })
-
- val componentGraph = new ComponentGraph
-
- def key(name: String) = Key.get(classOf[ComponentTakingExecutor], Names.named(name))
- val keyA = key("A")
- val keyB = key("B")
-
- componentGraph.add(mockComponentNode(keyA))
- componentGraph.add(mockComponentNode(keyB))
-
- componentGraph.complete(childInjector)
-
- (componentGraph.size, componentGraph.getInstance(keyA).executor, componentGraph.getInstance(keyB).executor)
- }
-
- @Test
- def providers_can_be_reused() {
- def createGraph() = {
- val graph = new ComponentGraph()
- graph.add(mockComponentNodeWithId(classOf[ExecutorProvider], "dummyId"))
- graph.complete()
- graph.setAvailableConfigs(Map())
- graph
- }
-
- val oldGraph = createGraph()
- val executor = oldGraph.getInstance(classOf[Executor])
-
- val newGraph = createGraph()
- newGraph.reuseNodes(oldGraph)
-
- val newExecutor = newGraph.getInstance(classOf[Executor])
- assertThat(executor, sameInstance(newExecutor))
- }
-
- @Test
- def component_id_can_be_injected() {
- val componentId: String = "myId:1.2@namespace"
-
- val componentGraph = new ComponentGraph
- componentGraph.add(mockComponentNodeWithId(classOf[ComponentTakingComponentId], componentId))
- componentGraph.complete()
-
- assertThat(componentGraph.getInstance(classOf[ComponentTakingComponentId]).componentId,
- is(ComponentId.fromString(componentId)))
- }
-
- @Test
- def rest_api_context_can_be_instantiated() {
- val configId = """raw:"" """
-
- val clazz = classOf[RestApiContext]
- val jerseyNode = new JerseyNode(uniqueComponentId(clazz.getName), configId, clazz, new Osgi {})
-
- val componentGraph = new ComponentGraph
- componentGraph.add(jerseyNode)
- componentGraph.complete()
- componentGraph.setAvailableConfigs(Map(keyAndConfig(classOf[JerseyBundlesConfig], configId),
- keyAndConfig(classOf[JerseyInjectionConfig], configId)))
-
- val restApiContext = componentGraph.getInstance(clazz)
- assertNotNull(restApiContext)
- assertThat(restApiContext.getBundles.size, is(0))
- }
-
-}
-
-//Note that all Components must be defined in a static context,
-//otherwise their constructor will take the outer class as the first parameter.
-object ComponentGraphTest {
- var counter = 0
-
-
- class SimpleComponent extends AbstractComponent
- class SimpleComponent2 extends AbstractComponent
- class SimpleDerivedComponent extends SimpleComponent
-
- class ComponentTakingConfig(val config: TestConfig) extends SimpleComponent {
- require(config != null)
- }
-
- class ComponentTakingComponent(val injectedComponent: SimpleComponent) extends AbstractComponent {
- require(injectedComponent != null)
- }
-
- class ComponentTakingConfigAndComponent(val config: TestConfig, val injectedComponent: SimpleComponent) extends AbstractComponent {
- require(config != null)
- require(injectedComponent != null)
- }
-
- class ComponentTakingAllSimpleComponents(val simpleComponents: ComponentRegistry[SimpleComponent]) extends AbstractComponent {
- require(simpleComponents != null)
- }
-
- class ComponentTakingAllSimpleComponentsUpperBound(val simpleComponents: ComponentRegistry[_ <: SimpleComponent])
- extends AbstractComponent {
-
- require(simpleComponents != null)
- }
-
- class ComponentTakingAllComponentsWithTypeVariable[COMPONENT <: AbstractComponent](val simpleComponents: ComponentRegistry[COMPONENT])
- extends AbstractComponent {
-
- require(simpleComponents != null)
- }
-
- class ComponentTakingNamedComponent(@Named("named-test") injectedComponent: SimpleComponent) extends AbstractComponent {
- require(injectedComponent != null)
- }
-
- class ComponentCausingCycle(component: ComponentCausingCycle) extends AbstractComponent
-
- class SimpleComponentProviderThatThrows extends Provider[SimpleComponent] {
- def get() = throw new AssertionError("Should never be called.")
- def deconstruct() {}
- }
-
- class ExecutorProvider extends Provider[Executor] {
- val executor = Executors.newSingleThreadExecutor()
- def get() = executor
- def deconstruct() { /*TODO */ }
- }
-
- class DerivedExecutorProvider extends ExecutorProvider
-
- class IntProvider extends Provider[java.lang.Integer] {
- def get() = throw new AssertionError("Should never be called.")
- def deconstruct() {}
- }
-
- class NewIntProvider extends Provider[Integer] {
- var i: Int = 0
- def get() = {
- i += 1
- i
- }
- def deconstruct() {}
- }
-
- class ComponentTakingExecutor(val executor: Executor) extends AbstractComponent {
- require(executor != null)
- }
-
- class ComponentWithInjectConstructor private () {
- def this(c: TestConfig, c2: Test2Config) = { this(); sys.error("Should not be called") }
- @Inject
- def this(c: Test2Config) = { this() }
- }
-
- class ComponentWithMultipleConstructors private (dummy : Int) {
- def this(c: TestConfig, c2: Test2Config) = { this(0); }
-
- def this() = { this(0); sys.error("Should not be called") }
- def this(c: Test2Config) = { this() }
- }
-
- class ComponentTakingComponentId(val componentId: ComponentId)
-
- def uniqueComponentId(className: String): ComponentId = {
- counter += 1
- ComponentId.fromString(className + counter)
- }
-
- def mockComponentNode(key: Key[_ <: AnyRef]): Node =
- mockComponentNode(key.getTypeLiteral.getRawType.asInstanceOf[Class[AnyRef]], key=key.getAnnotation)
-
- def mockComponentNode(clazz: Class[_ <: AnyRef], configId: String = "", key: JavaAnnotation = null): Node =
- new ComponentNode(uniqueComponentId(clazz.getName), configId, clazz, key)
-
- def mockComponentNodeWithId(clazz: Class[_ <: AnyRef], componentId: String, configId: String = "", key: JavaAnnotation = null): Node =
- new ComponentNode(ComponentId.fromString(componentId), configId, clazz, key)
-
- val singletonExecutorInjector = Guice.createInjector(new AbstractModule {
- override def configure() {
- bind(classOf[Executor]).toInstance(Executors.newSingleThreadExecutor())
- }
- })
-
- implicit def makeMatcherCovariant[T, U >: T](matcher: Matcher[T]) : Matcher[U] = matcher.asInstanceOf[Matcher[U]]
-
- abstract class AbstractClass
-}
-
diff --git a/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.scala b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.scala
deleted file mode 100644
index e55ac65680d..00000000000
--- a/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.scala
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright 2017 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 java.util
-import java.util.Collections
-import com.yahoo.container.di.osgi.OsgiUtil
-import org.junit.Test
-import org.junit.Assert._
-import org.hamcrest.CoreMatchers.is
-import org.hamcrest.Matchers.{contains, containsInAnyOrder}
-import org.osgi.framework.wiring.BundleWiring
-import scala.collection.JavaConverters._
-import com.yahoo.container.bundle.MockBundle
-
-/**
- *
- * @author gjoranv
- * @since 5.17
- */
-
-class JerseyNodeTest {
-
- trait WithMockBundle {
- object bundle extends MockBundle {
- val entry = Map(
- "com/foo" -> "Foo.class",
- "com/bar" -> "Bar.class)"
- ) map { case (packageName, className) => (packageName, packageName + "/" + className)}
-
-
- override def listResources(path: String, ignored: String, options: Int): util.Collection[String] = {
- if ((options & BundleWiring.LISTRESOURCES_RECURSE) != 0 && path == "/") entry.values.asJavaCollection
- else Collections.singleton(entry(path))
- }
- }
-
- val bundleClasses = bundle.entry.values.toList
- }
-
- @Test
- def all_bundle_entries_are_returned_when_no_packages_are_given() {
- new WithMockBundle {
- val entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Set()).asJavaCollection
- assertThat(entries, containsInAnyOrder(bundleClasses: _*))
- }
- }
-
- @Test
- def only_bundle_entries_from_the_given_packages_are_returned() {
- new WithMockBundle {
- val entries = OsgiUtil.getClassEntriesInBundleClassPath(bundle, Set("com.foo")).asJavaCollection
- assertThat(entries, contains(bundle.entry("com/foo")))
- }
- }
-
- @Test
- def bundle_info_is_initialized() {
- new WithMockBundle {
- val bundleInfo = JerseyNode.createBundleInfo(bundle, List())
- assertThat(bundleInfo.symbolicName, is(bundle.getSymbolicName))
- assertThat(bundleInfo.version, is(bundle.getVersion))
- assertThat(bundleInfo.fileLocation, is(bundle.getLocation))
- }
- }
-
-}
diff --git a/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.scala b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.scala
deleted file mode 100644
index 33c6d2a3e89..00000000000
--- a/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.scala
+++ /dev/null
@@ -1,249 +0,0 @@
-// Copyright 2017 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, AbstractComponent}
-import org.junit.Assert._
-import org.hamcrest.CoreMatchers.{is, not, sameInstance, equalTo}
-import com.yahoo.vespa.config.ConfigKey
-import java.util.concurrent.Executor
-import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.{ExecutorProvider, SimpleComponent, SimpleComponent2}
-import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.{ComponentTakingConfig, ComponentTakingExecutor, singletonExecutorInjector}
-import com.yahoo.container.di.makeClassCovariant
-import org.junit.Test
-import com.yahoo.config.subscription.ConfigGetter
-import com.yahoo.config.test.TestConfig
-
-/**
- * @author gjoranv
- * @author tonytv
- */
-class ReuseComponentsTest {
- import ReuseComponentsTest._
-
- @Test
- def require_that_component_is_reused_when_componentNode_is_unmodified() {
- def reuseAndTest(classToRegister: Class[AnyRef], classToLookup: Class[AnyRef]) {
- val graph = buildGraphAndSetNoConfigs(classToRegister)
- val instance = getComponent(graph, classToLookup)
-
- val newGraph = buildGraphAndSetNoConfigs(classToRegister)
- newGraph.reuseNodes(graph)
- val instance2 = getComponent(newGraph, classToLookup)
-
- assertThat(instance2, sameInstance(instance))
- }
-
- reuseAndTest(classOf[SimpleComponent], classOf[SimpleComponent])
- reuseAndTest(classOf[ExecutorProvider], classOf[Executor])
- }
-
-
- @Test(expected = classOf[IllegalStateException])
- def require_that_component_is_not_reused_when_class_is_changed() {
- val graph = buildGraphAndSetNoConfigs(classOf[SimpleComponent])
- val instance = getComponent(graph, classOf[SimpleComponent])
-
- val newGraph = buildGraphAndSetNoConfigs(classOf[SimpleComponent2])
- newGraph.reuseNodes(graph)
- val instance2 = getComponent(newGraph, classOf[SimpleComponent2])
-
- assertThat(instance2.getId, is(instance.getId))
- val throwsException = getComponent(newGraph, classOf[SimpleComponent])
- }
-
- @Test
- def require_that_component_is_not_reused_when_config_is_changed() {
- def setConfig(graph: ComponentGraph, config: String) {
- graph.setAvailableConfigs(
- Map(new ConfigKey(classOf[TestConfig], "component") ->
- ConfigGetter.getConfig(classOf[TestConfig], """raw: stringVal "%s" """.format(config))))
- }
-
- val componentClass = classOf[ComponentTakingConfig]
-
- val graph = buildGraph(componentClass)
- setConfig(graph, "oldConfig")
- val instance = getComponent(graph, componentClass)
-
- val newGraph = buildGraph(componentClass)
- setConfig(newGraph, "newConfig")
- newGraph.reuseNodes(graph)
- val instance2 = getComponent(newGraph, componentClass)
-
- assertThat(instance2, not(sameInstance(instance)))
- }
-
- @Test
- def require_that_component_is_not_reused_when_injected_component_is_changed() {
- import ComponentGraphTest.{ComponentTakingComponent, ComponentTakingConfig}
-
- def buildGraph(config: String) = {
- val graph = new ComponentGraph
-
- val rootComponent = mockComponentNode(classOf[ComponentTakingComponent], "root_component")
-
- val configId = "componentTakingConfigId"
- val injectedComponent = mockComponentNode(classOf[ComponentTakingConfig], "injected_component", configId)
-
- rootComponent.inject(injectedComponent)
-
- graph.add(rootComponent)
- graph.add(injectedComponent)
-
- graph.complete()
- graph.setAvailableConfigs(Map(new ConfigKey(classOf[TestConfig], configId) ->
- ConfigGetter.getConfig(classOf[TestConfig], """raw: stringVal "%s" """.format(config))))
-
- graph
- }
-
- val oldGraph = buildGraph(config="oldGraph")
- val oldInstance = getComponent(oldGraph, classOf[ComponentTakingComponent])
-
- val newGraph = buildGraph(config="newGraph")
- newGraph.reuseNodes(oldGraph)
- val newInstance = getComponent(newGraph, classOf[ComponentTakingComponent])
-
- assertThat(newInstance, not(sameInstance(oldInstance)))
- }
-
- @Test
- def require_that_component_is_not_reused_when_injected_component_registry_has_one_component_removed() {
- import ComponentGraphTest.ComponentTakingAllSimpleComponents
-
- def buildGraph(useBothInjectedComponents: Boolean) = {
- val graph = new ComponentGraph
- graph.add(mockComponentNode(classOf[ComponentTakingAllSimpleComponents], "root_component"))
-
- /* Below if-else has code duplication, but explicit ordering of the two components
- * was necessary to reproduce erroneous behaviour in ComponentGraph.reuseNodes that
- * occurred before ComponentRegistryNode got its own 'equals' implementation.
- */
- if (useBothInjectedComponents) {
- graph.add(mockComponentNode(classOf[SimpleComponent], "injected_component2"))
- graph.add(mockComponentNode(classOf[SimpleComponent], "injected_component1"))
- } else {
- graph.add(mockComponentNode(classOf[SimpleComponent], "injected_component1"))
- }
-
- graph.complete()
- graph.setAvailableConfigs(Map())
- graph
- }
-
- val oldGraph = buildGraph(useBothInjectedComponents = true)
- val oldSimpleComponentRegistry = getComponent(oldGraph, classOf[ComponentTakingAllSimpleComponents]).simpleComponents
-
- val newGraph = buildGraph(useBothInjectedComponents = false)
- newGraph.reuseNodes(oldGraph)
- val newSimpleComponentRegistry = getComponent(newGraph, classOf[ComponentTakingAllSimpleComponents]).simpleComponents
-
- assertThat(newSimpleComponentRegistry, not(sameInstance(oldSimpleComponentRegistry)))
- }
-
- @Test
- def require_that_injected_component_is_reused_even_when_dependent_component_is_changed() {
- import ComponentGraphTest.{ComponentTakingConfigAndComponent, SimpleComponent}
-
- def buildGraph(config: String) = {
- val graph = new ComponentGraph
-
- val configId = "componentTakingConfigAndComponent"
- val rootComponent = mockComponentNode(classOf[ComponentTakingConfigAndComponent], "root_component", configId)
-
- val injectedComponent = mockComponentNode(classOf[SimpleComponent], "injected_component")
-
- rootComponent.inject(injectedComponent)
-
- graph.add(rootComponent)
- graph.add(injectedComponent)
-
- graph.complete()
- graph.setAvailableConfigs(Map(new ConfigKey(classOf[TestConfig], configId) ->
- ConfigGetter.getConfig(classOf[TestConfig], """raw: stringVal "%s" """.format(config))))
-
- graph
- }
-
- val oldGraph = buildGraph(config="oldGraph")
- val oldInjectedComponent = getComponent(oldGraph, classOf[SimpleComponent])
- val oldDependentComponent = getComponent(oldGraph, classOf[ComponentTakingConfigAndComponent])
-
- val newGraph = buildGraph(config="newGraph")
- newGraph.reuseNodes(oldGraph)
- val newInjectedComponent = getComponent(newGraph, classOf[SimpleComponent])
- val newDependentComponent = getComponent(newGraph, classOf[ComponentTakingConfigAndComponent])
-
- assertThat(newDependentComponent, not(sameInstance(oldDependentComponent)))
- assertThat(newInjectedComponent, sameInstance(oldInjectedComponent))
- }
-
- @Test
- def require_that_node_depending_on_guice_node_is_reused() {
- def makeGraph = {
- val graph = new ComponentGraph
- graph.add(mockComponentNode(classOf[ComponentTakingExecutor], "dummyId"))
- graph.complete(singletonExecutorInjector)
- graph.setAvailableConfigs(Map())
- graph
- }
-
- val getComponentTakingExecutor = getComponent(_: ComponentGraph, classOf[ComponentTakingExecutor])
-
- val oldGraph = makeGraph
- getComponentTakingExecutor(oldGraph) // Ensure creation of GuiceNode
- val newGraph = makeGraph
- newGraph.reuseNodes(oldGraph)
- assertThat(getComponentTakingExecutor(oldGraph), sameInstance(getComponentTakingExecutor(newGraph)))
- }
-
- @Test
- def require_that_node_equals_only_checks_first_level_components_to_inject() {
-
- def createNodeWithInjectedNodeWithInjectedNode(indirectlyInjectedComponentId: String): Node = {
- val targetComponent = mockComponentNode(classOf[SimpleComponent], "target")
- val directlyInjectedComponent = mockComponentNode(classOf[SimpleComponent], "directlyInjected")
- val indirectlyInjectedComponent = mockComponentNode(classOf[SimpleComponent], indirectlyInjectedComponentId)
- directlyInjectedComponent.inject(indirectlyInjectedComponent)
- targetComponent.inject(directlyInjectedComponent)
-
- completeNode(targetComponent)
- completeNode(directlyInjectedComponent)
- completeNode(indirectlyInjectedComponent)
-
- targetComponent
- }
- val targetNode1 = createNodeWithInjectedNodeWithInjectedNode("indirectlyInjected_1")
- val targetNode2 = createNodeWithInjectedNodeWithInjectedNode("indirectlyInjected_2")
- assertThat(targetNode1, equalTo(targetNode2))
- }
-
- private def completeNode(node: ComponentNode) {
- node.setArguments(Array())
- node.setAvailableConfigs(Map())
- }
-
- private def buildGraph(componentClass: Class[_ <: AnyRef]) = {
- val commonComponentId = "component"
- val g = new ComponentGraph
- g.add(mockComponentNode(componentClass, commonComponentId, configId = commonComponentId))
- g.complete()
- g
- }
-
- private def buildGraphAndSetNoConfigs(componentClass: Class[_ <: AnyRef]) = {
- val g = buildGraph(componentClass)
- g.setAvailableConfigs(Map())
- g
- }
-}
-
-object ReuseComponentsTest {
-
- def mockComponentNode(clazz: Class[_ <: AnyRef], componentId: String = "", configId: String="") =
- new ComponentNode(new ComponentId(componentId), configId, clazz)
-
- def getComponent[T](graph: ComponentGraph, clazz: Class[T]) = {
- graph.getInstance(clazz)
- }
-}