diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /container-di |
Publish
Diffstat (limited to 'container-di')
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 |