diff options
Diffstat (limited to 'container-di/src/main')
36 files changed, 2949 insertions, 0 deletions
diff --git a/container-di/src/main/java/com/yahoo/container/bundle/BundleInstantiationSpecification.java b/container-di/src/main/java/com/yahoo/container/bundle/BundleInstantiationSpecification.java new file mode 100644 index 00000000000..0fb8a99a957 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/bundle/BundleInstantiationSpecification.java @@ -0,0 +1,86 @@ +// 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 com.yahoo.component.ComponentId; +import com.yahoo.component.ComponentSpecification; + + +/** + * Specifies how a component should be instantiated from a bundle. + * + * Immutable + * + * @author Tony Vaagenes + */ +public final class BundleInstantiationSpecification { + + public final ComponentId id; + public final ComponentSpecification classId; + public final ComponentSpecification bundle; + + public BundleInstantiationSpecification(ComponentSpecification id, ComponentSpecification classId, ComponentSpecification bundle) { + this.id = id.toId(); + this.classId = (classId != null) ? classId : id.withoutNamespace(); + this.bundle = (bundle != null) ? bundle : this.classId; + } + + // Must only be used when classId != null, otherwise the id must be handled as a ComponentSpecification + // (since converting a spec string to a ComponentId and then to a ComponentSpecification causes loss of information). + public BundleInstantiationSpecification(ComponentId id, ComponentSpecification classId, ComponentSpecification bundle) { + this(id.toSpecification(), classId, bundle); + assert (classId!= null); + } + + private static final String defaultInternalBundle = "container-search-and-docproc"; + + private static BundleInstantiationSpecification getInternalSpecificationFromString(String idSpec, String classSpec) { + return new BundleInstantiationSpecification( + new ComponentSpecification(idSpec), + (classSpec == null || classSpec.isEmpty())? null : new ComponentSpecification(classSpec), + new ComponentSpecification(defaultInternalBundle)); + } + + public static BundleInstantiationSpecification getInternalSearcherSpecification(ComponentSpecification idSpec, + ComponentSpecification classSpec) { + return new BundleInstantiationSpecification(idSpec, classSpec, new ComponentSpecification(defaultInternalBundle)); + } + + // TODO: These are the same for now because they are in the same bundle. + public static BundleInstantiationSpecification getInternalHandlerSpecificationFromStrings(String idSpec, String classSpec) { + return getInternalSpecificationFromString(idSpec, classSpec); + } + + public static BundleInstantiationSpecification getInternalProcessingSpecificationFromStrings(String idSpec, String classSpec) { + return getInternalSpecificationFromString(idSpec, classSpec); + } + + public static BundleInstantiationSpecification getInternalSearcherSpecificationFromStrings(String idSpec, String classSpec) { + return getInternalSpecificationFromString(idSpec, classSpec); + } + + public static BundleInstantiationSpecification getFromStrings(String idSpec, String classSpec, String bundleSpec) { + return new BundleInstantiationSpecification( + new ComponentSpecification(idSpec), + (classSpec == null || classSpec.isEmpty())? null : new ComponentSpecification(classSpec), + (bundleSpec == null || bundleSpec.isEmpty())? null : new ComponentSpecification(bundleSpec)); + } + + /** + * Return a new instance of the specification with bundle name altered + * + * @param bundleName the new name of the bundle + * @return the new instance of the specification + */ + public BundleInstantiationSpecification inBundle(String bundleName) { + return new BundleInstantiationSpecification(this.id, this.classId, new ComponentSpecification(bundleName)); + } + + public String getClassName() { + return classId.getName(); + } + + public BundleInstantiationSpecification nestInNamespace(ComponentId namespace) { + return new BundleInstantiationSpecification(id.nestInNamespace(namespace), classId, bundle); + } + +} diff --git a/container-di/src/main/java/com/yahoo/container/bundle/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/bundle/package-info.java b/container-di/src/main/java/com/yahoo/container/bundle/package-info.java new file mode 100644 index 00000000000..c9707371626 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/bundle/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.container.bundle; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/container/di/CloudSubscriberFactory.java b/container-di/src/main/java/com/yahoo/container/di/CloudSubscriberFactory.java new file mode 100644 index 00000000000..065733a719a --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/CloudSubscriberFactory.java @@ -0,0 +1,142 @@ +// 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 final Map<CloudSubscriber, Integer> activeSubscribers = new WeakHashMap<>(); + + private Optional<Long> testGeneration = Optional.empty(); + + 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; + + 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; + } + + //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(boolean isInitializing) { + 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(isInitializing)) + gotNextGen = true; + } + catch (IllegalArgumentException e) { + numExceptions++; + log.log(Level.WARNING, "Got exception from the config system (ignore if you just removed a " + + "component from your application that used the mentioned config) Subscriber info: " + + subscriber.toString(), e); + if (numExceptions >= 5) + throw new IllegalArgumentException("Failed retrieving the next config generation", e); + } + } + + generation = subscriber.getGeneration(); + 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/ComponentDeconstructor.java b/container-di/src/main/java/com/yahoo/container/di/ComponentDeconstructor.java new file mode 100644 index 00000000000..4e3881a6fe6 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/ComponentDeconstructor.java @@ -0,0 +1,18 @@ +// 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 org.osgi.framework.Bundle; + +import java.util.Collection; +import java.util.List; + +/** + * @author gjoranv + * @author Tony Vaagenes + */ +public interface ComponentDeconstructor { + + /** Deconstructs the given components in order, then the given bundles. */ + void deconstruct(List<Object> components, Collection<Bundle> bundles); + +} 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..a7ff6c46a8b --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/ConfigRetriever.java @@ -0,0 +1,185 @@ +// 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 java.util.logging.Level.FINE; + +/** + * @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); + } + + public ConfigSnapshot getConfigs(Set<ConfigKey<? extends ConfigInstance>> componentConfigKeys, + long leastGeneration, boolean isInitializing) { + // Loop until we get config. + while (true) { + Optional<ConfigSnapshot> maybeSnapshot = getConfigsOnce(componentConfigKeys, leastGeneration, isInitializing); + if (maybeSnapshot.isPresent()) { + var configSnapshot = maybeSnapshot.get(); + resetComponentSubscriberIfBootstrap(configSnapshot); + return configSnapshot; + } + } + } + + Optional<ConfigSnapshot> getConfigsOnce(Set<ConfigKey<? extends ConfigInstance>> componentConfigKeys, + long leastGeneration, boolean isInitializing) { + if (!Sets.intersection(componentConfigKeys, bootstrapKeys).isEmpty()) { + throw new IllegalArgumentException( + "Component config keys [" + componentConfigKeys + "] overlaps with bootstrap config keys [" + bootstrapKeys + "]"); + } + log.log(FINE, "getConfigsOnce: " + componentConfigKeys); + + Set<ConfigKey<? extends ConfigInstance>> allKeys = new HashSet<>(componentConfigKeys); + allKeys.addAll(bootstrapKeys); + setupComponentSubscriber(allKeys); + + return getConfigsOptional(leastGeneration, isInitializing); + } + + private Optional<ConfigSnapshot> getConfigsOptional(long leastGeneration, boolean isInitializing) { + long newestComponentGeneration = componentSubscriber.waitNextGeneration(isInitializing); + log.log(FINE, "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 (bootstrapSubscriber.generation() < newestComponentGeneration) { + long newestBootstrapGeneration = bootstrapSubscriber.waitNextGeneration(isInitializing); + log.log(FINE, "getConfigsOptional: new bootstrap generation: " + bootstrapSubscriber.generation()); + Optional<ConfigSnapshot> bootstrapConfig = bootstrapConfigIfChanged(); + if (bootstrapConfig.isPresent()) { + return bootstrapConfig; + } else { + if (newestBootstrapGeneration == newestComponentGeneration) { + log.log(FINE, "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(FINE, "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..82c7f65bc2a --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/Container.java @@ -0,0 +1,289 @@ +// 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.Injector; +import com.yahoo.config.ConfigInstance; +import com.yahoo.config.ConfigurationRuntimeException; +import com.yahoo.config.subscription.ConfigInterruptedException; +import com.yahoo.container.ComponentsConfig; +import com.yahoo.container.bundle.BundleInstantiationSpecification; +import com.yahoo.container.di.ConfigRetriever.BootstrapConfigs; +import com.yahoo.container.di.ConfigRetriever.ComponentsConfigs; +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.ApplicationBundlesConfig; +import com.yahoo.container.di.config.PlatformBundlesConfig; +import com.yahoo.container.di.config.RestApiContext; +import com.yahoo.container.di.config.SubscriberFactory; +import com.yahoo.vespa.config.ConfigKey; +import org.osgi.framework.Bundle; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.logging.Level; +import java.util.logging.Logger; + +import static java.util.logging.Level.FINE; + +/** + * @author gjoranv + * @author Tony Vaagenes + * @author ollivir + */ +public class Container { + + private static final Logger log = Logger.getLogger(Container.class.getName()); + + private final SubscriberFactory subscriberFactory; + private final ConfigKey<ApplicationBundlesConfig> applicationBundlesConfigKey; + private final ConfigKey<PlatformBundlesConfig> platformBundlesConfigKey; + private final ConfigKey<ComponentsConfig> componentsConfigKey; + private final ComponentDeconstructor componentDeconstructor; + private final Osgi osgi; + + private final ConfigRetriever configurer; + private List<String> platformBundles; // Used to verify that platform bundles don't change. + private long previousConfigGeneration = -1L; + private long leastGeneration = -1L; + + public Container(SubscriberFactory subscriberFactory, String configId, ComponentDeconstructor componentDeconstructor, Osgi osgi) { + this.subscriberFactory = subscriberFactory; + this.componentDeconstructor = componentDeconstructor; + this.osgi = osgi; + + applicationBundlesConfigKey = new ConfigKey<>(ApplicationBundlesConfig.class, configId); + platformBundlesConfigKey = new ConfigKey<>(PlatformBundlesConfig.class, configId); + componentsConfigKey = new ConfigKey<>(ComponentsConfig.class, configId); + var bootstrapKeys = Set.of(applicationBundlesConfigKey, platformBundlesConfigKey, componentsConfigKey); + this.configurer = new ConfigRetriever(bootstrapKeys, subscriberFactory::getSubscriber); + } + + public Container(SubscriberFactory subscriberFactory, String configId, ComponentDeconstructor componentDeconstructor) { + this(subscriberFactory, configId, componentDeconstructor, new Osgi() { + }); + } + + public ComponentGraph getNewComponentGraph(ComponentGraph oldGraph, Injector fallbackInjector, boolean isInitializing) { + try { + Collection<Bundle> obsoleteBundles = new HashSet<>(); + ComponentGraph newGraph = getConfigAndCreateGraph(oldGraph, fallbackInjector, isInitializing, obsoleteBundles); + newGraph.reuseNodes(oldGraph); + constructComponents(newGraph); + deconstructObsoleteComponents(oldGraph, newGraph, obsoleteBundles); + return newGraph; + } catch (Throwable t) { + invalidateGeneration(oldGraph.generation(), t); + throw t; + } + } + + private ComponentGraph getConfigAndCreateGraph(ComponentGraph graph, + Injector fallbackInjector, + boolean isInitializing, + Collection<Bundle> obsoleteBundles) // NOTE: Return value + { + ConfigSnapshot snapshot; + while (true) { + snapshot = configurer.getConfigs(graph.configKeys(), leastGeneration, isInitializing); + + log.log(FINE, String.format("createNewGraph:\n" + "graph.configKeys = %s\n" + "graph.generation = %s\n" + "snapshot = %s\n", + graph.configKeys(), graph.generation(), snapshot)); + + if (snapshot instanceof BootstrapConfigs) { + 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(FINE, "Got new bootstrap generation\n" + configGenerationsString()); + + if (graph.generation() == 0) { + platformBundles = getConfig(platformBundlesConfigKey, snapshot.configs()).bundlePaths(); + osgi.installPlatformBundles(platformBundles); + } else { + throwIfPlatformBundlesChanged(snapshot); + } + Collection<Bundle> bundlesToRemove = installApplicationBundles(snapshot.configs()); + obsoleteBundles.addAll(bundlesToRemove); + + graph = createComponentsGraph(snapshot.configs(), getBootstrapGeneration(), fallbackInjector); + + // Continues loop + + } else if (snapshot instanceof ComponentsConfigs) { + break; + } + } + log.log(FINE, "Got components configs,\n" + configGenerationsString()); + return createAndConfigureComponentsGraph(snapshot.configs(), fallbackInjector); + } + + private long getBootstrapGeneration() { + return configurer.getBootstrapGeneration(); + } + + private long getComponentsGeneration() { + return configurer.getComponentsGeneration(); + } + + private String configGenerationsString() { + return String.format("bootstrap generation = %d\n" + "components generation: %d\n" + "previous generation: %d", + getBootstrapGeneration(), getComponentsGeneration(), previousConfigGeneration); + } + + private void throwIfPlatformBundlesChanged(ConfigSnapshot snapshot) { + var checkPlatformBundles = getConfig(platformBundlesConfigKey, snapshot.configs()).bundlePaths(); + if (! checkPlatformBundles.equals(platformBundles)) + throw new RuntimeException("Platform bundles are not allowed to change!\nOld: " + platformBundles + "\nNew: " + checkPlatformBundles); + } + + private ComponentGraph createAndConfigureComponentsGraph(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> componentsConfigs, + Injector fallbackInjector) { + ComponentGraph componentGraph = createComponentsGraph(componentsConfigs, getComponentsGeneration(), fallbackInjector); + componentGraph.setAvailableConfigs(componentsConfigs); + return componentGraph; + } + + private void constructComponents(ComponentGraph graph) { + graph.nodes().forEach(Node::constructInstance); + } + + private void deconstructObsoleteComponents(ComponentGraph oldGraph, + ComponentGraph newGraph, + Collection<Bundle> obsoleteBundles) { + Map<Object, ?> newComponents = new IdentityHashMap<>(newGraph.size()); + for (Object component : newGraph.allConstructedComponentsAndProviders()) + newComponents.put(component, null); + + List<Object> obsoleteComponents = new ArrayList<>(); + for (Object component : oldGraph.allConstructedComponentsAndProviders()) + if ( ! newComponents.containsKey(component)) + obsoleteComponents.add(component); + + componentDeconstructor.deconstruct(obsoleteComponents, obsoleteBundles); + } + + private Set<Bundle> installApplicationBundles(Map<ConfigKey<? extends ConfigInstance>, ConfigInstance> configsIncludingBootstrapConfigs) { + ApplicationBundlesConfig applicationBundlesConfig = getConfig(applicationBundlesConfigKey, configsIncludingBootstrapConfigs); + return osgi.useApplicationBundles(applicationBundlesConfig.bundles()); + } + + 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 = bundleInstantiationSpecification(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 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())); + } + } + } + + private void invalidateGeneration(long generation, Throwable cause) { + leastGeneration = Math.max(configurer.getComponentsGeneration(), configurer.getBootstrapGeneration()) + 1; + if (!(cause instanceof InterruptedException) && !(cause instanceof ConfigInterruptedException)) { + log.log(Level.WARNING, newGraphErrorMessage(generation, cause), cause); + } + } + + private static String newGraphErrorMessage(long generation, Throwable cause) { + 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 retainMessage = ". Retaining previous component generation."; + + if (generation == 0) { + if (cause instanceof ComponentNode.ComponentConstructorException) { + return failedFirstMessage + constructMessage; + } else { + return failedFirstMessage; + } + } else { + if (cause instanceof ComponentNode.ComponentConstructorException) { + return failedNewMessage + constructMessage + retainMessage; + } else { + return failedNewMessage + retainMessage; + } + } + } + + public void shutdown(ComponentGraph graph, ComponentDeconstructor deconstructor) { + shutdownConfigurer(); + if (graph != null) { + deconstructAllComponents(graph, deconstructor); + } + } + + 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) { + // This is only used for shutdown, so no need to uninstall any bundles. + deconstructor.deconstruct(graph.allConstructedComponentsAndProviders(), Collections.emptyList()); + } + + 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); + } + + private static BundleInstantiationSpecification bundleInstantiationSpecification(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..940986e2f38 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/Osgi.java @@ -0,0 +1,54 @@ +// 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; + +import static java.util.Collections.emptySet; + +/** + * @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 installPlatformBundles(Collection<String> bundlePaths) { + System.out.println("installPlatformBundles " + bundlePaths); + } + + /** + * Returns the set of bundles that is not used by the current application generation, + * and therefore should be scheduled for uninstalling. + */ + default Set<Bundle> useApplicationBundles(Collection<FileReference> bundles) { + System.out.println("useBundles " + bundles.stream().map(Object::toString).collect(Collectors.joining(", "))); + return emptySet(); + } + + 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/Provider.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/Provider.java new file mode 100644 index 00000000000..3fd3195e5dd --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/Provider.java @@ -0,0 +1,26 @@ +// 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; + +import com.yahoo.component.Deconstructable; + +/** + * <p>Provides a component of the parameter type T. + * If (and only if) dependency injection does not have a component of type T, + * it will request one from the Provider providing type T.</p> + * + * <p>Providers are useful in these situations:</p> + * <ul> + * <li>Some code is needed to create the component instance in question.</li> + * <li>The component creates resources that must be deconstructed.</li> + * <li>A fallback component should be provided in case the application (or system) + * does not provide a component instance.</li> + * </ul> + * + * @author Tony Vaagenes + * @author gjoranv + */ +public interface Provider<T> extends Deconstructable { + + T get(); + +} 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..fef2809f236 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentGraph.java @@ -0,0 +1,431 @@ +// 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.container.di.componentgraph.cycle.CycleFinder; +import com.yahoo.container.di.componentgraph.cycle.Graph; + +import java.util.Collections; +import java.util.logging.Level; +import com.yahoo.vespa.config.ConfigKey; + +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 + * + * Not thread safe. + */ +public class ComponentGraph { + + private static final Logger log = Logger.getLogger(ComponentGraph.class.getName()); + + private final long generation; + private final 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::component) + .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) { + componentNodes().forEach(node -> node.setAvailableConfigs(Keys.invariantCopy(configs))); + } + + 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.isEmpty()) { + node.instance = Optional.empty(); + } + } + } + } + + /** All constructed components and providers of this, in reverse creation order, i.e., suited for ordered deconstruction. */ + public List<Object> allConstructedComponentsAndProviders() { + List<Node> orderedNodes = topologicalSort(nodes()); + Collections.reverse(orderedNodes); + return orderedNodes.stream().map(node -> node.constructedInstance().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.component() == 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.isEmpty()) { + Object instance; + try { + log.log(Level.FINE, "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. + * + * For each iteration, the algorithm finds the components that are not "wanted by" any other component, + * and prepends those components into the resulting 'sorted' list. Hence, the first element in the returned + * list is the component that is directly or indirectly wanted by "most" other components. + * + * @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: " + findCycle(notReady)); + } + + ready.forEach(node -> node.usedComponents() + .forEach(injectedNode -> numIncoming.merge(injectedNode.componentId(), -1, (a, b) -> a + b))); + sorted.addAll(0, ready); + unsorted = notReady; + } + return sorted; + } + + private static List<String> findCycle(List<Node> nodes) { + var cyclicGraph = new Graph<String>(); + for (var node : nodes) { + for (var adjacent : node.usedComponents()) { + cyclicGraph.edge(node.componentId().stringValue(), + adjacent.componentId().stringValue()); + } + } + return new CycleFinder<>(cyclicGraph).findCycle(); + } + +} 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..b6fa4241e26 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentNode.java @@ -0,0 +1,313 @@ +// Copyright Verizon Media. 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.time.Duration; +import java.time.Instant; +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; +import static java.util.logging.Level.FINE; +import static java.util.logging.Level.INFO; + +/** + * @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).component()); + } else if (ob instanceof ConfigKey) { + actualArguments.add(availableConfigs.get(ob)); + } else { + actualArguments.add(ob); + } + } + + Object instance; + try { + log.log(FINE, () -> "Constructing " + idAndType()); + Instant start = Instant.now(); + instance = constructor.newInstance(actualArguments.toArray()); + Duration duration = Duration.between(start, Instant.now()); + log.log(duration.compareTo(Duration.ofMinutes(1)) > 0 ? INFO : FINE, + () -> "Finished constructing " + idAndType() + " in " + duration); + } catch (InvocationTargetException | InstantiationException | IllegalAccessException e) { + StackTraceElement dependencyInjectorMarker = new StackTraceElement("============= Dependency Injection =============", "newInstance", null, -1); + throw removeStackTrace(new ComponentConstructorException("Error constructing " + idAndType() + ": " + e.getMessage(), 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..429052c0039 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.java @@ -0,0 +1,107 @@ +// 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.component())); + + 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..b0d9d1f3921 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Exceptions.java @@ -0,0 +1,47 @@ +// 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 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..0f8aa678934 --- /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.component())); + + 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..be80fc1616d --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Keys.java @@ -0,0 +1,39 @@ +// 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..3afc8bb817c --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/Node.java @@ -0,0 +1,162 @@ +// 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 java.util.logging.Level.FINE; + +/** + * @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(); + + /** Constructs the instance represented by this node, if not already done. */ + public void constructInstance() { + if ( ! instance.isPresent()) + instance = Optional.of(newInstance()); + } + + /** + * Returns the component represented by this - which is either the instance, or if the instance is a provider, + * the component returned by it. + */ + public Object component() { + constructInstance(); + if (instance.get() instanceof Provider) { + Provider<?> provider = (Provider<?>) instance.get(); + return provider.get(); + } else { + return instance.get(); + } + } + + 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; + } + + /** Returns the already constructed instance in this, if any */ + public Optional<?> constructedInstance() { + return instance; + } + + /** + * @param identityObject he 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/componentgraph/core/package-info.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/package-info.java new file mode 100644 index 00000000000..e9b5b14d5d8 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.container.di.componentgraph.core; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/CycleFinder.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/CycleFinder.java new file mode 100644 index 00000000000..327949bb8d0 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/CycleFinder.java @@ -0,0 +1,95 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.componentgraph.cycle; + +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +import static com.yahoo.container.di.componentgraph.cycle.CycleFinder.State.BLACK; +import static com.yahoo.container.di.componentgraph.cycle.CycleFinder.State.GRAY; +import static com.yahoo.container.di.componentgraph.cycle.CycleFinder.State.WHITE; +import static java.util.logging.Level.FINE; +import static java.util.Collections.singletonList; + + +/** + * <p>Applies the + * <a href="https://www.geeksforgeeks.org/detect-cycle-direct-graph-using-colors/"> three-color algorithm</a> + * to detect a cycle in a directed graph. If there are multiple cycles, this implementation only detects one + * of them and does not guarantee that the shortest cycle is found. + * </p> + * + * @author gjoranv + */ +public class CycleFinder<T> { + private static final Logger log = Logger.getLogger(CycleFinder.class.getName()); + + enum State { + WHITE, GRAY, BLACK; + } + + private final Graph<T> graph; + + private Map<T, State> colors; + + private List<T> cycle; + + public CycleFinder(Graph<T> graph) { + this.graph = graph; + } + + private void resetState() { + cycle = null; + colors = new LinkedHashMap<>(); + graph.getVertices().forEach(v -> colors.put(v, WHITE)); + } + + /** + * Returns a list of vertices constituting a cycle in the graph, or an empty + * list if no cycle was found. Only the first encountered cycle is returned. + */ + public List<T> findCycle() { + resetState(); + for (T vertex : graph.getVertices()) { + if (colors.get(vertex) == WHITE) { + if (visitDepthFirst(vertex, new ArrayList<>(singletonList(vertex)))) { + if (cycle == null) throw new IllegalStateException("Null cycle - this should never happen"); + if (cycle.isEmpty()) throw new IllegalStateException("Empty cycle - this should never happen"); + log.log(FINE, "Cycle detected: " + cycle); + return cycle; + } + } + } + return new ArrayList<>(); + } + + private boolean visitDepthFirst(T vertex, List<T> path) { + colors.put(vertex, GRAY); + log.log(FINE, "Vertex start " + vertex + " - colors: " + colors + " - path: " + path); + for (T adjacent : graph.getAdjacent(vertex)) { + path.add(adjacent); + if (colors.get(adjacent) == GRAY) { + cycle = removePathIntoCycle(path); + return true; + } + if (colors.get(adjacent) == WHITE && visitDepthFirst(adjacent, path)) { + return true; + } + path.remove(adjacent); + } + colors.put(vertex, BLACK); + log.log(FINE, "Vertex end " + vertex + " - colors: " + colors + " - path: " + path); + return false; + } + + private List<T> removePathIntoCycle(List<T> pathWithCycle) { + T cycleStart = pathWithCycle.get(pathWithCycle.size() - 1); + return pathWithCycle.stream() + .dropWhile(vertex -> ! vertex.equals(cycleStart)) + .collect(Collectors.toList()); + } + +} diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/Graph.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/Graph.java new file mode 100644 index 00000000000..946330668bd --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/cycle/Graph.java @@ -0,0 +1,41 @@ +// Copyright 2020 Oath Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. + +package com.yahoo.container.di.componentgraph.cycle; + +import java.util.LinkedHashMap; +import java.util.LinkedHashSet; +import java.util.Map; +import java.util.Set; + +/** + * Class representing a directed graph. + * + * @author gjoranv + */ +public class Graph<T> { + + private final Map<T, LinkedHashSet<T>> adjMap = new LinkedHashMap<>(); + + public void edge(T from, T to) { + if (from == null || to == null) + throw new IllegalArgumentException("Null vertices are not allowed, edge: " + from + "->" + to); + + adjMap.computeIfAbsent(from, k -> new LinkedHashSet<>()).add(to); + adjMap.computeIfAbsent(to, k -> new LinkedHashSet<>()); + } + + Set<T> getVertices() { + return adjMap.keySet(); + } + + /** + * Returns the outgoing edges of the given vertex. + */ + Set<T> getAdjacent(T vertex) { + return adjMap.get(vertex); + } + + private void throwIfMissingVertex(T vertex) { + if (! adjMap.containsKey(vertex)) throw new IllegalArgumentException("No such vertex in the graph: " + vertex); + } +} diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java new file mode 100644 index 00000000000..0c11cfb5ba4 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.container.di.componentgraph; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java b/container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java new file mode 100644 index 00000000000..c88f851909c --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java @@ -0,0 +1,13 @@ +// 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.config; + +/** + * @author gjoranv + */ +public class ResolveDependencyException extends RuntimeException { + + public ResolveDependencyException(String message) { + super(message); + } + +} diff --git a/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java b/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java new file mode 100644 index 00000000000..bfb9a8f9160 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java @@ -0,0 +1,98 @@ +// 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; +import com.google.inject.Inject; +import com.google.inject.Key; +import com.yahoo.component.ComponentId; +import org.osgi.framework.Version; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +/** + * Only for internal JDisc use. + * + * @author gjoranv + */ +public class RestApiContext { + + private final List<BundleInfo> bundles = new ArrayList<>(); + private final List<Injectable> injectableComponents = new ArrayList<>(); + + public final JerseyBundlesConfig bundlesConfig; + public final JerseyInjectionConfig injectionConfig; + + @Inject + public RestApiContext(JerseyBundlesConfig bundlesConfig, JerseyInjectionConfig injectionConfig) { + this.bundlesConfig = bundlesConfig; + this.injectionConfig = injectionConfig; + } + + public List<BundleInfo> getBundles() { + return Collections.unmodifiableList(bundles); + } + + public void addBundle(BundleInfo bundle) { + bundles.add(bundle); + } + + public List<Injectable> getInjectableComponents() { + return Collections.unmodifiableList(injectableComponents); + } + + public void addInjectableComponent(Key<?> key, ComponentId id, Object component) { + injectableComponents.add(new Injectable(key, id, component)); + } + + public static class Injectable { + public final Key<?> key; + public final ComponentId id; + public final Object instance; + + public Injectable(Key<?> key, ComponentId id, Object instance) { + this.key = key; + this.id = id; + this.instance = instance; + } + @Override + public String toString() { + return id.toString(); + } + } + + public static class BundleInfo { + public final String symbolicName; + public final Version version; + public final String fileLocation; + public final URL webInfUrl; + public final ClassLoader classLoader; + + private Set<String> classEntries; + + public BundleInfo(String symbolicName, Version version, String fileLocation, URL webInfUrl, ClassLoader classLoader) { + this.symbolicName = symbolicName; + this.version = version; + this.fileLocation = fileLocation; + this.webInfUrl = webInfUrl; + this.classLoader = classLoader; + } + + @Override + public String toString() { + return symbolicName + ":" + version; + } + + public void setClassEntries(Collection<String> entries) { + this.classEntries = ImmutableSet.copyOf(entries); + } + + public Set<String> getClassEntries() { + return classEntries; + } + } +} diff --git a/container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java b/container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java new file mode 100644 index 00000000000..60207447bfd --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java @@ -0,0 +1,23 @@ +// 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.config; + +import com.yahoo.config.ConfigInstance; +import com.yahoo.vespa.config.ConfigKey; + +import java.util.Map; + +/** + * @author Tony Vaagenes + * @author gjoranv + */ +public interface Subscriber { + + long waitNextGeneration(boolean isInitializing); + long generation(); + + boolean configChanged(); + Map<ConfigKey<ConfigInstance>, ConfigInstance> config(); + + void close(); + +} diff --git a/container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java b/container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java new file mode 100644 index 00000000000..c1c36a1b3de --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java @@ -0,0 +1,20 @@ +// 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.config; + +import com.google.inject.ProvidedBy; +import com.yahoo.container.di.CloudSubscriberFactory; +import com.yahoo.vespa.config.ConfigKey; + +import java.util.Set; + +/** + * @author Tony Vaagenes + * @author gjoranv + */ +@ProvidedBy(CloudSubscriberFactory.Provider.class) +public interface SubscriberFactory { + + Subscriber getSubscriber(Set<? extends ConfigKey<?>> configKeys); + void reloadActiveSubscribers(long generation); + +} diff --git a/container-di/src/main/java/com/yahoo/container/di/config/package-info.java b/container-di/src/main/java/com/yahoo/container/di/config/package-info.java new file mode 100644 index 00000000000..b8f65b1c3c8 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.container.di.config; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/container/di/osgi/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/java/com/yahoo/container/di/osgi/package-info.java b/container-di/src/main/java/com/yahoo/container/di/osgi/package-info.java new file mode 100644 index 00000000000..9685cf571bd --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/osgi/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author Tony Vaagenes + */ +@ExportPackage +package com.yahoo.container.di.osgi; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java b/container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java new file mode 100644 index 00000000000..8c501963db3 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java @@ -0,0 +1,50 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.osgi.provider.model; + +import com.yahoo.component.ComponentId; +import com.yahoo.component.ComponentSpecification; +import com.yahoo.container.bundle.BundleInstantiationSpecification; + +/** + * Describes how a component should be created. + * + * Immutable + * + * @author gjoranv + */ +public class ComponentModel { + + public final BundleInstantiationSpecification bundleInstantiationSpec; + public final String configId; // only used in the container, null when used in the model + + public ComponentModel(BundleInstantiationSpecification bundleInstantiationSpec, String configId) { + if (bundleInstantiationSpec == null) + throw new IllegalArgumentException("Null bundle instantiation spec!"); + + this.bundleInstantiationSpec = bundleInstantiationSpec; + this.configId = configId; + } + + public ComponentModel(String idSpec, String classSpec, String bundleSpec, String configId) { + this(BundleInstantiationSpecification.getFromStrings(idSpec, classSpec, bundleSpec), configId); + } + + // For vespamodel + public ComponentModel(BundleInstantiationSpecification bundleInstantiationSpec) { + this(bundleInstantiationSpec, null); + } + + // For vespamodel + public ComponentModel(String idSpec, String classSpec, String bundleSpec) { + this(BundleInstantiationSpecification.getFromStrings(idSpec, classSpec, bundleSpec)); + } + + public ComponentId getComponentId() { + return bundleInstantiationSpec.id; + } + + public ComponentSpecification getClassId() { + return bundleInstantiationSpec.classId; + } + +} diff --git a/container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java b/container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java new file mode 100644 index 00000000000..f930f56ae4a --- /dev/null +++ b/container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.osgi.provider.model; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/resources/configdefinitions/application-bundles.def b/container-di/src/main/resources/configdefinitions/application-bundles.def new file mode 100644 index 00000000000..7e03b1e3ac8 --- /dev/null +++ b/container-di/src/main/resources/configdefinitions/application-bundles.def @@ -0,0 +1,5 @@ +# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package=com.yahoo.container.di.config + +# References to user bundles to install. +bundles[] file diff --git a/container-di/src/main/resources/configdefinitions/container.components.def b/container-di/src/main/resources/configdefinitions/container.components.def new file mode 100644 index 00000000000..f27abc2fa5a --- /dev/null +++ b/container-di/src/main/resources/configdefinitions/container.components.def @@ -0,0 +1,23 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=container + +## A list of components. Components depending on other components may use this to +## get its list of components injected. + +## A component +components[].id string +## The component id used by this component to subscribe to its configs (if any) +components[].configId reference default=":parent:" + +## The id of the class to instantiate for this component. +components[].classId string default="" + +## The symbolic name of the Osgi bundle this component is located in. +## Assumed to be the same as the classid if not set. +components[].bundle string default="" + +## The component id of the component to inject to this component +components[].inject[].id string + +## The name to use for the injected component when injected to this component +components[].inject[].name string default="" diff --git a/container-di/src/main/resources/configdefinitions/container.di.config.jersey-bundles.def b/container-di/src/main/resources/configdefinitions/container.di.config.jersey-bundles.def new file mode 100644 index 00000000000..a226420274d --- /dev/null +++ b/container-di/src/main/resources/configdefinitions/container.di.config.jersey-bundles.def @@ -0,0 +1,8 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=container.di.config + +# The SymbolicName[:Version] of the Jersey bundles +bundles[].spec string + +# The packages to scan for Jersey resources +bundles[].packages[] string diff --git a/container-di/src/main/resources/configdefinitions/container.di.config.jersey-injection.def b/container-di/src/main/resources/configdefinitions/container.di.config.jersey-injection.def new file mode 100644 index 00000000000..9f5be59abbd --- /dev/null +++ b/container-di/src/main/resources/configdefinitions/container.di.config.jersey-injection.def @@ -0,0 +1,5 @@ +# Copyright 2017 Yahoo Holdings. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=container.di.config + +inject[].instance string +inject[].forClass string diff --git a/container-di/src/main/resources/configdefinitions/platform-bundles.def b/container-di/src/main/resources/configdefinitions/platform-bundles.def new file mode 100644 index 00000000000..a30a846b565 --- /dev/null +++ b/container-di/src/main/resources/configdefinitions/platform-bundles.def @@ -0,0 +1,5 @@ +# Copyright Verizon Media. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package=com.yahoo.container.di.config + +# Paths to platform bundles to install. +bundlePaths[] string |