aboutsummaryrefslogtreecommitdiffstats
path: root/container-di
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /container-di
Publish
Diffstat (limited to 'container-di')
-rw-r--r--container-di/.gitignore2
-rw-r--r--container-di/OWNERS1
-rw-r--r--container-di/benchmarks/src/test/java/com/yahoo/component/ComponentIdBenchmark.java50
-rw-r--r--container-di/pom.xml226
-rw-r--r--container-di/src/main/java/com/yahoo/container/bundle/BundleInstantiationSpecification.java86
-rw-r--r--container-di/src/main/java/com/yahoo/container/bundle/package-info.java5
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/ComponentDeconstructor.java10
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/Provider.java25
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/core/package-info.java5
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java7
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java12
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java101
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java21
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java18
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/config/package-info.java5
-rw-r--r--container-di/src/main/java/com/yahoo/container/di/osgi/package-info.java8
-rw-r--r--container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java50
-rw-r--r--container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java5
-rw-r--r--container-di/src/main/resources/configdefinitions/bundles.def6
-rw-r--r--container-di/src/main/resources/configdefinitions/components.def24
-rw-r--r--container-di/src/main/resources/configdefinitions/jersey-bundles.def8
-rw-r--r--container-di/src/main/resources/configdefinitions/jersey-injection.def5
-rw-r--r--container-di/src/main/scala/com/yahoo/container/bundle/MockBundle.scala96
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/CloudSubscriberFactory.scala100
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/ConfigRetriever.scala101
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/Container.scala200
-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.scala330
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentNode.scala205
-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/DotGraph.scala46
-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.scala93
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/Node.scala111
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/osgi/OsgiUtil.scala135
-rw-r--r--container-di/src/main/scala/com/yahoo/container/di/package.scala41
-rw-r--r--container-di/src/test/java/com/yahoo/component/ComponentSpecTestCase.java75
-rw-r--r--container-di/src/test/java/com/yahoo/component/provider/test/ComponentRegistryTestCase.java93
-rw-r--r--container-di/src/test/java/com/yahoo/component/test/ComponentIdTestCase.java39
-rw-r--r--container-di/src/test/java/demo/Base.java66
-rw-r--r--container-di/src/test/java/demo/ComponentConfigTest.java48
-rw-r--r--container-di/src/test/java/demo/ComponentRegistryTest.java42
-rw-r--r--container-di/src/test/java/demo/ContainerTestBase.java74
-rw-r--r--container-di/src/test/java/demo/DeconstructTest.java35
-rw-r--r--container-di/src/test/java/demo/FallbackToGuiceInjectorTest.java104
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala88
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala350
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala49
-rw-r--r--container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala538
-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
-rw-r--r--container-di/src/test/vespa-configdef/bootstrap1.def5
-rw-r--r--container-di/src/test/vespa-configdef/bootstrap2.def6
-rw-r--r--container-di/src/test/vespa-configdef/components1.def5
-rw-r--r--container-di/src/test/vespa-configdef/int.def6
-rw-r--r--container-di/src/test/vespa-configdef/string.def6
-rw-r--r--container-di/src/test/vespa-configdef/test.def6
-rw-r--r--container-di/src/test/vespa-configdef/test2.def6
-rw-r--r--container-di/src/test/vespa-configdef/thread-pool.def6
59 files changed, 4252 insertions, 0 deletions
diff --git a/container-di/.gitignore b/container-di/.gitignore
new file mode 100644
index 00000000000..3cc25b51fc4
--- /dev/null
+++ b/container-di/.gitignore
@@ -0,0 +1,2 @@
+/pom.xml.build
+/target
diff --git a/container-di/OWNERS b/container-di/OWNERS
new file mode 100644
index 00000000000..3b2ba1ede81
--- /dev/null
+++ b/container-di/OWNERS
@@ -0,0 +1 @@
+gjoranv
diff --git a/container-di/benchmarks/src/test/java/com/yahoo/component/ComponentIdBenchmark.java b/container-di/benchmarks/src/test/java/com/yahoo/component/ComponentIdBenchmark.java
new file mode 100644
index 00000000000..e89d98941be
--- /dev/null
+++ b/container-di/benchmarks/src/test/java/com/yahoo/component/ComponentIdBenchmark.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+/**
+ * @author balder
+ */
+public class ComponentIdBenchmark {
+ public void run() {
+ boolean result=true;
+ String strings[] = createStrings(1000);
+ // Warm-up
+ out("Warming up...");
+ for (int i=0; i<30*1000; i++)
+ result = result ^ createComponentId(strings);
+
+ long startTime=System.currentTimeMillis();
+ out("Running...");
+ for (int i=0; i<100*1000; i++)
+ result = result ^ createComponentId(strings);
+ out("Ignore this: " + result); // Make sure we are not fooled by optimization by creating an observable result
+ long endTime=System.currentTimeMillis();
+ out("Create anonymous component ids of 1000 strings 100.000 times took " + (endTime-startTime) + " ms");
+ }
+
+ private final String [] createStrings(int num) {
+ String strings [] = new String [num];
+ for(int i=0; i < strings.length; i++) {
+ strings[i] = "this.is.a.short.compound.name." + i;
+ }
+ return strings;
+ }
+
+ private final boolean createComponentId(String [] strings) {
+ boolean retval = true;
+ for (int i=0; i < strings.length; i++) {
+ ComponentId n = ComponentId.createAnonymousComponentId(strings[i]);
+ retval = retval ^ n.isAnonymous();
+ }
+ return retval;
+ }
+
+ private void out(String string) {
+ System.out.println(string);
+ }
+
+ public static void main(String[] args) {
+ new ComponentIdBenchmark().run();
+ }
+
+}
diff --git a/container-di/pom.xml b/container-di/pom.xml
new file mode 100644
index 00000000000..f78cae8f434
--- /dev/null
+++ b/container-di/pom.xml
@@ -0,0 +1,226 @@
+<?xml version="1.0"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>container-di</artifactId>
+ <version>6-SNAPSHOT</version>
+ <packaging>jar</packaging>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>annotations</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-library</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>provided-dependencies</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>scalalib</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>org.scala-lang</groupId>
+ <artifactId>scala-compiler</artifactId>
+ <scope>test</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>
+ <exclusions>
+ <exclusion>
+ <groupId>log4j</groupId>
+ <artifactId>log4j</artifactId>
+ </exclusion>
+ </exclusions>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-bundle</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.inject</groupId>
+ <artifactId>guice</artifactId>
+ <classifier>no_aop</classifier>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>component</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-class-plugin</artifactId>
+ <version>${project.version}</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>config-gen</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>configgen-test-defs</id>
+ <phase>generate-test-sources</phase>
+ <goals>
+ <goal>config-gen</goal>
+ </goals>
+ <configuration>
+ <defFilesDirectories>src/test/vespa-configdef</defFilesDirectories>
+ <outputDirectory>target/generated-test-sources/vespa-configgen-plugin</outputDirectory>
+ <testConfig>true</testConfig>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.scala-tools</groupId>
+ <artifactId>maven-scala-plugin</artifactId>
+ <version>2.14.1</version>
+ <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-surefire-plugin</artifactId>
+ <configuration>
+ <forkMode>once</forkMode>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ <profiles>
+ <profile>
+ <id>scalacoverage</id>
+ <repositories>
+ <repository>
+ <id>scct repository</id>
+ <url>http://mtkopone.github.com/scct/maven-repo</url>
+ </repository>
+ </repositories>
+ <dependencies>
+ <dependency>
+ <groupId>reaktor</groupId>
+ <artifactId>scct_2.9.1</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-surefire-plugin</artifactId>
+ <configuration>
+ <systemPropertyVariables>
+ <scct.project.name>${project.name}</scct.project.name>
+ <scct.coverage.file>${project.build.outputDirectory}/coverage.data</scct.coverage.file>
+ <scct.report.dir>${project.build.directory}/coverage-report</scct.report.dir>
+ <scct.source.dir>${project.build.sourceDirectory}</scct.source.dir>
+ </systemPropertyVariables>
+ <redirectTestOutputToFile>${test.hide}</redirectTestOutputToFile>
+ <forkMode>once</forkMode>
+ </configuration>
+ </plugin>
+ <plugin>
+ <groupId>org.scala-tools</groupId>
+ <artifactId>maven-scala-plugin</artifactId>
+ <configuration>
+ <compilerPlugins>
+ <compilerPlugin>
+ <groupId>reaktor</groupId>
+ <artifactId>scct_2.9.1</artifactId>
+ <version>0.1-SNAPSHOT</version>
+ </compilerPlugin>
+ </compilerPlugins>
+ </configuration>
+ </plugin>
+ </plugins>
+ </build>
+ </profile>
+ </profiles>
+</project>
diff --git a/container-di/src/main/java/com/yahoo/container/bundle/BundleInstantiationSpecification.java b/container-di/src/main/java/com/yahoo/container/bundle/BundleInstantiationSpecification.java
new file mode 100644
index 00000000000..f2d4072d34c
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/bundle/BundleInstantiationSpecification.java
@@ -0,0 +1,86 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.bundle;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import net.jcip.annotations.Immutable;
+
+
+/**
+ * Specifies how a component should be instantiated from a bundle.
+ *
+ * @author tonytv
+ */
+@Immutable
+public final class BundleInstantiationSpecification {
+
+ public final ComponentId id;
+ public final ComponentSpecification classId;
+ public final ComponentSpecification bundle;
+
+ public BundleInstantiationSpecification(ComponentSpecification id, ComponentSpecification classId, ComponentSpecification bundle) {
+ this.id = id.toId();
+ this.classId = (classId != null) ? classId : id.withoutNamespace();
+ this.bundle = (bundle != null) ? bundle : this.classId;
+ }
+
+ // Must only be used when classId != null, otherwise the id must be handled as a ComponentSpecification
+ // (since converting a spec string to a ComponentId and then to a ComponentSpecification causes loss of information).
+ public BundleInstantiationSpecification(ComponentId id, ComponentSpecification classId, ComponentSpecification bundle) {
+ this(id.toSpecification(), classId, bundle);
+ assert (classId!= null);
+ }
+
+ private static final String defaultInternalBundle = "container-search-and-docproc";
+
+ private static BundleInstantiationSpecification getInternalSpecificationFromString(String idSpec, String classSpec) {
+ return new BundleInstantiationSpecification(
+ new ComponentSpecification(idSpec),
+ (classSpec == null || classSpec.isEmpty())? null : new ComponentSpecification(classSpec),
+ new ComponentSpecification(defaultInternalBundle));
+ }
+
+ public static BundleInstantiationSpecification getInternalSearcherSpecification(ComponentSpecification idSpec,
+ ComponentSpecification classSpec) {
+ return new BundleInstantiationSpecification(idSpec, classSpec, new ComponentSpecification(defaultInternalBundle));
+ }
+
+ // TODO: These are the same for now because they are in the same bundle.
+ public static BundleInstantiationSpecification getInternalHandlerSpecificationFromStrings(String idSpec, String classSpec) {
+ return getInternalSpecificationFromString(idSpec, classSpec);
+ }
+
+ public static BundleInstantiationSpecification getInternalProcessingSpecificationFromStrings(String idSpec, String classSpec) {
+ return getInternalSpecificationFromString(idSpec, classSpec);
+ }
+
+ public static BundleInstantiationSpecification getInternalSearcherSpecificationFromStrings(String idSpec, String classSpec) {
+ return getInternalSpecificationFromString(idSpec, classSpec);
+ }
+
+ public static BundleInstantiationSpecification getFromStrings(String idSpec, String classSpec, String bundleSpec) {
+ return new BundleInstantiationSpecification(
+ new ComponentSpecification(idSpec),
+ (classSpec == null || classSpec.isEmpty())? null : new ComponentSpecification(classSpec),
+ (bundleSpec == null || bundleSpec.isEmpty())? null : new ComponentSpecification(bundleSpec));
+
+ }
+
+ /**
+ * Return a new instance of the specification with bundle name altered
+ * @param bundleName New name of bundle
+ * @return the new instance of the specification.
+ */
+ public BundleInstantiationSpecification inBundle(String bundleName) {
+ return new BundleInstantiationSpecification(this.id, this.classId, new ComponentSpecification(bundleName));
+ }
+
+ public String getClassName() {
+ return classId.getName();
+ }
+
+ public BundleInstantiationSpecification nestInNamespace(ComponentId namespace) {
+ return new BundleInstantiationSpecification(id.nestInNamespace(namespace), classId, bundle);
+ }
+
+}
diff --git a/container-di/src/main/java/com/yahoo/container/bundle/package-info.java b/container-di/src/main/java/com/yahoo/container/bundle/package-info.java
new file mode 100644
index 00000000000..4e409e5da06
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/bundle/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.container.bundle;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-di/src/main/java/com/yahoo/container/di/ComponentDeconstructor.java b/container-di/src/main/java/com/yahoo/container/di/ComponentDeconstructor.java
new file mode 100644
index 00000000000..8681d0bc595
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/ComponentDeconstructor.java
@@ -0,0 +1,10 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di;
+
+/**
+ * @author gjoranv
+ * @author tonytv
+ */
+public interface ComponentDeconstructor {
+ void deconstruct(Object component);
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/Provider.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/Provider.java
new file mode 100644
index 00000000000..38482371455
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/Provider.java
@@ -0,0 +1,25 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph;
+
+/**
+ * <p>Provides a component of the parameter type T.
+ * If (and only if) dependency injection does not have a component of type T,
+ * it will request one from the Provider providing type T.</p>
+ *
+ * <p>Providers are useful in these situations:</p>
+ * <ul>
+ * <li>Some code is needed to create the component instance in question.</li>
+ * <li>The component creates resources that must be deconstructed.</li>
+ * <li>A fallback component should be provided in case the application (or system)
+ * does not provide a component instance.</li>
+ * </ul>
+ *
+ * @author tonytv
+ * @author gjoranv
+ */
+public interface Provider<T> {
+
+ T get();
+ void deconstruct();
+
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/package-info.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/package-info.java
new file mode 100644
index 00000000000..545b7dabc05
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.container.di.componentgraph.core;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java
new file mode 100644
index 00000000000..e0333aa8b44
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java
@@ -0,0 +1,7 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+@PublicApi
+package com.yahoo.container.di.componentgraph;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java b/container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java
new file mode 100644
index 00000000000..d4fb1df397a
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java
@@ -0,0 +1,12 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.config;
+
+/**
+ * @author gjoranv
+ * @since 5.17
+ */
+public class ResolveDependencyException extends RuntimeException {
+ public ResolveDependencyException(String message) {
+ super(message);
+ }
+}
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
new file mode 100644
index 00000000000..dcedd442b19
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java
@@ -0,0 +1,101 @@
+// Copyright 2016 Yahoo Inc. 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;
+import com.google.inject.Inject;
+import com.google.inject.Key;
+import com.yahoo.component.ComponentId;
+import org.osgi.framework.Version;
+
+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;
+
+/**
+ * Only for internal JDisc use.
+ *
+ * @author gjoranv
+ * @since 5.11
+ */
+public class RestApiContext {
+
+ private final List<BundleInfo> bundles = new ArrayList<>();
+ private final List<Injectable> injectableComponents = new ArrayList<>();
+
+ public final JerseyBundlesConfig bundlesConfig;
+ public final JerseyInjectionConfig injectionConfig;
+
+ @Inject
+ public RestApiContext(JerseyBundlesConfig bundlesConfig, JerseyInjectionConfig injectionConfig) {
+ this.bundlesConfig = bundlesConfig;
+ this.injectionConfig = injectionConfig;
+ }
+
+ public List<BundleInfo> getBundles() {
+ return Collections.unmodifiableList(bundles);
+ }
+
+ public void addBundle(BundleInfo bundle) {
+ bundles.add(bundle);
+ }
+
+ public List<Injectable> getInjectableComponents() {
+ return Collections.unmodifiableList(injectableComponents);
+ }
+
+ public void addInjectableComponent(Key<?> key, ComponentId id, Object component) {
+ injectableComponents.add(new Injectable(key, id, component));
+ }
+
+ public static class Injectable {
+ public final Key<?> key;
+ public final ComponentId id;
+ public final Object instance;
+
+ public Injectable(Key<?> key, ComponentId id, Object instance) {
+ this.key = key;
+ this.id = id;
+ this.instance = instance;
+ }
+ @Override
+ public String toString() {
+ return id.toString();
+ }
+ }
+
+ public static class BundleInfo {
+ public final String symbolicName;
+ public final Version version;
+ public final String fileLocation;
+ public final URL webInfUrl;
+ public final ClassLoader classLoader;
+
+ private Set<String> classEntries;
+
+ public BundleInfo(String symbolicName, Version version, String fileLocation, URL webInfUrl, ClassLoader classLoader) {
+ this.symbolicName = symbolicName;
+ this.version = version;
+ this.fileLocation = fileLocation;
+ this.webInfUrl = webInfUrl;
+ this.classLoader = classLoader;
+ }
+
+ @Override
+ public String toString() {
+ return symbolicName + ":" + version;
+ }
+
+ public void setClassEntries(Collection<String> entries) {
+ this.classEntries = ImmutableSet.copyOf(entries);
+ }
+
+ public Set<String> getClassEntries() {
+ return classEntries;
+ }
+ }
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java b/container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java
new file mode 100644
index 00000000000..fe81e510148
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java
@@ -0,0 +1,21 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.config;
+
+import com.yahoo.config.ConfigInstance;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.util.Map;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public interface Subscriber {
+ long waitNextGeneration();
+ long generation();
+
+ boolean configChanged();
+ Map<ConfigKey<ConfigInstance>, ConfigInstance> config();
+
+ void close();
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java b/container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java
new file mode 100644
index 00000000000..194435d6945
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java
@@ -0,0 +1,18 @@
+// Copyright 2016 Yahoo Inc. 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.inject.ProvidedBy;
+import com.yahoo.container.di.CloudSubscriberFactory;
+import com.yahoo.vespa.config.ConfigKey;
+
+import java.util.Set;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+@ProvidedBy(CloudSubscriberFactory.Provider.class)
+public interface SubscriberFactory {
+ Subscriber getSubscriber(Set<? extends ConfigKey<?>> configKeys);
+ void reloadActiveSubscribers(long generation);
+}
diff --git a/container-di/src/main/java/com/yahoo/container/di/config/package-info.java b/container-di/src/main/java/com/yahoo/container/di/config/package-info.java
new file mode 100644
index 00000000000..0a04308157a
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/config/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.container.di.config;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-di/src/main/java/com/yahoo/container/di/osgi/package-info.java b/container-di/src/main/java/com/yahoo/container/di/osgi/package-info.java
new file mode 100644
index 00000000000..fc7e48bc6ef
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/container/di/osgi/package-info.java
@@ -0,0 +1,8 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+/**
+ * @author tonytv
+ */
+@ExportPackage
+package com.yahoo.container.di.osgi;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java b/container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java
new file mode 100644
index 00000000000..52719382b4f
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java
@@ -0,0 +1,50 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.osgi.provider.model;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import net.jcip.annotations.Immutable;
+
+/**
+ * Describes how a component should be created.
+ *
+ * @author gjoranv
+ */
+@Immutable
+public class ComponentModel {
+
+ public final BundleInstantiationSpecification bundleInstantiationSpec;
+ public final String configId; // only used in the container, null when used in the model
+
+ public ComponentModel(BundleInstantiationSpecification bundleInstantiationSpec, String configId) {
+ if (bundleInstantiationSpec == null)
+ throw new IllegalArgumentException("Null bundle instantiation spec!");
+
+ this.bundleInstantiationSpec = bundleInstantiationSpec;
+ this.configId = configId;
+ }
+
+ public ComponentModel(String idSpec, String classSpec, String bundleSpec, String configId) {
+ this(BundleInstantiationSpecification.getFromStrings(idSpec, classSpec, bundleSpec), configId);
+ }
+
+ // For vespamodel
+ public ComponentModel(BundleInstantiationSpecification bundleInstantiationSpec) {
+ this(bundleInstantiationSpec, null);
+ }
+
+ // For vespamodel
+ public ComponentModel(String idSpec, String classSpec, String bundleSpec) {
+ this(BundleInstantiationSpecification.getFromStrings(idSpec, classSpec, bundleSpec));
+ }
+
+ public ComponentId getComponentId() {
+ return bundleInstantiationSpec.id;
+ }
+
+ public ComponentSpecification getClassId() {
+ return bundleInstantiationSpec.classId;
+ }
+
+}
diff --git a/container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java b/container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java
new file mode 100644
index 00000000000..6f201e049f8
--- /dev/null
+++ b/container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.osgi.provider.model;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/container-di/src/main/resources/configdefinitions/bundles.def b/container-di/src/main/resources/configdefinitions/bundles.def
new file mode 100644
index 00000000000..b53a34581ba
--- /dev/null
+++ b/container-di/src/main/resources/configdefinitions/bundles.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+namespace=container
+
+# References to all 3rd-party bundles to be installed.
+bundle[] file
diff --git a/container-di/src/main/resources/configdefinitions/components.def b/container-di/src/main/resources/configdefinitions/components.def
new file mode 100644
index 00000000000..f311648f561
--- /dev/null
+++ b/container-di/src/main/resources/configdefinitions/components.def
@@ -0,0 +1,24 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=3
+namespace=container
+
+## A list of components. Components depending on other components may use this to
+## get its list of components injected.
+
+## A component
+components[].id string
+## The component id used by this component to subscribe to its configs (if any)
+components[].configId reference default=":parent:"
+
+## The id of the class to instantiate for this component.
+components[].classId string default=""
+
+## The symbolic name of the Osgi bundle this component is located in.
+## Assumed to be the same as the classid if not set.
+components[].bundle string default=""
+
+## The component id of the component to inject to this component
+components[].inject[].id string
+
+## The name to use for the injected component when injected to this component
+components[].inject[].name string default=""
diff --git a/container-di/src/main/resources/configdefinitions/jersey-bundles.def b/container-di/src/main/resources/configdefinitions/jersey-bundles.def
new file mode 100644
index 00000000000..460adf03b51
--- /dev/null
+++ b/container-di/src/main/resources/configdefinitions/jersey-bundles.def
@@ -0,0 +1,8 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=container.di.config
+
+# The SymbolicName[:Version] of the Jersey bundles
+bundles[].spec string
+
+# The packages to scan for Jersey resources
+bundles[].packages[] string
diff --git a/container-di/src/main/resources/configdefinitions/jersey-injection.def b/container-di/src/main/resources/configdefinitions/jersey-injection.def
new file mode 100644
index 00000000000..be5dcf5cdf0
--- /dev/null
+++ b/container-di/src/main/resources/configdefinitions/jersey-injection.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+namespace=container.di.config
+
+inject[].instance string
+inject[].forClass string
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
new file mode 100644
index 00000000000..f039a92b285
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/bundle/MockBundle.scala
@@ -0,0 +1,96 @@
+// Copyright 2016 Yahoo Inc. 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
+ * @since 5.15
+ */
+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
new file mode 100644
index 00000000000..c9206ee5e05
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/CloudSubscriberFactory.scala
@@ -0,0 +1,100 @@
+// Copyright 2016 Yahoo Inc. 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 CloudSubscriberFactory._
+import config.{Subscriber, SubscriberFactory}
+import scala.collection.JavaConversions._
+import com.yahoo.vespa.config.ConfigKey
+import scala.Some
+import com.yahoo.config.subscription.{ConfigHandle, ConfigSource, ConfigSourceSet, ConfigSubscriber}
+import java.lang.IllegalArgumentException
+import java.util.logging.Logger
+import com.yahoo.log.LogLevel
+
+
+/**
+ * @author tonytv
+ */
+
+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.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().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
+
+ 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]]
+
+ override def waitNextGeneration() = {
+ require(!handles.isEmpty)
+
+ /* Catch and ignore 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(LogLevel.DEBUG, "Ignoring exception from the config library: " + e.getMessage + "\n" + e.getStackTrace)
+ if (numExceptions >= 5)
+ throw new IllegalArgumentException("Failed retrieving the next config generation.", e)
+ }
+ }
+
+ generation = subscriber.getGeneration
+ 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
new file mode 100644
index 00000000000..a1b8167171a
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/ConfigRetriever.scala
@@ -0,0 +1,101 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di
+
+
+import config.Subscriber
+import java.util.logging.Logger
+import com.yahoo.log.LogLevel
+import ConfigRetriever._
+import annotation.tailrec
+import com.yahoo.config.ConfigInstance
+import scala.collection.JavaConversions._
+import com.yahoo.vespa.config.ConfigKey
+
+/**
+ * @author tonytv
+ * @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()
+
+
+ @tailrec
+ final def getConfigs(componentConfigKeys: Set[ConfigKeyT], leastGeneration: Long): ConfigSnapshot = {
+ require(componentConfigKeys intersect bootstrapKeys isEmpty)
+ log.log(LogLevel.DEBUG, "getConfigs: " + componentConfigKeys)
+
+ setupComponentSubscriber(componentConfigKeys ++ bootstrapKeys)
+
+ getConfigsOptional(leastGeneration) match {
+ case Some(snapshot) => resetComponentSubscriberIfBootstrap(snapshot); snapshot
+ case None => getConfigs(componentConfigKeys, leastGeneration)
+ }
+ }
+
+ private def getConfigsOptional(leastGeneration: Long): Option[ConfigSnapshot] = {
+ val newestComponentGeneration = componentSubscriber.waitNextGeneration()
+
+ if (newestComponentGeneration < leastGeneration) {
+ None
+ } else if (bootstrapSubscriber.generation < newestComponentGeneration) {
+ val newestBootstrapGeneration = bootstrapSubscriber.waitNextGeneration()
+ bootstrapConfigIfChanged() orElse {
+ if (newestBootstrapGeneration == newestComponentGeneration) componentsConfigIfChanged()
+ else None
+ }
+ } else {
+ 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.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
+ componentSubscriber = subscribe(keys)
+ }
+ }
+
+ 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
new file mode 100644
index 00000000000..28d99f89d73
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/Container.scala
@@ -0,0 +1,200 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di
+
+import com.yahoo.container.di.ConfigRetriever.{ComponentsConfigs, BootstrapConfigs}
+import com.yahoo.container.di.componentgraph.core.{JerseyNode, ComponentGraph, ComponentNode}
+import com.yahoo.container.di.config.{RestApiContext, SubscriberFactory}
+import Container._
+
+import scala.collection.JavaConversions._
+import scala.math.max
+import com.yahoo.config._
+import com.yahoo.vespa.config.ConfigKey
+import java.util.IdentityHashMap
+import java.util.logging.Logger
+import com.yahoo.container.bundle.BundleInstantiationSpecification
+import com.google.inject.{Injector, Guice}
+import com.yahoo.container.{BundlesConfig, ComponentsConfig}
+
+
+/**
+ *
+ * @author gjoranv
+ * @author tonytv
+ */
+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), subscriberFactory.getSubscriber(_))
+ var previousConfigGeneration = -1L
+ var leastGeneration = -1L
+
+ @throws(classOf[InterruptedException])
+ def runOnce(
+ oldGraph: ComponentGraph = new ComponentGraph,
+ fallbackInjector: GuiceInjector = Guice.createInjector()): 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 foreach(componentDeconstructor.deconstruct(_))
+ }
+
+ try {
+ //TODO: wrap user exceptions.
+ val newGraph = createNewGraph(oldGraph, fallbackInjector)
+ newGraph.reuseNodes(oldGraph)
+ constructComponents(newGraph)
+ deconstructObsoleteComponents(oldGraph, newGraph)
+ newGraph
+ } catch {
+ case e : Throwable =>
+ invalidateGeneration()
+ throw e
+ }
+ }
+
+ private def invalidateGeneration() {
+ leastGeneration = max(configurer.getComponentsGeneration, configurer.getBootstrapGeneration) + 1
+ }
+
+ final def createNewGraph(graph: ComponentGraph = new ComponentGraph,
+ fallbackInjector: Injector): ComponentGraph = {
+
+ val snapshot = configurer.getConfigs(graph.configKeys, leastGeneration)
+ log.fine("""createNewGraph:
+ graph.configKeys = %s
+ graph.generation = %s
+ snapshot = %s
+ """.format(graph.configKeys, graph.generation, snapshot))
+
+ val preventTailRecursion =
+ snapshot match {
+ case BootstrapConfigs(configs) if getBootstrapGeneration > previousConfigGeneration =>
+ installBundles(configs)
+ createNewGraph(
+ createComponentsGraph(configs, getBootstrapGeneration,fallbackInjector),
+ fallbackInjector)
+ case BootstrapConfigs(_) =>
+ createNewGraph(graph, fallbackInjector)
+ case ComponentsConfigs(configs) =>
+ 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()
+ inject <- component.inject()
+ } {
+ 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)
+ 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) {
+ 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
new file mode 100644
index 00000000000..6752559fde6
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/Osgi.scala
@@ -0,0 +1,40 @@
+// Copyright 2016 Yahoo Inc. 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 tonytv
+ */
+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
new file mode 100644
index 00000000000..fe0aa2fd001
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentGraph.scala
@@ -0,0 +1,330 @@
+// Copyright 2016 Yahoo Inc. 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 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
+
+/**
+ * @author tonytv
+ * @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[AnyRef] => clazz
+ case typeVariable: TypeVariable[_] =>
+ throw new RuntimeException("Can't create ComponentRegistry of unknown type variable " + typeVariable)
+ }
+
+ componentRegistryNodes.find(_.componentClass == componentType).
+ getOrElse(newComponentRegistryNode(componentClass))
+ }
+
+ 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.info("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
new file mode 100644
index 00000000000..61f3c7a049c
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentNode.scala
@@ -0,0 +1,205 @@
+// Copyright 2016 Yahoo Inc. 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.{Modifier, ParameterizedType, Constructor, Type, InvocationTargetException}
+import java.util.logging.Logger
+import com.google.inject.Inject
+
+import com.yahoo.config.ConfigInstance
+import com.yahoo.vespa.config.ConfigKey
+import com.yahoo.component.{ComponentId, AbstractComponent}
+import com.yahoo.container.di.{ConfigKeyT, JavaAnnotation, createKey, makeClassCovariant, removeStackTrace, preserveStackTrace}
+import com.yahoo.container.di.componentgraph.Provider
+
+import Node.equalEdges
+import ComponentNode._
+import java.lang.IllegalStateException
+import scala.Some
+import scala.Array
+
+/**
+ * @author tonytv
+ * @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(new RuntimeException(s"An exception occurred while constructing $idAndType",
+ cutStackTraceAtConstructor(e.getCause)))
+
+ }
+
+ initId(instance)
+ }
+
+ private def initId(component: AnyRef) = {
+ def checkAndSetId(c: AbstractComponent) {
+ if (c.hasInitializedId && c.getId != componentId )
+ throw new IllegalStateException("Component with id '" + componentId + "' has set a bogus component id: '" + c.getId + "'")
+
+ 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 _ => throwRuntimeExceptionRemoveStackTrace("Multiple constructors annotated with inject in class " + clazz.getName)
+ }
+ }
+
+ def constructorWithMostConfigParameters = {
+ def isConfigInstance(clazz: Class[_]) = classOf[ConfigInstance].isAssignableFrom(clazz)
+
+ publicConstructors match {
+ case Array() => throwRuntimeExceptionRemoveStackTrace("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 throwRuntimeExceptionRemoveStackTrace(message: String) =
+ throw removeStackTrace(new RuntimeException(message))
+
+
+ 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
new file mode 100644
index 00000000000..864eb17ddfb
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.scala
@@ -0,0 +1,71 @@
+// Copyright 2016 Yahoo Inc. 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 tonytv
+ * @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/DotGraph.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/DotGraph.scala
new file mode 100644
index 00000000000..d31c78af0d1
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/DotGraph.scala
@@ -0,0 +1,46 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core
+
+/**
+ * Generate dot graph from a ComponentGraph
+ *
+ * @author tonytv
+ * @author gjoranv
+ * @since 5.1.4
+ */
+
+object DotGraph {
+
+ def generate(graph: ComponentGraph): String = {
+ val nodes = graph.nodes map(node)
+
+ val edges = for {
+ node <- graph.nodes
+ usedNode <- node.usedComponents
+ } yield edge(node, usedNode)
+
+ (nodes ++ edges).
+ mkString(
+ """|digraph {
+ | graph [ratio="compress"];
+ | """.stripMargin, "\n ", "\n}")
+ }
+
+ private def label(node: Node) = {
+ node.label.replace("\n", "\\n")
+ }
+
+ private def node(node: Node): String = {
+ <node>
+ "{node.componentId.stringValue()}"[shape=record, fontsize=11, label="{label(node)}"];
+ </node>.text.trim
+ }
+
+ private def edge(node: Node, usedNode:Node): String = {
+ <edge>
+ "{node.componentId.stringValue()}"->"{usedNode.componentId.stringValue()}";
+ </edge>.text.trim
+
+ }
+
+}
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
new file mode 100644
index 00000000000..f96284d5924
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/GuiceNode.scala
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. 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 tonytv
+ * @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
new file mode 100644
index 00000000000..3831e8f3404
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/JerseyNode.scala
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. 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.{ComponentSpecification, ComponentId}
+import com.yahoo.container.di.Osgi.RelativePath
+import com.yahoo.container.di.config.RestApiContext
+import org.osgi.framework.wiring.BundleWiring
+import scala.collection.JavaConverters._
+
+import scala.collection.convert.wrapAsJava._
+import RestApiContext.BundleInfo
+import JerseyNode._
+import com.yahoo.container.di.Osgi
+import org.osgi.framework.Bundle
+import java.net.URL
+
+/**
+ * Represents an instance of RestApiContext
+ *
+ * @author gjoranv
+ * @author tonytv
+ * @since 5.15
+ */
+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)
+ 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
new file mode 100644
index 00000000000..9d30552b0aa
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/Node.scala
@@ -0,0 +1,111 @@
+// Copyright 2016 Yahoo Inc. 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.container.di.ConfigKeyT
+import com.yahoo.container.di.componentgraph.Provider
+import com.google.inject.Key
+import Node._
+
+
+
+/**
+ * @author tonytv
+ * @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 = {
+ component(
+ instance.getOrElse {
+ instance = Some(newInstance())
+ instance.get
+ })
+ }
+
+ 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 {
+
+ 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
new file mode 100644
index 00000000000..dee95b84fe3
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/osgi/OsgiUtil.scala
@@ -0,0 +1,135 @@
+// Copyright 2016 Yahoo Inc. 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.yahoo.component.ComponentSpecification
+import com.yahoo.container.di.Osgi.RelativePath
+import com.yahoo.vespa.scalalib.arm.Using.using
+import com.yahoo.vespa.scalalib.osgi.maven.ProjectBundleClassPaths
+import com.yahoo.vespa.scalalib.osgi.maven.ProjectBundleClassPaths.BundleClasspathMapping
+import org.osgi.framework.Bundle
+import org.osgi.framework.wiring.BundleWiring
+
+import com.google.common.io.Files.fileTreeTraverser
+
+import scala.collection.convert.decorateAsScala._
+
+import com.yahoo.vespa.scalalib.java.function.FunctionConverters._
+
+/**
+ * Tested by com.yahoo.application.container.jersey.JerseyTest
+ * @author tonytv
+ */
+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,
+ 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)
+ }
+ }
+
+ 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.classPathMappingsFileName)
+ if (classPathMappingsFileLocation == null)
+ throw new RuntimeException(s"Couldn't find ${ProjectBundleClassPaths.classPathMappingsFileName} 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))
+
+ using(new JarFile(jarPath.toFile)) { jarFile =>
+ jarFile.stream().
+ map[String] { entry: JarEntry => entry.getName}.
+ filter { name: String => name.endsWith(classFileTypeSuffix)}.
+ filter(acceptedPackage).
+ collect(Collectors.toList()).
+ asScala
+ }
+ }
+
+ def packageToPath(packageName: String) = packageName.replaceAllLiterally(".", "/")
+}
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
new file mode 100644
index 00000000000..f1e2a35797e
--- /dev/null
+++ b/container-di/src/main/scala/com/yahoo/container/di/package.scala
@@ -0,0 +1,41 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container
+
+import com.yahoo.config.ConfigInstance
+import com.yahoo.vespa.config.ConfigKey
+import java.lang.reflect.Type
+import com.google.inject.Key
+
+/**
+ *
+ * @author gjoranv
+ * @author tonytv
+ */
+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: RuntimeException): RuntimeException = {
+ if (preserveStackTrace) exception
+ else {
+ exception.setStackTrace(Array())
+ exception
+ }
+ }
+
+ //For debug purposes only
+ val preserveStackTrace: Boolean = Option(System.getProperty("jdisc.container.preserveStackTrace")).filterNot(_.isEmpty).isDefined
+} \ No newline at end of file
diff --git a/container-di/src/test/java/com/yahoo/component/ComponentSpecTestCase.java b/container-di/src/test/java/com/yahoo/component/ComponentSpecTestCase.java
new file mode 100644
index 00000000000..4b0c8fca4d1
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/component/ComponentSpecTestCase.java
@@ -0,0 +1,75 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+import junit.framework.TestCase;
+
+/**
+ * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a>
+ */
+public class ComponentSpecTestCase extends TestCase {
+
+ public void testMatches() {
+ ComponentId a = new ComponentId("test:1");
+ ComponentId b = new ComponentId("test:1.1.1");
+ ComponentId c = new ComponentId("test:2");
+ ComponentId d = new ComponentId("test:3");
+ ComponentId e = new ComponentId("test");
+
+ ComponentSpecification aspec = new ComponentSpecification("test");
+ ComponentSpecification bspec = new ComponentSpecification("test:1");
+ ComponentSpecification cspec = new ComponentSpecification("test:2");
+ ComponentSpecification dspec = new ComponentSpecification("test1:2");
+
+ assertTrue(aspec.matches(a));
+ assertTrue(aspec.matches(b));
+ assertTrue(aspec.matches(c));
+ assertTrue(aspec.matches(d));
+ assertTrue(aspec.matches(e));
+
+ assertTrue(bspec.matches(a));
+ assertTrue(bspec.matches(b));
+ assertFalse(bspec.matches(c));
+ assertFalse(bspec.matches(d));
+ assertFalse(bspec.matches(e));
+
+ assertFalse(cspec.matches(a));
+ assertFalse(cspec.matches(b));
+ assertTrue(cspec.matches(c));
+ assertFalse(cspec.matches(d));
+ assertFalse(cspec.matches(e));
+
+ assertFalse(dspec.matches(a));
+ assertFalse(dspec.matches(b));
+ assertFalse(dspec.matches(c));
+ assertFalse(dspec.matches(d));
+ assertFalse(dspec.matches(e));
+
+ }
+
+ public void testMatchesWithNamespace() {
+ ComponentId namespace = new ComponentId("namespace:2");
+
+ ComponentId a = new ComponentId("test", new Version(1), namespace);
+ ComponentId b = new ComponentId("test:1@namespace:2");
+ ComponentId c = new ComponentId("test:1@namespace");
+ assertEquals(a, b);
+ assertFalse(a.equals(c));
+
+ ComponentSpecification spec = new ComponentSpecification("test", null, namespace);
+ assertTrue(spec.matches(a));
+ assertTrue(spec.matches(b));
+ assertFalse(spec.matches(c));
+ }
+
+ public void testStringValue() {
+ assertStringValueEqualsInputSpec("a:1.0.0.alpha@namespace");
+ assertStringValueEqualsInputSpec("a:1.0.0.alpha");
+ assertStringValueEqualsInputSpec("a:1.0");
+ assertStringValueEqualsInputSpec("a");
+ }
+
+ private void assertStringValueEqualsInputSpec(String componentSpec) {
+ assertEquals(componentSpec,
+ new ComponentSpecification(componentSpec).stringValue());
+ }
+}
diff --git a/container-di/src/test/java/com/yahoo/component/provider/test/ComponentRegistryTestCase.java b/container-di/src/test/java/com/yahoo/component/provider/test/ComponentRegistryTestCase.java
new file mode 100644
index 00000000000..b14d56dd31d
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/component/provider/test/ComponentRegistryTestCase.java
@@ -0,0 +1,93 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.provider.ComponentRegistry;
+
+/**
+ * Tests that ComponentRegistry handles namespaces correctly.
+ * @author tonytv
+ */
+public class ComponentRegistryTestCase {
+ private static class TestComponent extends AbstractComponent {
+ TestComponent(ComponentId componentId) {
+ super(componentId);
+ }
+ }
+
+ private static final String componentName = "component";
+
+ private static final String namespace1 = "namespace1";
+ private static final String namespace2 = "namespace2";
+ private static final String namespace21 = "namespace2:1";
+
+ private static final TestComponent component1 = componentInNamespace(namespace1);
+ private static final TestComponent component2 = componentInNamespace(namespace2);
+ private static final TestComponent component21 = componentInNamespace(namespace21);
+
+ private ComponentRegistry<TestComponent> registry;
+
+ private static ComponentSpecification specInNamespace(String namespace) {
+ return new ComponentSpecification(componentName + "@" + namespace);
+ }
+
+ private static ComponentId idInNamespace(String namespace) {
+ return specInNamespace(namespace).toId();
+ }
+
+ private static TestComponent componentInNamespace(String namespace) {
+ return new TestComponent(idInNamespace(namespace));
+ }
+
+ @Before
+ public void before() {
+ registry = new ComponentRegistry<>();
+
+ registry.register(component1.getId(), component1);
+ registry.register(component2.getId(), component2);
+ registry.register(component21.getId(), component21);
+ }
+
+ @Test
+ public void testAllPresent() {
+ assertEquals(3, registry.getComponentCount());
+ }
+
+ @Test
+ public void testIdNamespaceLookup() {
+ assertEquals(component1, registry.getComponent(idInNamespace(namespace1)));
+ assertEquals(component2, registry.getComponent(idInNamespace(namespace2)));
+ assertEquals(component21, registry.getComponent(idInNamespace(namespace21)));
+ }
+
+ @Test
+ public void testSpecNamespaceLookup() {
+ assertEquals(component1, registry.getComponent(specInNamespace(namespace1)));
+
+ // Version for namespace must match the specification exactly, so do not return version '1' when an
+ // empty version is asked for.
+ assertEquals(component2, registry.getComponent(specInNamespace(namespace2)));
+ assertEquals(component21, registry.getComponent(specInNamespace(namespace21)));
+ }
+
+ @Test
+ public void testInnerComponentNotMixedWithTopLevelComponent() {
+ assertNull(registry.getComponent(componentName));
+
+ TestComponent topLevel = new TestComponent(new ComponentId(componentName));
+ registry.register(topLevel.getId(), topLevel);
+ assertEquals(topLevel, registry.getComponent(componentName));
+
+ assertEquals(component1, registry.getComponent(specInNamespace(namespace1)));
+ assertEquals(component1, registry.getComponent(idInNamespace(namespace1)));
+ }
+
+}
diff --git a/container-di/src/test/java/com/yahoo/component/test/ComponentIdTestCase.java b/container-di/src/test/java/com/yahoo/component/test/ComponentIdTestCase.java
new file mode 100644
index 00000000000..7359cc8ba66
--- /dev/null
+++ b/container-di/src/test/java/com/yahoo/component/test/ComponentIdTestCase.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.test;
+
+import com.yahoo.component.ComponentId;
+import junit.framework.TestCase;
+
+/**
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class ComponentIdTestCase extends TestCase {
+
+ public void testFileNameConversion() {
+ assertFileNameEquals("a","a");
+ assertFileNameEquals("a-1","a-1");
+ assertFileNameEquals("a-1","a-1.0.0");
+ assertFileNameEquals("a-1.0.0.qualifier","a-1.0.0.qualifier");
+ assertFileNameEquals("a.b-1.0.0.qualifier","a.b-1.0.0.qualifier");
+ assertFileNameEquals("a@space","a@space");
+ assertFileNameEquals("a-1@space","a-1@space");
+ assertFileNameEquals("a-1@space","a-1.0.0@space");
+ assertFileNameEquals("a-1.0.0.qualifier@space","a-1.0.0.qualifier@space");
+ assertFileNameEquals("a.b-1.0.0.qualifier@space","a.b-1.0.0.qualifier@space");
+ }
+
+ /** Takes two id file names as input */
+ private void assertFileNameEquals(String expected,String initial) {
+ assertEquals("'" + initial + "' became id '" + ComponentId.fromFileName(initial) + "' which should become '" + expected + "'",
+ expected,ComponentId.fromFileName(initial).toFileName());
+ }
+
+ public void testCompareWithNameSpace() {
+ ComponentId withNS = ComponentId.fromString("foo@ns");
+ ComponentId withoutNS = ComponentId.fromString("foo"); // Should be less than withNs
+
+ assertEquals(withNS.compareTo(withoutNS), 1);
+ assertEquals(withoutNS.compareTo(withNS), -1);
+ }
+
+}
diff --git a/container-di/src/test/java/demo/Base.java b/container-di/src/test/java/demo/Base.java
new file mode 100644
index 00000000000..7bc2c7b4404
--- /dev/null
+++ b/container-di/src/test/java/demo/Base.java
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. 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.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;
+import com.yahoo.vespa.config.ConfigKey;
+import org.junit.Before;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class Base {
+ private ComponentGraph componentGraph;
+ private Injector injector;
+ private Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configs =
+ new HashMap<>();
+
+ @Before
+ public void createGraph() {
+ injector = Guice.createInjector();
+ componentGraph = new ComponentGraph(0);
+ }
+
+ public void register(Class<?> componentClass) {
+ componentGraph.add(mockComponentNode(componentClass));
+ }
+
+ public ComponentId toId(Class<?> componentClass) {
+ return ComponentId.fromString(componentClass.getName());
+ }
+
+ @SuppressWarnings("unchecked")
+ private Node mockComponentNode(Class<?> componentClass) {
+ return new ComponentNode(toId(componentClass), toId(componentClass).toString(), (Class<Object>)componentClass, null);
+ }
+
+ public <T> T getInstance(Class<T> componentClass) {
+ return componentGraph.getInstance(componentClass);
+ }
+
+ @SuppressWarnings("unchecked")
+ public void complete() {
+ componentGraph.complete(injector);
+ componentGraph.setAvailableConfigs(ContainerTest.convertMap(configs));
+ }
+
+ public void setInjector(Injector injector) {
+ this.injector = injector;
+ }
+
+ @SuppressWarnings("unchecked")
+ public void addConfig(ConfigInstance configInstance, ComponentId id) {
+ configs.put(new ConfigKey<>((Class<ConfigInstance>)configInstance.getClass(), id.toString()),
+ configInstance);
+ }
+}
diff --git a/container-di/src/test/java/demo/ComponentConfigTest.java b/container-di/src/test/java/demo/ComponentConfigTest.java
new file mode 100644
index 00000000000..5b3be2f549a
--- /dev/null
+++ b/container-di/src/test/java/demo/ComponentConfigTest.java
@@ -0,0 +1,48 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package demo;
+
+import com.yahoo.config.test.ThreadPoolConfig;
+import com.yahoo.container.di.componentgraph.Provider;
+import org.junit.Test;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+
+import static org.junit.Assert.assertNotNull;
+
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class ComponentConfigTest extends Base {
+ public static class ThreadPoolExecutorProvider implements Provider<Executor> {
+ private ExecutorService executor;
+
+ public ThreadPoolExecutorProvider(ThreadPoolConfig config) {
+ executor = Executors.newFixedThreadPool(config.numThreads());
+ }
+
+ @Override
+ public Executor get() {
+ return executor;
+ }
+
+ @Override
+ public void deconstruct() {
+ executor.shutdown();
+ }
+ }
+
+ @Test
+ public void require_that_non_components_can_be_configured() {
+ register(ThreadPoolExecutorProvider.class);
+ addConfig(new ThreadPoolConfig(new ThreadPoolConfig.Builder().numThreads(4)),
+ toId(ThreadPoolExecutorProvider.class));
+ complete();
+
+ Executor executor = getInstance(Executor.class);
+ assertNotNull(executor);
+ }
+}
diff --git a/container-di/src/test/java/demo/ComponentRegistryTest.java b/container-di/src/test/java/demo/ComponentRegistryTest.java
new file mode 100644
index 00000000000..193f65048ab
--- /dev/null
+++ b/container-di/src/test/java/demo/ComponentRegistryTest.java
@@ -0,0 +1,42 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package demo;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.provider.ComponentRegistry;
+import org.junit.Test;
+
+import static org.junit.Assert.assertNotNull;
+
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class ComponentRegistryTest extends Base {
+ public static class SearchHandler extends AbstractComponent {
+ private final ComponentRegistry<Searcher> searchers;
+
+ public SearchHandler(ComponentRegistry<Searcher> searchers) {
+ this.searchers = searchers;
+ }
+ }
+
+ public static class Searcher extends AbstractComponent {}
+
+ public static class FooSearcher extends Searcher {}
+ public static class BarSearcher extends Searcher {}
+
+ @Test
+ public void require_that_component_registry_can_be_injected() {
+ register(SearchHandler.class);
+ register(FooSearcher.class);
+ register(BarSearcher.class);
+ complete();
+
+ SearchHandler handler = getInstance(SearchHandler.class);
+
+ ComponentRegistry<Searcher> searchers = handler.searchers;
+ assertNotNull(searchers.getComponent(toId(FooSearcher.class)));
+ assertNotNull(searchers.getComponent(toId(BarSearcher.class)));
+ }
+}
diff --git a/container-di/src/test/java/demo/ContainerTestBase.java b/container-di/src/test/java/demo/ContainerTestBase.java
new file mode 100644
index 00000000000..fdc73913052
--- /dev/null
+++ b/container-di/src/test/java/demo/ContainerTestBase.java
@@ -0,0 +1,74 @@
+// Copyright 2016 Yahoo Inc. 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.*;
+import scala.collection.immutable.*;
+import scala.collection.immutable.Set;
+
+import java.util.Collection;
+import java.util.List;
+
+/**
+ * @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.runOnce(componentGraph, Guice.createInjector());
+ } 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
new file mode 100644
index 00000000000..10e9844f6de
--- /dev/null
+++ b/container-di/src/test/java/demo/DeconstructTest.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package demo;
+
+import com.yahoo.container.di.ContainerTest;
+import org.junit.Test;
+
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+public class DeconstructTest extends ContainerTestBase {
+ public static class DeconstructableComponent extends ContainerTest.DestructableComponent {
+ private boolean isDeconstructed = false;
+
+ @Override
+ public void deconstruct() {
+ isDeconstructed = true;
+ }
+ }
+
+ @Test
+ public void require_that_unused_components_are_deconstructed() {
+ writeBootstrapConfigs("d1", DeconstructableComponent.class);
+ complete();
+
+ DeconstructableComponent d1 = getInstance(DeconstructableComponent.class);
+
+ writeBootstrapConfigs("d2", DeconstructableComponent.class);
+ complete();
+
+ assertTrue(d1.isDeconstructed);
+ }
+}
diff --git a/container-di/src/test/java/demo/FallbackToGuiceInjectorTest.java b/container-di/src/test/java/demo/FallbackToGuiceInjectorTest.java
new file mode 100644
index 00000000000..3d5550ab0a3
--- /dev/null
+++ b/container-di/src/test/java/demo/FallbackToGuiceInjectorTest.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package demo;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+import com.yahoo.component.AbstractComponent;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertNotNull;
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+@SuppressWarnings("unused")
+public class FallbackToGuiceInjectorTest extends Base {
+ public static class MyComponent extends AbstractComponent {
+ private final String url;
+ private final Executor executor;
+
+ @Inject
+ public MyComponent(@Named("url") String url, Executor executor) {
+ this.url = url;
+ this.executor = executor;
+ }
+
+ public MyComponent() {
+ throw new RuntimeException("Constructor annotated with @Inject is preferred.");
+ }
+ }
+
+ public static class ComponentTakingDefaultString{
+ private final String injectedString;
+
+ public ComponentTakingDefaultString(String empty_string_created_by_guice) {
+ this.injectedString = empty_string_created_by_guice;
+ }
+ }
+
+ public static class ComponentThatCannotBeConstructed {
+ public ComponentThatCannotBeConstructed(Integer cannot_be_injected_because_Integer_has_no_default_ctor) { }
+ }
+
+ @Rule
+ public final ExpectedException exception = ExpectedException.none();
+
+ @Test
+ public void guice_injector_is_used_when_no_global_component_exists() {
+ setInjector(
+ Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ bind(Executor.class).toInstance(Executors.newSingleThreadExecutor());
+ bind(String.class).annotatedWith(Names.named("url")).toInstance("http://yahoo.com");
+ }
+ }));
+
+ register(MyComponent.class);
+ complete();
+
+ MyComponent component = getInstance(MyComponent.class);
+ assertThat(component.url, is("http://yahoo.com"));
+ assertNotNull(component.executor);
+ }
+
+ @Test
+ public void guice_injector_creates_a_new_instance_with_default_ctor_when_no_explicit_binding_exists() {
+ setInjector(emptyGuiceInjector());
+ register(ComponentTakingDefaultString.class);
+ complete();
+
+ ComponentTakingDefaultString component = getInstance(ComponentTakingDefaultString.class);
+ assertThat(component.injectedString, is(""));
+ }
+
+ @Test
+ public void guice_injector_fails_when_no_explicit_binding_exists_and_class_has_no_default_ctor() {
+ setInjector(emptyGuiceInjector());
+ register(ComponentThatCannotBeConstructed.class);
+
+ exception.expect(RuntimeException.class);
+ exception.expectMessage("When resolving dependencies of 'demo.FallbackToGuiceInjectorTest$ComponentThatCannotBeConstructed'");
+ complete();
+ }
+
+ private Injector emptyGuiceInjector() {
+ return Guice.createInjector(new AbstractModule() {
+ @Override
+ protected void configure() {
+ }
+ });
+ }
+}
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
new file mode 100644
index 00000000000..3eff01eed55
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/ConfigRetrieverTest.scala
@@ -0,0 +1,88 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di
+
+import org.junit.Assert._
+import org.hamcrest.CoreMatchers.{is, instanceOf => hamcrestInstanceOf}
+import com.yahoo.vespa.config.ConfigKey
+import com.yahoo.config.test.{TestConfig, Bootstrap2Config, Bootstrap1Config}
+import com.yahoo.container.di.ConfigRetriever.{ComponentsConfigs, BootstrapConfigs}
+import org.junit.{Ignore, After, Before, Test}
+import scala.collection.JavaConversions._
+import scala.reflect.ClassTag
+import org.hamcrest.Matcher
+
+/**
+ *
+ * @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(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)),
+ subscriber.getSubscriber(_))
+ }
+
+ 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
new file mode 100644
index 00000000000..b64de80e39f
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/ContainerTest.scala
@@ -0,0 +1,350 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di
+
+import com.yahoo.container.di.componentgraph.core.ComponentGraphTest.{SimpleComponent2, SimpleComponent}
+import com.yahoo.container.di.componentgraph.Provider
+import com.yahoo.container.di.componentgraph.core.{Node, ComponentGraph}
+import org.junit.{Test, Before, After}
+import org.junit.Assert._
+import org.hamcrest.CoreMatchers._
+import com.yahoo.config.test.TestConfig
+import com.yahoo.component.AbstractComponent
+import ContainerTest._
+import scala.collection.JavaConversions
+import com.yahoo.config.di.IntConfig
+import scala.concurrent.{future, Await}
+import scala.concurrent.duration._
+import scala.concurrent.ExecutionContext.Implicits.global
+import scala.util.Try
+import com.yahoo.container.di.config.RestApiContext
+import com.yahoo.container.bundle.MockBundle
+
+/**
+ * @author tonytv
+ * @author gjoranv
+ */
+class ContainerTest {
+ var dirConfigSource: DirConfigSource = null
+
+ @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.runOnce())
+ 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.runOnce()
+ val component = createComponentTakingConfig(componentGraph)
+
+ assertThat(component.config.stringVal(), is("original"))
+
+ // Reconfigure
+ dirConfigSource.writeConfig("test", """stringVal "reconfigured" """)
+ container.reloadConfig(2)
+
+ val newComponentGraph = container.runOnce(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.runOnce()
+ val component = createComponentTakingConfig(graph)
+ assertThat(component.getId.toString, is("id1"))
+
+ writeBootstrapConfigsWithMultipleComponents(Array(
+ ("id1", classOf[ComponentTakingConfig]),
+ ("id2", classOf[ComponentTakingConfig])))
+
+ container.reloadConfig(2)
+ val newGraph = container.runOnce(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.runOnce()
+ val componentToDestruct = oldGraph.getInstance(classOf[DestructableComponent])
+
+ writeBootstrapConfigs("id2", classOf[DestructableComponent])
+ container.reloadConfig(2)
+ container.runOnce(oldGraph)
+ assertTrue(componentToDestruct.deconstructed)
+ }
+
+ @Test
+ def previous_graph_is_retained_when_new_graph_throws_exception() {
+ val simpleComponentEntry = ComponentEntry("simpleComponent", classOf[SimpleComponent])
+
+ writeBootstrapConfigs(Array(simpleComponentEntry))
+ val container = newContainer(dirConfigSource)
+ var currentGraph = container.runOnce()
+
+ val simpleComponent = currentGraph.getInstance(classOf[SimpleComponent])
+
+ writeBootstrapConfigs("thrower", classOf[ComponentThrowingException])
+ dirConfigSource.writeConfig("test", """stringVal "myString" """)
+ container.reloadConfig(2)
+ try {
+ currentGraph = container.runOnce(currentGraph)
+ fail("Expected exception")
+ } catch {
+ case e: Exception => e.printStackTrace()
+ }
+
+ val componentTakingConfigEntry = ComponentEntry("componentTakingConfig", classOf[ComponentTakingConfig])
+ writeBootstrapConfigs(Array(simpleComponentEntry, componentTakingConfigEntry))
+ container.reloadConfig(3)
+ currentGraph = container.runOnce(currentGraph)
+
+ assertSame(simpleComponent, currentGraph.getInstance(classOf[SimpleComponent]))
+ assertNotNull(currentGraph.getInstance(classOf[ComponentTakingConfig]))
+ }
+
+ @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.runOnce()
+
+ writeBootstrapConfigs("thrower", classOf[ComponentThrowingException])
+ container.reloadConfig(2)
+
+ try {
+ currentGraph = container.runOnce(currentGraph)
+ fail("expected exception")
+ } catch {
+ case e: Exception =>
+ }
+
+ val newGraph = future {
+ currentGraph = container.runOnce(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.runOnce()
+
+ 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 =
+ 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.runOnce()
+
+ val restApiContext = componentGraph.getInstance(restApiClass)
+ }
+
+ case class ComponentEntry(componentId: String, classId: Class[_]) {
+ def asConfig(position: Int) = {
+ <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.runOnce()
+ val destructableEntity = oldGraph.getInstance(classOf[DestructableEntity])
+
+ writeBootstrapConfigs("id2", classOf[DestructableProvider])
+ container.reloadConfig(2)
+ container.runOnce(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 == false)
+ instance.deconstructed = true
+ }
+ }
+
+ class ComponentTakingConfig(val config: TestConfig) extends AbstractComponent {
+ require(config != null)
+ }
+
+ class ComponentThrowingException(config:IntConfig) extends AbstractComponent {
+ throw new RuntimeException("This component can never be created")
+ }
+
+ 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]) = JavaConversions.mapAsScalaMap(map).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
new file mode 100644
index 00000000000..20e4fcb03f8
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/DirConfigSource.scala
@@ -0,0 +1,49 @@
+// Copyright 2016 Yahoo Inc. 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}
+
+// TODO: Make this a junit rule. Does not yet work. Look out for junit updates
+// (@Rule def configSourceRule = dirConfigSource)
+
+/**
+ * @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
+ }
+} \ No newline at end of file
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
new file mode 100644
index 00000000000..43733923235
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ComponentGraphTest.scala
@@ -0,0 +1,538 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.di.componentgraph.core
+
+import org.junit.Test
+import org.junit.Assert._
+import org.hamcrest.CoreMatchers.{is, sameInstance, equalTo, not, containsString}
+
+import com.yahoo.component.provider.ComponentRegistry
+import com.google.inject.name.{Names, Named}
+import com.yahoo.component.{ComponentId, AbstractComponent}
+import org.hamcrest.Matcher
+import com.yahoo.vespa.config.ConfigKey
+import com.yahoo.config.{ConfigInstance}
+import com.yahoo.config.test.{TestConfig, Test2Config}
+import java.util.concurrent.{Executors, Executor}
+import com.google.inject.{Guice, Key, AbstractModule, Inject, Provider=>GuiceProvider}
+import com.yahoo.container.di._
+import com.yahoo.config.subscription.ConfigGetter
+import com.yahoo.container.di.config.{JerseyInjectionConfig, JerseyBundlesConfig, RestApiContext}
+import com.yahoo.container.di.componentgraph.Provider
+
+/**
+ * @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
new file mode 100644
index 00000000000..fcbde13639d
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/JerseyNodeTest.scala
@@ -0,0 +1,66 @@
+// Copyright 2016 Yahoo Inc. 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
new file mode 100644
index 00000000000..e9c0be03a30
--- /dev/null
+++ b/container-di/src/test/scala/com/yahoo/container/di/componentgraph/core/ReuseComponentsTest.scala
@@ -0,0 +1,249 @@
+// Copyright 2016 Yahoo Inc. 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)
+ }
+}
diff --git a/container-di/src/test/vespa-configdef/bootstrap1.def b/container-di/src/test/vespa-configdef/bootstrap1.def
new file mode 100644
index 00000000000..3af0c945db8
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/bootstrap1.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+namespace=config.test
+
+dummy string default=""
diff --git a/container-di/src/test/vespa-configdef/bootstrap2.def b/container-di/src/test/vespa-configdef/bootstrap2.def
new file mode 100644
index 00000000000..ba0e8cccd56
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/bootstrap2.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+namespace=config.test
+
+dummy string default=""
+
diff --git a/container-di/src/test/vespa-configdef/components1.def b/container-di/src/test/vespa-configdef/components1.def
new file mode 100644
index 00000000000..3af0c945db8
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/components1.def
@@ -0,0 +1,5 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+namespace=config.test
+
+dummy string default=""
diff --git a/container-di/src/test/vespa-configdef/int.def b/container-di/src/test/vespa-configdef/int.def
new file mode 100644
index 00000000000..99fb79d64cf
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/int.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.di
+
+intVal int default=1
diff --git a/container-di/src/test/vespa-configdef/string.def b/container-di/src/test/vespa-configdef/string.def
new file mode 100644
index 00000000000..53585338ee6
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/string.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.di
+
+stringVal string default="_default_"
diff --git a/container-di/src/test/vespa-configdef/test.def b/container-di/src/test/vespa-configdef/test.def
new file mode 100644
index 00000000000..d77b2e9df16
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/test.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.test
+
+stringVal string default="default"
diff --git a/container-di/src/test/vespa-configdef/test2.def b/container-di/src/test/vespa-configdef/test2.def
new file mode 100644
index 00000000000..d77b2e9df16
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/test2.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.test
+
+stringVal string default="default"
diff --git a/container-di/src/test/vespa-configdef/thread-pool.def b/container-di/src/test/vespa-configdef/thread-pool.def
new file mode 100644
index 00000000000..31741e913b6
--- /dev/null
+++ b/container-di/src/test/vespa-configdef/thread-pool.def
@@ -0,0 +1,6 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+version=1
+
+namespace=config.test
+
+numThreads int