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