diff options
author | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
---|---|---|
committer | Jon Bratseth <bratseth@yahoo-inc.com> | 2016-06-15 23:09:44 +0200 |
commit | 72231250ed81e10d66bfe70701e64fa5fe50f712 (patch) | |
tree | 2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /container-di/src/main |
Publish
Diffstat (limited to 'container-di/src/main')
32 files changed, 2011 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..f2d4072d34c --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/bundle/BundleInstantiationSpecification.java @@ -0,0 +1,86 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.bundle; + +import com.yahoo.component.ComponentId; +import com.yahoo.component.ComponentSpecification; +import net.jcip.annotations.Immutable; + + +/** + * Specifies how a component should be instantiated from a bundle. + * + * @author tonytv + */ +@Immutable +public final class BundleInstantiationSpecification { + + public final ComponentId id; + public final ComponentSpecification classId; + public final ComponentSpecification bundle; + + public BundleInstantiationSpecification(ComponentSpecification id, ComponentSpecification classId, ComponentSpecification bundle) { + this.id = id.toId(); + this.classId = (classId != null) ? classId : id.withoutNamespace(); + this.bundle = (bundle != null) ? bundle : this.classId; + } + + // Must only be used when classId != null, otherwise the id must be handled as a ComponentSpecification + // (since converting a spec string to a ComponentId and then to a ComponentSpecification causes loss of information). + public BundleInstantiationSpecification(ComponentId id, ComponentSpecification classId, ComponentSpecification bundle) { + this(id.toSpecification(), classId, bundle); + assert (classId!= null); + } + + private static final String defaultInternalBundle = "container-search-and-docproc"; + + private static BundleInstantiationSpecification getInternalSpecificationFromString(String idSpec, String classSpec) { + return new BundleInstantiationSpecification( + new ComponentSpecification(idSpec), + (classSpec == null || classSpec.isEmpty())? null : new ComponentSpecification(classSpec), + new ComponentSpecification(defaultInternalBundle)); + } + + public static BundleInstantiationSpecification getInternalSearcherSpecification(ComponentSpecification idSpec, + ComponentSpecification classSpec) { + return new BundleInstantiationSpecification(idSpec, classSpec, new ComponentSpecification(defaultInternalBundle)); + } + + // TODO: These are the same for now because they are in the same bundle. + public static BundleInstantiationSpecification getInternalHandlerSpecificationFromStrings(String idSpec, String classSpec) { + return getInternalSpecificationFromString(idSpec, classSpec); + } + + public static BundleInstantiationSpecification getInternalProcessingSpecificationFromStrings(String idSpec, String classSpec) { + return getInternalSpecificationFromString(idSpec, classSpec); + } + + public static BundleInstantiationSpecification getInternalSearcherSpecificationFromStrings(String idSpec, String classSpec) { + return getInternalSpecificationFromString(idSpec, classSpec); + } + + public static BundleInstantiationSpecification getFromStrings(String idSpec, String classSpec, String bundleSpec) { + return new BundleInstantiationSpecification( + new ComponentSpecification(idSpec), + (classSpec == null || classSpec.isEmpty())? null : new ComponentSpecification(classSpec), + (bundleSpec == null || bundleSpec.isEmpty())? null : new ComponentSpecification(bundleSpec)); + + } + + /** + * Return a new instance of the specification with bundle name altered + * @param bundleName New name of bundle + * @return the new instance of the specification. + */ + public BundleInstantiationSpecification inBundle(String bundleName) { + return new BundleInstantiationSpecification(this.id, this.classId, new ComponentSpecification(bundleName)); + } + + public String getClassName() { + return classId.getName(); + } + + public BundleInstantiationSpecification nestInNamespace(ComponentId namespace) { + return new BundleInstantiationSpecification(id.nestInNamespace(namespace), classId, bundle); + } + +} diff --git a/container-di/src/main/java/com/yahoo/container/bundle/package-info.java b/container-di/src/main/java/com/yahoo/container/bundle/package-info.java new file mode 100644 index 00000000000..4e409e5da06 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/bundle/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.container.bundle; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/container/di/ComponentDeconstructor.java b/container-di/src/main/java/com/yahoo/container/di/ComponentDeconstructor.java new file mode 100644 index 00000000000..8681d0bc595 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/ComponentDeconstructor.java @@ -0,0 +1,10 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di; + +/** + * @author gjoranv + * @author tonytv + */ +public interface ComponentDeconstructor { + void deconstruct(Object component); +} diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/Provider.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/Provider.java new file mode 100644 index 00000000000..38482371455 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/Provider.java @@ -0,0 +1,25 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.componentgraph; + +/** + * <p>Provides a component of the parameter type T. + * If (and only if) dependency injection does not have a component of type T, + * it will request one from the Provider providing type T.</p> + * + * <p>Providers are useful in these situations:</p> + * <ul> + * <li>Some code is needed to create the component instance in question.</li> + * <li>The component creates resources that must be deconstructed.</li> + * <li>A fallback component should be provided in case the application (or system) + * does not provide a component instance.</li> + * </ul> + * + * @author tonytv + * @author gjoranv + */ +public interface Provider<T> { + + T get(); + void deconstruct(); + +} diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/package-info.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/package-info.java new file mode 100644 index 00000000000..545b7dabc05 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/core/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.container.di.componentgraph.core; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java b/container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java new file mode 100644 index 00000000000..e0333aa8b44 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/componentgraph/package-info.java @@ -0,0 +1,7 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +@PublicApi +package com.yahoo.container.di.componentgraph; + +import com.yahoo.api.annotations.PublicApi; +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java b/container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java new file mode 100644 index 00000000000..d4fb1df397a --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/ResolveDependencyException.java @@ -0,0 +1,12 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.config; + +/** + * @author gjoranv + * @since 5.17 + */ +public class ResolveDependencyException extends RuntimeException { + public ResolveDependencyException(String message) { + super(message); + } +} diff --git a/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java b/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java new file mode 100644 index 00000000000..dcedd442b19 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/RestApiContext.java @@ -0,0 +1,101 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.config; + +import com.google.common.collect.ImmutableSet; +import com.google.inject.Inject; +import com.google.inject.Key; +import com.yahoo.component.ComponentId; +import org.osgi.framework.Version; + +import java.net.URL; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Iterator; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +/** + * Only for internal JDisc use. + * + * @author gjoranv + * @since 5.11 + */ +public class RestApiContext { + + private final List<BundleInfo> bundles = new ArrayList<>(); + private final List<Injectable> injectableComponents = new ArrayList<>(); + + public final JerseyBundlesConfig bundlesConfig; + public final JerseyInjectionConfig injectionConfig; + + @Inject + public RestApiContext(JerseyBundlesConfig bundlesConfig, JerseyInjectionConfig injectionConfig) { + this.bundlesConfig = bundlesConfig; + this.injectionConfig = injectionConfig; + } + + public List<BundleInfo> getBundles() { + return Collections.unmodifiableList(bundles); + } + + public void addBundle(BundleInfo bundle) { + bundles.add(bundle); + } + + public List<Injectable> getInjectableComponents() { + return Collections.unmodifiableList(injectableComponents); + } + + public void addInjectableComponent(Key<?> key, ComponentId id, Object component) { + injectableComponents.add(new Injectable(key, id, component)); + } + + public static class Injectable { + public final Key<?> key; + public final ComponentId id; + public final Object instance; + + public Injectable(Key<?> key, ComponentId id, Object instance) { + this.key = key; + this.id = id; + this.instance = instance; + } + @Override + public String toString() { + return id.toString(); + } + } + + public static class BundleInfo { + public final String symbolicName; + public final Version version; + public final String fileLocation; + public final URL webInfUrl; + public final ClassLoader classLoader; + + private Set<String> classEntries; + + public BundleInfo(String symbolicName, Version version, String fileLocation, URL webInfUrl, ClassLoader classLoader) { + this.symbolicName = symbolicName; + this.version = version; + this.fileLocation = fileLocation; + this.webInfUrl = webInfUrl; + this.classLoader = classLoader; + } + + @Override + public String toString() { + return symbolicName + ":" + version; + } + + public void setClassEntries(Collection<String> entries) { + this.classEntries = ImmutableSet.copyOf(entries); + } + + public Set<String> getClassEntries() { + return classEntries; + } + } +} diff --git a/container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java b/container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java new file mode 100644 index 00000000000..fe81e510148 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/Subscriber.java @@ -0,0 +1,21 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.config; + +import com.yahoo.config.ConfigInstance; +import com.yahoo.vespa.config.ConfigKey; + +import java.util.Map; + +/** + * @author tonytv + * @author gjoranv + */ +public interface Subscriber { + long waitNextGeneration(); + long generation(); + + boolean configChanged(); + Map<ConfigKey<ConfigInstance>, ConfigInstance> config(); + + void close(); +} diff --git a/container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java b/container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java new file mode 100644 index 00000000000..194435d6945 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/SubscriberFactory.java @@ -0,0 +1,18 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.config; + +import com.google.inject.ProvidedBy; +import com.yahoo.container.di.CloudSubscriberFactory; +import com.yahoo.vespa.config.ConfigKey; + +import java.util.Set; + +/** + * @author tonytv + * @author gjoranv + */ +@ProvidedBy(CloudSubscriberFactory.Provider.class) +public interface SubscriberFactory { + Subscriber getSubscriber(Set<? extends ConfigKey<?>> configKeys); + void reloadActiveSubscribers(long generation); +} diff --git a/container-di/src/main/java/com/yahoo/container/di/config/package-info.java b/container-di/src/main/java/com/yahoo/container/di/config/package-info.java new file mode 100644 index 00000000000..0a04308157a --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/config/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.container.di.config; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/container/di/osgi/package-info.java b/container-di/src/main/java/com/yahoo/container/di/osgi/package-info.java new file mode 100644 index 00000000000..fc7e48bc6ef --- /dev/null +++ b/container-di/src/main/java/com/yahoo/container/di/osgi/package-info.java @@ -0,0 +1,8 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +/** + * @author tonytv + */ +@ExportPackage +package com.yahoo.container.di.osgi; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java b/container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java new file mode 100644 index 00000000000..52719382b4f --- /dev/null +++ b/container-di/src/main/java/com/yahoo/osgi/provider/model/ComponentModel.java @@ -0,0 +1,50 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.osgi.provider.model; + +import com.yahoo.component.ComponentId; +import com.yahoo.component.ComponentSpecification; +import com.yahoo.container.bundle.BundleInstantiationSpecification; +import net.jcip.annotations.Immutable; + +/** + * Describes how a component should be created. + * + * @author gjoranv + */ +@Immutable +public class ComponentModel { + + public final BundleInstantiationSpecification bundleInstantiationSpec; + public final String configId; // only used in the container, null when used in the model + + public ComponentModel(BundleInstantiationSpecification bundleInstantiationSpec, String configId) { + if (bundleInstantiationSpec == null) + throw new IllegalArgumentException("Null bundle instantiation spec!"); + + this.bundleInstantiationSpec = bundleInstantiationSpec; + this.configId = configId; + } + + public ComponentModel(String idSpec, String classSpec, String bundleSpec, String configId) { + this(BundleInstantiationSpecification.getFromStrings(idSpec, classSpec, bundleSpec), configId); + } + + // For vespamodel + public ComponentModel(BundleInstantiationSpecification bundleInstantiationSpec) { + this(bundleInstantiationSpec, null); + } + + // For vespamodel + public ComponentModel(String idSpec, String classSpec, String bundleSpec) { + this(BundleInstantiationSpecification.getFromStrings(idSpec, classSpec, bundleSpec)); + } + + public ComponentId getComponentId() { + return bundleInstantiationSpec.id; + } + + public ComponentSpecification getClassId() { + return bundleInstantiationSpec.classId; + } + +} diff --git a/container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java b/container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java new file mode 100644 index 00000000000..6f201e049f8 --- /dev/null +++ b/container-di/src/main/java/com/yahoo/osgi/provider/model/package-info.java @@ -0,0 +1,5 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +@ExportPackage +package com.yahoo.osgi.provider.model; + +import com.yahoo.osgi.annotation.ExportPackage; diff --git a/container-di/src/main/resources/configdefinitions/bundles.def b/container-di/src/main/resources/configdefinitions/bundles.def new file mode 100644 index 00000000000..b53a34581ba --- /dev/null +++ b/container-di/src/main/resources/configdefinitions/bundles.def @@ -0,0 +1,6 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=1 +namespace=container + +# References to all 3rd-party bundles to be installed. +bundle[] file diff --git a/container-di/src/main/resources/configdefinitions/components.def b/container-di/src/main/resources/configdefinitions/components.def new file mode 100644 index 00000000000..f311648f561 --- /dev/null +++ b/container-di/src/main/resources/configdefinitions/components.def @@ -0,0 +1,24 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +version=3 +namespace=container + +## A list of components. Components depending on other components may use this to +## get its list of components injected. + +## A component +components[].id string +## The component id used by this component to subscribe to its configs (if any) +components[].configId reference default=":parent:" + +## The id of the class to instantiate for this component. +components[].classId string default="" + +## The symbolic name of the Osgi bundle this component is located in. +## Assumed to be the same as the classid if not set. +components[].bundle string default="" + +## The component id of the component to inject to this component +components[].inject[].id string + +## The name to use for the injected component when injected to this component +components[].inject[].name string default="" diff --git a/container-di/src/main/resources/configdefinitions/jersey-bundles.def b/container-di/src/main/resources/configdefinitions/jersey-bundles.def new file mode 100644 index 00000000000..460adf03b51 --- /dev/null +++ b/container-di/src/main/resources/configdefinitions/jersey-bundles.def @@ -0,0 +1,8 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=container.di.config + +# The SymbolicName[:Version] of the Jersey bundles +bundles[].spec string + +# The packages to scan for Jersey resources +bundles[].packages[] string diff --git a/container-di/src/main/resources/configdefinitions/jersey-injection.def b/container-di/src/main/resources/configdefinitions/jersey-injection.def new file mode 100644 index 00000000000..be5dcf5cdf0 --- /dev/null +++ b/container-di/src/main/resources/configdefinitions/jersey-injection.def @@ -0,0 +1,5 @@ +# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +namespace=container.di.config + +inject[].instance string +inject[].forClass string diff --git a/container-di/src/main/scala/com/yahoo/container/bundle/MockBundle.scala b/container-di/src/main/scala/com/yahoo/container/bundle/MockBundle.scala new file mode 100644 index 00000000000..f039a92b285 --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/bundle/MockBundle.scala @@ -0,0 +1,96 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.bundle + + +import java.net.URL +import java.util + +import org.osgi.framework.wiring._ +import org.osgi.framework.{ServiceReference, Version, Bundle} +import java.io.InputStream +import java.util.{Collections, Hashtable, Dictionary} +import MockBundle._ +import org.osgi.resource.{Capability, Wire, Requirement} + +/** + * + * @author gjoranv + * @since 5.15 + */ +class MockBundle extends Bundle with BundleWiring { + + override def getState = Bundle.ACTIVE + + override def start(options: Int) {} + override def start() {} + override def stop(options: Int) {} + override def stop() {} + override def update(input: InputStream) {} + override def update() {} + override def uninstall() {} + + override def getHeaders(locale: String) = getHeaders + + override def getSymbolicName = SymbolicName + override def getVersion: Version = BundleVersion + override def getLocation = getSymbolicName + override def getBundleId: Long = 0L + + override def getHeaders: Dictionary[String, String] = new Hashtable[String, String]() + + override def getRegisteredServices = Array[ServiceReference[_]]() + override def getServicesInUse = getRegisteredServices + + override def hasPermission(permission: Any) = true + + override def getResource(name: String) = throw new UnsupportedOperationException + override def loadClass(name: String) = throw new UnsupportedOperationException + override def getResources(name: String) = throw new UnsupportedOperationException + + override def getEntryPaths(path: String) = throw new UnsupportedOperationException + override def getEntry(path: String) = throw new UnsupportedOperationException + override def findEntries(path: String, filePattern: String, recurse: Boolean) = Collections.emptyEnumeration() + + + override def getLastModified = 1L + + override def getBundleContext = throw new UnsupportedOperationException + override def getSignerCertificates(signersType: Int) = Collections.emptyMap() + + override def adapt[A](`type`: Class[A]) = + `type` match { + case MockBundle.bundleWiringClass => this.asInstanceOf[A] + case _ => ??? + } + + override def getDataFile(filename: String) = null + override def compareTo(o: Bundle) = getBundleId compareTo o.getBundleId + + + //TODO: replace with mockito + override def findEntries(p1: String, p2: String, p3: Int): util.List[URL] = ??? + override def getRequiredResourceWires(p1: String): util.List[Wire] = ??? + override def getResourceCapabilities(p1: String): util.List[Capability] = ??? + override def isCurrent: Boolean = ??? + override def getRequiredWires(p1: String): util.List[BundleWire] = ??? + override def getCapabilities(p1: String): util.List[BundleCapability] = ??? + override def getProvidedResourceWires(p1: String): util.List[Wire] = ??? + override def getProvidedWires(p1: String): util.List[BundleWire] = ??? + override def getRevision: BundleRevision = ??? + override def getResourceRequirements(p1: String): util.List[Requirement] = ??? + override def isInUse: Boolean = ??? + override def listResources(p1: String, p2: String, p3: Int): util.Collection[String] = Collections.emptyList() + override def getClassLoader: ClassLoader = MockBundle.getClass.getClassLoader + override def getRequirements(p1: String): util.List[BundleRequirement] = ??? + override def getResource: BundleRevision = ??? + override def getBundle: Bundle = ??? +} + +object MockBundle { + val SymbolicName = "mock-bundle" + val BundleVersion = new Version(1, 0, 0) + + val bundleWiringClass = classOf[BundleWiring] + + +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/CloudSubscriberFactory.scala b/container-di/src/main/scala/com/yahoo/container/di/CloudSubscriberFactory.scala new file mode 100644 index 00000000000..c9206ee5e05 --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/CloudSubscriberFactory.scala @@ -0,0 +1,100 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di + +import com.yahoo.config.{ConfigInstance} +import CloudSubscriberFactory._ +import config.{Subscriber, SubscriberFactory} +import scala.collection.JavaConversions._ +import com.yahoo.vespa.config.ConfigKey +import scala.Some +import com.yahoo.config.subscription.{ConfigHandle, ConfigSource, ConfigSourceSet, ConfigSubscriber} +import java.lang.IllegalArgumentException +import java.util.logging.Logger +import com.yahoo.log.LogLevel + + +/** + * @author tonytv + */ + +class CloudSubscriberFactory(configSource: ConfigSource) extends SubscriberFactory +{ + private var testGeneration: Option[Long] = None + + private val activeSubscribers = new java.util.WeakHashMap[CloudSubscriber, Int]() + + override def getSubscriber(configKeys: java.util.Set[_ <: ConfigKey[_]]): Subscriber = { + val subscriber = new CloudSubscriber(configKeys.toSet.asInstanceOf[Set[ConfigKeyT]], configSource) + + testGeneration.foreach(subscriber.subscriber.reload(_)) //TODO: test specific code, remove + activeSubscribers.put(subscriber, 0) + + subscriber + } + + //TODO: test specific code, remove + override def reloadActiveSubscribers(generation: Long) { + testGeneration = Some(generation) + + val l = activeSubscribers.keySet().toSet + l.foreach { _.subscriber.reload(generation) } + } +} + +object CloudSubscriberFactory { + val log = Logger.getLogger(classOf[CloudSubscriberFactory].getName) + + private class CloudSubscriber(keys: Set[ConfigKeyT], configSource: ConfigSource) extends Subscriber + { + private[CloudSubscriberFactory] val subscriber = new ConfigSubscriber(configSource) + private val handles: Map[ConfigKeyT, ConfigHandle[_ <: ConfigInstance]] = keys.map(subscribe).toMap + + + //if waitNextGeneration has not yet been called, -1 should be returned + var generation: Long = -1 + + private def subscribe(key: ConfigKeyT) = (key, subscriber.subscribe(key.getConfigClass, key.getConfigId)) + + override def configChanged = handles.values.exists(_.isChanged) + + //mapValues returns a view,, so we need to force evaluation of it here to prevent deferred evaluation. + override def config = handles.mapValues(_.getConfig).toMap.view.force. + asInstanceOf[Map[ConfigKey[ConfigInstance], ConfigInstance]] + + override def waitNextGeneration() = { + require(!handles.isEmpty) + + /* Catch and ignore config exceptions due to missing config values for parameters that do + * not have a default value. These exceptions occur when the user has removed a component + * from services.xml, and the component takes a config that has parameters without a + * default value in the def-file. There is a new 'components' config underway, where the + * component is removed, so this old config generation will soon be replaced by a new one. */ + var gotNextGen = false + var numExceptions = 0 + while (!gotNextGen) { + try{ + if (subscriber.nextGeneration()) + gotNextGen = true + } catch { + case e: IllegalArgumentException => + numExceptions += 1 + log.log(LogLevel.DEBUG, "Ignoring exception from the config library: " + e.getMessage + "\n" + e.getStackTrace) + if (numExceptions >= 5) + throw new IllegalArgumentException("Failed retrieving the next config generation.", e) + } + } + + generation = subscriber.getGeneration + generation + } + + override def close() { + subscriber.close() + } + } + + + class Provider extends com.google.inject.Provider[SubscriberFactory] { + override def get() = new CloudSubscriberFactory(ConfigSourceSet.createDefault()) + } +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/ConfigRetriever.scala b/container-di/src/main/scala/com/yahoo/container/di/ConfigRetriever.scala new file mode 100644 index 00000000000..a1b8167171a --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/ConfigRetriever.scala @@ -0,0 +1,101 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di + + +import config.Subscriber +import java.util.logging.Logger +import com.yahoo.log.LogLevel +import ConfigRetriever._ +import annotation.tailrec +import com.yahoo.config.ConfigInstance +import scala.collection.JavaConversions._ +import com.yahoo.vespa.config.ConfigKey + +/** + * @author tonytv + * @author gjoranv + */ +final class ConfigRetriever(bootstrapKeys: Set[ConfigKeyT], + subscribe: Set[ConfigKeyT] => Subscriber) +{ + require(!bootstrapKeys.isEmpty) + + private val bootstrapSubscriber: Subscriber = subscribe(bootstrapKeys) + + private var componentSubscriber: Subscriber = subscribe(Set()) + private var componentSubscriberKeys: Set[ConfigKeyT] = Set() + + + @tailrec + final def getConfigs(componentConfigKeys: Set[ConfigKeyT], leastGeneration: Long): ConfigSnapshot = { + require(componentConfigKeys intersect bootstrapKeys isEmpty) + log.log(LogLevel.DEBUG, "getConfigs: " + componentConfigKeys) + + setupComponentSubscriber(componentConfigKeys ++ bootstrapKeys) + + getConfigsOptional(leastGeneration) match { + case Some(snapshot) => resetComponentSubscriberIfBootstrap(snapshot); snapshot + case None => getConfigs(componentConfigKeys, leastGeneration) + } + } + + private def getConfigsOptional(leastGeneration: Long): Option[ConfigSnapshot] = { + val newestComponentGeneration = componentSubscriber.waitNextGeneration() + + if (newestComponentGeneration < leastGeneration) { + None + } else if (bootstrapSubscriber.generation < newestComponentGeneration) { + val newestBootstrapGeneration = bootstrapSubscriber.waitNextGeneration() + bootstrapConfigIfChanged() orElse { + if (newestBootstrapGeneration == newestComponentGeneration) componentsConfigIfChanged() + else None + } + } else { + componentsConfigIfChanged() + } + } + + private def bootstrapConfigIfChanged(): Option[BootstrapConfigs] = configIfChanged(bootstrapSubscriber, BootstrapConfigs) + private def componentsConfigIfChanged(): Option[ComponentsConfigs] = configIfChanged(componentSubscriber, ComponentsConfigs) + + private def configIfChanged[T <: ConfigSnapshot](subscriber: Subscriber, + constructor: Map[ConfigKeyT, ConfigInstance] => T ): + Option[T] = { + if (subscriber.configChanged) Some(constructor(subscriber.config.toMap)) + else None + } + + private def resetComponentSubscriberIfBootstrap(snapshot: ConfigSnapshot) { + snapshot match { + case BootstrapConfigs(_) => setupComponentSubscriber(Set()) + case _ => + } + } + + private def setupComponentSubscriber(keys: Set[ConfigKeyT]) { + if (componentSubscriberKeys != keys) { + componentSubscriber.close() + + componentSubscriberKeys = keys + componentSubscriber = subscribe(keys) + } + } + + def shutdown() { + bootstrapSubscriber.close() + componentSubscriber.close() + } + + //TODO: check if these are really needed + final def getBootstrapGeneration = bootstrapSubscriber.generation + final def getComponentsGeneration = componentSubscriber.generation +} + + +object ConfigRetriever { + private val log = Logger.getLogger(classOf[ConfigRetriever].getName) + + sealed abstract class ConfigSnapshot + case class BootstrapConfigs(configs: Map[ConfigKeyT, ConfigInstance]) extends ConfigSnapshot + case class ComponentsConfigs(configs: Map[ConfigKeyT, ConfigInstance]) extends ConfigSnapshot +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/Container.scala b/container-di/src/main/scala/com/yahoo/container/di/Container.scala new file mode 100644 index 00000000000..28d99f89d73 --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/Container.scala @@ -0,0 +1,200 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di + +import com.yahoo.container.di.ConfigRetriever.{ComponentsConfigs, BootstrapConfigs} +import com.yahoo.container.di.componentgraph.core.{JerseyNode, ComponentGraph, ComponentNode} +import com.yahoo.container.di.config.{RestApiContext, SubscriberFactory} +import Container._ + +import scala.collection.JavaConversions._ +import scala.math.max +import com.yahoo.config._ +import com.yahoo.vespa.config.ConfigKey +import java.util.IdentityHashMap +import java.util.logging.Logger +import com.yahoo.container.bundle.BundleInstantiationSpecification +import com.google.inject.{Injector, Guice} +import com.yahoo.container.{BundlesConfig, ComponentsConfig} + + +/** + * + * @author gjoranv + * @author tonytv + */ +class Container( + subscriberFactory: SubscriberFactory, + configId: String, + componentDeconstructor: ComponentDeconstructor, + osgi: Osgi = new Osgi {} + ) +{ + val bundlesConfigKey = new ConfigKey(classOf[BundlesConfig], configId) + val componentsConfigKey = new ConfigKey(classOf[ComponentsConfig], configId) + + var configurer = new ConfigRetriever(Set(bundlesConfigKey, componentsConfigKey), subscriberFactory.getSubscriber(_)) + var previousConfigGeneration = -1L + var leastGeneration = -1L + + @throws(classOf[InterruptedException]) + def runOnce( + oldGraph: ComponentGraph = new ComponentGraph, + fallbackInjector: GuiceInjector = Guice.createInjector()): ComponentGraph = { + + def deconstructObsoleteComponents(oldGraph: ComponentGraph, newGraph: ComponentGraph) { + val oldComponents = new IdentityHashMap[AnyRef, AnyRef]() + oldGraph.allComponentsAndProviders foreach(oldComponents.put(_, null)) + newGraph.allComponentsAndProviders foreach(oldComponents.remove(_)) + oldComponents.keySet foreach(componentDeconstructor.deconstruct(_)) + } + + try { + //TODO: wrap user exceptions. + val newGraph = createNewGraph(oldGraph, fallbackInjector) + newGraph.reuseNodes(oldGraph) + constructComponents(newGraph) + deconstructObsoleteComponents(oldGraph, newGraph) + newGraph + } catch { + case e : Throwable => + invalidateGeneration() + throw e + } + } + + private def invalidateGeneration() { + leastGeneration = max(configurer.getComponentsGeneration, configurer.getBootstrapGeneration) + 1 + } + + final def createNewGraph(graph: ComponentGraph = new ComponentGraph, + fallbackInjector: Injector): ComponentGraph = { + + val snapshot = configurer.getConfigs(graph.configKeys, leastGeneration) + log.fine("""createNewGraph: + graph.configKeys = %s + graph.generation = %s + snapshot = %s + """.format(graph.configKeys, graph.generation, snapshot)) + + val preventTailRecursion = + snapshot match { + case BootstrapConfigs(configs) if getBootstrapGeneration > previousConfigGeneration => + installBundles(configs) + createNewGraph( + createComponentsGraph(configs, getBootstrapGeneration,fallbackInjector), + fallbackInjector) + case BootstrapConfigs(_) => + createNewGraph(graph, fallbackInjector) + case ComponentsConfigs(configs) => + createAndConfigureComponentsGraph(configs, fallbackInjector) + } + + preventTailRecursion + } + + + def getBootstrapGeneration: Long = { + configurer.getBootstrapGeneration + } + + def getComponentsGeneration: Long = { + configurer.getComponentsGeneration + } + + private def createAndConfigureComponentsGraph[T]( + componentsConfigs: Map[ConfigKeyT, ConfigInstance], + fallbackInjector: Injector): ComponentGraph = { + + val componentGraph = createComponentsGraph(componentsConfigs, getComponentsGeneration, fallbackInjector) + componentGraph.setAvailableConfigs(componentsConfigs) + componentGraph + } + + def injectNodes(config: ComponentsConfig, graph: ComponentGraph) { + for { + component <- config.components() + inject <- component.inject() + } { + def getNode = ComponentGraph.getNode(graph, _: String) + + //TODO: Support inject.name() + getNode(component.id()).inject(getNode(inject.id())) + } + + } + + def installBundles(configsIncludingBootstrapConfigs: Map[ConfigKeyT, ConfigInstance]) { + val bundlesConfig = getConfig(bundlesConfigKey, configsIncludingBootstrapConfigs) + osgi.useBundles(bundlesConfig.bundle()) + } + + private def createComponentsGraph[T]( + configsIncludingBootstrapConfigs: Map[ConfigKeyT, ConfigInstance], + generation: Long, + fallbackInjector: Injector): ComponentGraph = { + + previousConfigGeneration = generation + + val graph = new ComponentGraph(generation) + + val componentsConfig = getConfig(componentsConfigKey, configsIncludingBootstrapConfigs) + addNodes(componentsConfig, graph) + injectNodes(componentsConfig, graph) + + graph.complete(fallbackInjector) + graph + } + + def addNodes[T](componentsConfig: ComponentsConfig, graph: ComponentGraph) { + def isRestApiContext(clazz: Class[_]) = classOf[RestApiContext].isAssignableFrom(clazz) + def asRestApiContext(clazz: Class[_]) = clazz.asInstanceOf[Class[RestApiContext]] + + for (config : ComponentsConfig.Components <- componentsConfig.components) { + val specification = bundleInstatiationSpecification(config) + val componentClass = osgi.resolveClass(specification) + + val componentNode = + if (isRestApiContext(componentClass)) + new JerseyNode(specification.id, config.configId(), asRestApiContext(componentClass), osgi) + else + new ComponentNode(specification.id, config.configId(), componentClass) + + graph.add(componentNode) + } + } + + private def constructComponents(graph: ComponentGraph) { + graph.nodes foreach (_.newOrCachedInstance()) + } + + def shutdown(graph: ComponentGraph, deconstructor: ComponentDeconstructor) { + shutdownConfigurer() + if (graph != null) + deconstructAllComponents(graph, deconstructor) + } + + def shutdownConfigurer() { + configurer.shutdown() + } + + // Reload config manually, when subscribing to non-configserver sources + def reloadConfig(generation: Long) { + subscriberFactory.reloadActiveSubscribers(generation) + } + + def deconstructAllComponents(graph: ComponentGraph, deconstructor: ComponentDeconstructor) { + graph.allComponentsAndProviders foreach(deconstructor.deconstruct(_)) + } + +} + +object Container { + val log = Logger.getLogger(classOf[Container].getName) + + def getConfig[T <: ConfigInstance](key: ConfigKey[T], configs: Map[ConfigKeyT, ConfigInstance]) : T = { + key.getConfigClass.cast(configs.getOrElse(key.asInstanceOf[ConfigKeyT], sys.error("Missing config " + key))) + } + + def bundleInstatiationSpecification(config: ComponentsConfig.Components) = + BundleInstantiationSpecification.getFromStrings(config.id(), config.classId(), config.bundle()) +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/Osgi.scala b/container-di/src/main/scala/com/yahoo/container/di/Osgi.scala new file mode 100644 index 00000000000..6752559fde6 --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/Osgi.scala @@ -0,0 +1,40 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di + +import com.yahoo.config.FileReference +import com.yahoo.container.bundle.{MockBundle, BundleInstantiationSpecification} +import com.yahoo.container.di.Osgi.BundleClasses +import org.osgi.framework.Bundle +import com.yahoo.component.ComponentSpecification + +/** + * + * @author gjoranv + * @author tonytv + */ +trait Osgi { + + def getBundleClasses(bundle: ComponentSpecification, packagesToScan: Set[String]): BundleClasses = { + BundleClasses(new MockBundle, List()) + } + + def useBundles(bundles: java.util.Collection[FileReference]) { + println("useBundles " + bundles.toArray.mkString(", ")) + } + + def resolveClass(spec: BundleInstantiationSpecification): Class[AnyRef] = { + println("resolving class " + spec.classId) + Class.forName(spec.classId.getName).asInstanceOf[Class[AnyRef]] + } + + def getBundle(spec: ComponentSpecification): Bundle = { + println("resolving bundle " + spec) + new MockBundle() + } + +} + +object Osgi { + type RelativePath = String //e.g. "com/yahoo/MyClass.class" + case class BundleClasses(bundle: Bundle, classEntries: Iterable[RelativePath]) +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentGraph.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentGraph.scala new file mode 100644 index 00000000000..fe0aa2fd001 --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentGraph.scala @@ -0,0 +1,330 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.componentgraph.core + +import java.util.logging.Logger + +import com.yahoo.component.provider.ComponentRegistry +import com.yahoo.config.ConfigInstance + +import java.lang.annotation.{Annotation => JavaAnnotation} +import java.lang.IllegalStateException +import collection.mutable +import annotation.tailrec + +import com.yahoo.container.di.{ConfigKeyT, GuiceInjector} +import com.yahoo.container.di.componentgraph.Provider +import com.yahoo.vespa.config.ConfigKey +import com.google.inject.{Guice, ConfigurationException, Key, BindingAnnotation} +import net.jcip.annotations.NotThreadSafe + +import com.yahoo.component.{AbstractComponent, ComponentId} +import java.lang.reflect.{TypeVariable, WildcardType, Method, ParameterizedType, Type} +import com.yahoo.container.di.removeStackTrace +import scala.util.Try +import scala.Some + +/** + * @author tonytv + * @author gjoranv + */ +@NotThreadSafe +class ComponentGraph(val generation: Long = 0) { + + import ComponentGraph._ + + private var nodesById = Map[ComponentId, Node]() + + private[di] def size = nodesById.size + + def nodes = nodesById.values + + def add(component: Node) { + require(!nodesById.isDefinedAt(component.componentId), "Multiple components with the same id " + component.componentId) + nodesById += component.componentId -> component + } + + def lookupGlobalComponent(key: Key[_]): Option[Node] = { + require(key.getTypeLiteral.getType.isInstanceOf[Class[_]], "Type not supported " + key.getTypeLiteral) + val clazz = key.getTypeLiteral.getRawType + + + val components = matchingComponentNodes(nodesById.values, key) + + def singleNonProviderComponentOrThrow: Option[Node] = { + val nonProviderComponents = components filter (c => !classOf[Provider[_]].isAssignableFrom(c.instanceType)) + nonProviderComponents.size match { + case 0 => throw new IllegalStateException(s"Multiple global component providers for class '${clazz.getName}' found") + case 1 => Some(nonProviderComponents.head.asInstanceOf[Node]) + case _ => throw new IllegalStateException(s"Multiple global components with class '${clazz.getName}' found") + } + } + + components.size match { + case 0 => None + case 1 => Some(components.head.asInstanceOf[Node]) + case _ => singleNonProviderComponentOrThrow + } + } + + def getInstance[T](clazz: Class[T]) : T = { + getInstance(Key.get(clazz)) + } + + def getInstance[T](key: Key[T]) : T = { + lookupGlobalComponent(key). + // TODO: Combine exception handling with lookupGlobalComponent. + getOrElse(throw new IllegalStateException("No global component with key '%s' ".format(key.toString))). + newOrCachedInstance().asInstanceOf[T] + } + + private def componentNodes: Traversable[ComponentNode] = + nodesOfType(nodesById.values, classOf[ComponentNode]) + + private def componentRegistryNodes: Traversable[ComponentRegistryNode] = + nodesOfType(nodesById.values, classOf[ComponentRegistryNode]) + + private def osgiComponentsOfClass(clazz: Class[_]): Traversable[ComponentNode] = { + componentNodes.filter(node => clazz.isAssignableFrom(node.componentType)) + } + + def complete(fallbackInjector: GuiceInjector = Guice.createInjector()) { + def detectCycles = topologicalSort(nodesById.values.toList) + + componentNodes foreach {completeNode(_, fallbackInjector)} + componentRegistryNodes foreach completeComponentRegistryNode + detectCycles + } + + def configKeys: Set[ConfigKeyT] = { + nodesById.values.flatMap(_.configKeys).toSet + } + + def setAvailableConfigs(configs: Map[ConfigKeyT, ConfigInstance]) { + componentNodes foreach { _.setAvailableConfigs(configs) } + } + + def reuseNodes(old: ComponentGraph) { + def copyInstancesIfNodeEqual() { + val commonComponentIds = nodesById.keySet & old.nodesById.keySet + for (id <- commonComponentIds) { + if (nodesById(id) == old.nodesById(id)) { + nodesById(id).instance = old.nodesById(id).instance + } + } + } + def resetInstancesWithModifiedDependencies() { + for { + node <- topologicalSort(nodesById.values.toList) + usedComponent <- node.usedComponents + } { + if (usedComponent.instance == None) { + node.instance = None + } + } + } + + copyInstancesIfNodeEqual() + resetInstancesWithModifiedDependencies() + } + + def allComponentsAndProviders = nodes map {_.instance.get} + + private def completeComponentRegistryNode(registry: ComponentRegistryNode) { + registry.injectAll(osgiComponentsOfClass(registry.componentClass)) + } + + private def completeNode(node: ComponentNode, fallbackInjector: GuiceInjector) { + try { + val arguments = node.getAnnotatedConstructorParams.map(handleParameter(node, fallbackInjector, _)) + + node.setArguments(arguments) + } catch { + case e : Exception => throw removeStackTrace(new RuntimeException(s"When resolving dependencies of ${node.idAndType}", e)) + } + } + + private def handleParameter(node : Node, + fallbackInjector: GuiceInjector, + annotatedParameterType: (Type, Array[JavaAnnotation])): AnyRef = + { + def isConfigParameter(clazz : Class[_]) = classOf[ConfigInstance].isAssignableFrom(clazz) + def isComponentRegistry(t : Type) = t == classOf[ComponentRegistry[_]] + + val (parameterType, annotations) = annotatedParameterType + + (parameterType match { + case componentIdClass: Class[_] if componentIdClass == classOf[ComponentId] => node.componentId + case configClass : Class[_] if isConfigParameter(configClass) => handleConfigParameter(node.asInstanceOf[ComponentNode], configClass) + case registry : ParameterizedType if isComponentRegistry(registry.getRawType) => getComponentRegistry(registry.getActualTypeArguments.head) + case clazz : Class[_] => handleComponentParameter(node, fallbackInjector, clazz, annotations) + case other: ParameterizedType => sys.error(s"Injection of parameterized type $other is not supported.") + case other => sys.error(s"Injection of type $other is not supported.") + }).asInstanceOf[AnyRef] + } + + + def newComponentRegistryNode(componentClass: Class[AnyRef]): ComponentRegistryNode = { + val registry = new ComponentRegistryNode(componentClass) + add(registry) //TODO: don't mutate nodes here. + registry + } + + private def getComponentRegistry(componentType : Type) : ComponentRegistryNode = { + val componentClass = componentType match { + case wildCardType: WildcardType => + assert(wildCardType.getLowerBounds.isEmpty) + assert(wildCardType.getUpperBounds.size == 1) + wildCardType.getUpperBounds.head.asInstanceOf[Class[AnyRef]] + case clazz: Class[AnyRef] => clazz + case typeVariable: TypeVariable[_] => + throw new RuntimeException("Can't create ComponentRegistry of unknown type variable " + typeVariable) + } + + componentRegistryNodes.find(_.componentClass == componentType). + getOrElse(newComponentRegistryNode(componentClass)) + } + + def handleConfigParameter(node : ComponentNode, clazz: Class[_]) : ConfigKeyT = { + new ConfigKey(clazz.asInstanceOf[Class[ConfigInstance]], node.configId) + } + + def getKey(clazz: Class[_], bindingAnnotation: Option[JavaAnnotation]) = + bindingAnnotation.map(Key.get(clazz, _)).getOrElse(Key.get(clazz)) + + private def handleComponentParameter(node: Node, + fallbackInjector: GuiceInjector, + clazz: Class[_], + annotations: Array[JavaAnnotation]) : Node = { + + val bindingAnnotations = annotations.filter(isBindingAnnotation) + val key = getKey(clazz, bindingAnnotations.headOption) + + def matchingGuiceNode(key: Key[_], instance: AnyRef): Option[GuiceNode] = { + matchingNodes(nodesById.values, classOf[GuiceNode], key). + filter(node => node.newOrCachedInstance eq instance). // TODO: assert that there is only one (after filter) + headOption + } + + def lookupOrCreateGlobalComponent: Node = { + lookupGlobalComponent(key).getOrElse { + val instance = + try { + log.info("Trying the fallback injector to create" + messageForNoGlobalComponent(clazz, node)) + fallbackInjector.getInstance(key).asInstanceOf[AnyRef] + } catch { + case e: ConfigurationException => + throw removeStackTrace(new IllegalStateException( + if (messageForMultipleClassLoaders(clazz).isEmpty) + "No global" + messageForNoGlobalComponent(clazz, node) + else + messageForMultipleClassLoaders(clazz))) + + } + matchingGuiceNode(key, instance).getOrElse { + val node = new GuiceNode(instance, key.getAnnotation) + add(node) + node + } + } + } + + if (bindingAnnotations.size > 1) + sys.error("More than one binding annotation used in class '%s'".format(node.instanceType)) + + val injectedNodesOfCorrectType = matchingComponentNodes(node.componentsToInject, key) + injectedNodesOfCorrectType.size match { + case 0 => lookupOrCreateGlobalComponent + case 1 => injectedNodesOfCorrectType.head.asInstanceOf[Node] + case _ => sys.error("Multiple components of type '%s' injected into component '%s'".format(clazz.getName, node.instanceType)) //TODO: !className for last parameter + } + } + +} + +object ComponentGraph { + val log = Logger.getLogger(classOf[ComponentGraph].getName) + + def messageForNoGlobalComponent(clazz: Class[_], node: Node) = + s" component of class ${clazz.getName} to inject into component ${node.idAndType}." + + def messageForMultipleClassLoaders(clazz: Class[_]): String = { + val errMsg = "Class " + clazz.getName + " is provided by the framework, and cannot be embedded in a user bundle. " + + "To resolve this problem, please refer to osgi-classloading.html#multiple-implementations in the documentation" + + (for { + resolvedClass <- Try {Class.forName(clazz.getName, false, this.getClass.getClassLoader)} + if resolvedClass != clazz + } yield errMsg) + .getOrElse("") + } + + // For unit testing + def getNode(graph: ComponentGraph, componentId: String): Node = { + graph.nodesById(new ComponentId(componentId)) + } + + private def nodesOfType[T <: Node](nodes: Traversable[Node], clazz : Class[T]) : Traversable[T] = { + nodes.collect { + case node if clazz.isInstance(node) => clazz.cast(node) + } + } + + private def matchingComponentNodes(nodes: Traversable[Node], key: Key[_]) : Traversable[ComponentNode] = { + matchingNodes(nodes, classOf[ComponentNode], key) + } + + // Finds all nodes with a given nodeType and instance with given key + private def matchingNodes[T <: Node](nodes: Traversable[Node], nodeType: Class[T], key: Key[_]) : Traversable[T] = { + val clazz = key.getTypeLiteral.getRawType + val annotation = key.getAnnotation + + val filteredByClass = nodesOfType(nodes, nodeType) filter { node => clazz.isAssignableFrom(node.componentType) } + val filteredByClassAndAnnotation = filteredByClass filter { node => annotation == node.instanceKey.getAnnotation } + + if (filteredByClass.size == 1) filteredByClass + else if (filteredByClassAndAnnotation.size > 0) filteredByClassAndAnnotation + else filteredByClass + } + + // Returns true if annotation is a BindingAnnotation, e.g. com.google.inject.name.Named + def isBindingAnnotation(annotation: JavaAnnotation) : Boolean = { + def isBindingAnnotation(clazz: Class[_]) : Boolean = { + val clazzOrSuperIsBindingAnnotation = + (clazz.getAnnotation(classOf[BindingAnnotation]) != null) || + Option(clazz.getSuperclass).map(isBindingAnnotation).getOrElse(false) + + (clazzOrSuperIsBindingAnnotation /: clazz.getInterfaces.map(isBindingAnnotation))(_ || _) + } + isBindingAnnotation(annotation.getClass) + } + + /** + * The returned list is the nodes from the graph bottom-up. + * @return A list where a earlier than b in the list implies that there is no path from a to b + */ + def topologicalSort(nodes: List[Node]): List[Node] = { + val numIncoming = mutable.Map[ComponentId, Int]().withDefaultValue(0) + + def forEachUsedComponent(nodes: Traversable[Node])(f: Node => Unit) { + nodes.foreach(_.usedComponents.foreach(f)) + } + + def partitionByNoIncoming(nodes: List[Node]) = + nodes.partition(node => numIncoming(node.componentId) == 0) + + @tailrec + def sort(sorted: List[Node], unsorted: List[Node]) : List[Node] = { + if (unsorted.isEmpty) { + sorted + } else { + val (ready, notReady) = partitionByNoIncoming(unsorted) + require(!ready.isEmpty, "There's a cycle in the graph.") //TODO: return cycle + forEachUsedComponent(ready) { injectedNode => numIncoming(injectedNode.componentId) -= 1} + sort(ready ::: sorted, notReady) + } + } + + forEachUsedComponent(nodes) { injectedNode => numIncoming(injectedNode.componentId) += 1 } + sort(List(), nodes) + } +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentNode.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentNode.scala new file mode 100644 index 00000000000..61f3c7a049c --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentNode.scala @@ -0,0 +1,205 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.componentgraph.core + +import java.lang.reflect.{Modifier, ParameterizedType, Constructor, Type, InvocationTargetException} +import java.util.logging.Logger +import com.google.inject.Inject + +import com.yahoo.config.ConfigInstance +import com.yahoo.vespa.config.ConfigKey +import com.yahoo.component.{ComponentId, AbstractComponent} +import com.yahoo.container.di.{ConfigKeyT, JavaAnnotation, createKey, makeClassCovariant, removeStackTrace, preserveStackTrace} +import com.yahoo.container.di.componentgraph.Provider + +import Node.equalEdges +import ComponentNode._ +import java.lang.IllegalStateException +import scala.Some +import scala.Array + +/** + * @author tonytv + * @author gjoranv + */ +class ComponentNode(componentId: ComponentId, + val configId: String, + clazz: Class[_ <: AnyRef], + val XXX_key: JavaAnnotation = null) // TODO expose key, not javaAnnotation + extends Node(componentId) +{ + require(!isAbstract(clazz), "Can't instantiate abstract class " + clazz.getName) + + var arguments : Array[AnyRef] = _ + + val constructor: Constructor[AnyRef] = bestConstructor(clazz) + + var availableConfigs: Map[ConfigKeyT, ConfigInstance] = null + + override val instanceKey = createKey(clazz, XXX_key) + + override val instanceType = clazz + + override def usedComponents: List[Node] = { + require(arguments != null, "Arguments must be set first.") + arguments.collect{case node: Node => node}.toList + } + + override val componentType: Class[AnyRef] = { + def allSuperClasses(clazz: Class[_], coll : List[Class[_]]) : List[Class[_]] = { + if (clazz == null) coll + else allSuperClasses(clazz.getSuperclass, clazz :: coll) + } + + def allGenericInterfaces(clazz : Class[_]) = allSuperClasses(clazz, List()) flatMap (_.getGenericInterfaces) + + def isProvider = classOf[Provider[_]].isAssignableFrom(clazz) + def providerComponentType = (allGenericInterfaces(clazz).collect { + case t: ParameterizedType if t.getRawType == classOf[Provider[_]] => t.getActualTypeArguments.head + }).head + + if (isProvider) providerComponentType.asInstanceOf[Class[AnyRef]] //TODO: Test what happens if you ask for something that isn't a class, e.g. a parametrized type. + else clazz.asInstanceOf[Class[AnyRef]] + } + + def setArguments(arguments: Array[AnyRef]) { + this.arguments = arguments + } + + def cutStackTraceAtConstructor(throwable: Throwable): Throwable = { + def takeUntilComponentNode(elements: Array[StackTraceElement]) = + elements.takeWhile(_.getClassName != classOf[ComponentNode].getName) + + def dropToInitAtEnd(elements: Array[StackTraceElement]) = + elements.reverse.dropWhile(_.getMethodName != "<init>").reverse + + val modifyStackTrace = takeUntilComponentNode _ andThen dropToInitAtEnd + + val dependencyInjectorStackTraceMarker = new StackTraceElement("============= Dependency Injection =============", "newInstance", null, -1) + + if (throwable != null && !preserveStackTrace) { + throwable.setStackTrace(modifyStackTrace(throwable.getStackTrace) :+ + dependencyInjectorStackTraceMarker) + + cutStackTraceAtConstructor(throwable.getCause) + } + throwable + } + + override protected def newInstance() : AnyRef = { + assert (arguments != null, "graph.complete must be called before retrieving instances.") + + val actualArguments = arguments.map { + case node: Node => node.newOrCachedInstance() + case config: ConfigKeyT => availableConfigs(config.asInstanceOf[ConfigKeyT]) + case other => other + } + + val instance = + try { + constructor.newInstance(actualArguments: _*) + } catch { + case e: InvocationTargetException => + throw removeStackTrace(new RuntimeException(s"An exception occurred while constructing $idAndType", + cutStackTraceAtConstructor(e.getCause))) + + } + + initId(instance) + } + + private def initId(component: AnyRef) = { + def checkAndSetId(c: AbstractComponent) { + if (c.hasInitializedId && c.getId != componentId ) + throw new IllegalStateException("Component with id '" + componentId + "' has set a bogus component id: '" + c.getId + "'") + + c.initId(componentId) + } + + component match { + case component: AbstractComponent => checkAndSetId(component) + case other => () + } + component + } + + override def equals(other: Any) = { + other match { + case that: ComponentNode => + super.equals(that) && + equalEdges(arguments.toList, that.arguments.toList) && + usedConfigs == that.usedConfigs + } + } + + private def usedConfigs = { + require(availableConfigs != null, "setAvailableConfigs must be called!") + ( arguments collect {case c: ConfigKeyT => c} map (availableConfigs) ).toList + } + + def getAnnotatedConstructorParams: Array[(Type, Array[JavaAnnotation])] = { + constructor.getGenericParameterTypes zip constructor.getParameterAnnotations + } + + def setAvailableConfigs(configs: Map[ConfigKeyT, ConfigInstance]) { + require (arguments != null, "graph.complete must be called before graph.setAvailableConfigs.") + availableConfigs = configs + } + + override def configKeys = { + configParameterClasses.map(new ConfigKey(_, configId)).toSet + } + + + private def configParameterClasses: Array[Class[ConfigInstance]] = { + constructor.getGenericParameterTypes.collect { + case clazz: Class[_] if classOf[ConfigInstance].isAssignableFrom(clazz) => clazz.asInstanceOf[Class[ConfigInstance]] + } + } + + override def label = { + val configNames = configKeys.map(_.getName + ".def").toList + + (List(instanceType.getSimpleName, Node.packageName(instanceType)) ::: configNames). + mkString("{", "|", "}") + } + +} + +object ComponentNode { + val log = Logger.getLogger(classOf[ComponentNode].getName) + + private def bestConstructor(clazz: Class[AnyRef]) = { + val publicConstructors = clazz.getConstructors.asInstanceOf[Array[Constructor[AnyRef]]] + + def constructorAnnotatedWithInject = { + publicConstructors filter {_.getAnnotation(classOf[Inject]) != null} match { + case Array() => None + case Array(single) => Some(single) + case _ => throwRuntimeExceptionRemoveStackTrace("Multiple constructors annotated with inject in class " + clazz.getName) + } + } + + def constructorWithMostConfigParameters = { + def isConfigInstance(clazz: Class[_]) = classOf[ConfigInstance].isAssignableFrom(clazz) + + publicConstructors match { + case Array() => throwRuntimeExceptionRemoveStackTrace("No public constructors in class " + clazz.getName) + case Array(single) => single + case _ => + log.warning("Multiple public constructors found in class %s, there should only be one. ".format(clazz.getName) + + "If more than one public constructor is needed, the primary one must be annotated with @Inject.") + publicConstructors. + sortBy(_.getParameterTypes.filter(isConfigInstance).size). + last + } + } + + constructorAnnotatedWithInject getOrElse constructorWithMostConfigParameters + } + + private def throwRuntimeExceptionRemoveStackTrace(message: String) = + throw removeStackTrace(new RuntimeException(message)) + + + def isAbstract(clazz: Class[_ <: AnyRef]) = Modifier.isAbstract(clazz.getModifiers) +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.scala new file mode 100644 index 00000000000..864eb17ddfb --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/ComponentRegistryNode.scala @@ -0,0 +1,71 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.componentgraph.core + +import com.yahoo.component.provider.ComponentRegistry +import com.yahoo.component.{ComponentId, Component} +import ComponentRegistryNode._ +import com.google.inject.Key +import com.google.inject.util.Types +import Node.syntheticComponentId + +/** + * @author tonytv + * @author gjoranv + */ +class ComponentRegistryNode(val componentClass : Class[AnyRef]) + extends Node(componentId(componentClass)) { + + def usedComponents = componentsToInject + + protected def newInstance() = { + val registry = new ComponentRegistry[AnyRef] + + componentsToInject foreach { component => + registry.register(component.componentId, component.newOrCachedInstance()) + } + + registry + } + + override val instanceKey = + Key.get(Types.newParameterizedType(classOf[ComponentRegistry[_]], componentClass)).asInstanceOf[Key[AnyRef]] + + override val instanceType: Class[AnyRef] = instanceKey.getTypeLiteral.getRawType.asInstanceOf[Class[AnyRef]] + override val componentType: Class[AnyRef] = instanceType + + override def configKeys = Set() + + override def equals(other: Any) = { + other match { + case that: ComponentRegistryNode => + componentId == that.componentId && // includes componentClass + instanceType == that.instanceType && + equalEdges(usedComponents, that.usedComponents) + case _ => false + } + } + + override def label = + "{ComponentRegistry\\<%s\\>|%s}".format(componentClass.getSimpleName, Node.packageName(componentClass)) +} + +object ComponentRegistryNode { + val componentRegistryNamespace = ComponentId.fromString("ComponentRegistry") + + def componentId(componentClass: Class[_]) = { + syntheticComponentId(componentClass.getName, componentClass, componentRegistryNamespace) + } + + def equalEdges(edges: List[Node], otherEdges: List[Node]): Boolean = { + def compareEdges = { + (sortByComponentId(edges) zip sortByComponentId(otherEdges)). + forall(equalEdge) + } + + def sortByComponentId(in: List[Node]) = in.sortBy(_.componentId) + def equalEdge(edgePair: (Node, Node)): Boolean = edgePair._1.componentId == edgePair._2.componentId + + edges.size == otherEdges.size && + compareEdges + } +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/DotGraph.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/DotGraph.scala new file mode 100644 index 00000000000..d31c78af0d1 --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/DotGraph.scala @@ -0,0 +1,46 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.componentgraph.core + +/** + * Generate dot graph from a ComponentGraph + * + * @author tonytv + * @author gjoranv + * @since 5.1.4 + */ + +object DotGraph { + + def generate(graph: ComponentGraph): String = { + val nodes = graph.nodes map(node) + + val edges = for { + node <- graph.nodes + usedNode <- node.usedComponents + } yield edge(node, usedNode) + + (nodes ++ edges). + mkString( + """|digraph { + | graph [ratio="compress"]; + | """.stripMargin, "\n ", "\n}") + } + + private def label(node: Node) = { + node.label.replace("\n", "\\n") + } + + private def node(node: Node): String = { + <node> + "{node.componentId.stringValue()}"[shape=record, fontsize=11, label="{label(node)}"]; + </node>.text.trim + } + + private def edge(node: Node, usedNode:Node): String = { + <edge> + "{node.componentId.stringValue()}"->"{usedNode.componentId.stringValue()}"; + </edge>.text.trim + + } + +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/GuiceNode.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/GuiceNode.scala new file mode 100644 index 00000000000..f96284d5924 --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/GuiceNode.scala @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.componentgraph.core + +import com.yahoo.container.di.{JavaAnnotation, createKey} +import com.yahoo.component.ComponentId +import Node.syntheticComponentId +import GuiceNode._ + +/** + * @author tonytv + * @author gjoranv + */ +final class GuiceNode(myInstance: AnyRef, + annotation: JavaAnnotation) extends Node(componentId(myInstance)) { + + override def configKeys = Set() + + override val instanceKey = createKey(myInstance.getClass, annotation) + override val instanceType = myInstance.getClass + override val componentType = instanceType + + + override def usedComponents = List() + + override protected def newInstance() = myInstance + + override def inject(component: Node) { + throw new UnsupportedOperationException("Illegal to inject components to a GuiceNode!") + } + + override def label = + "{{%s|Guice}|%s}".format(instanceType.getSimpleName, Node.packageName(instanceType)) +} + +object GuiceNode { + val guiceNamespace = ComponentId.fromString("Guice") + + def componentId(instance: AnyRef) = { + syntheticComponentId(instance.getClass.getName, instance, guiceNamespace) + } +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/JerseyNode.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/JerseyNode.scala new file mode 100644 index 00000000000..3831e8f3404 --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/JerseyNode.scala @@ -0,0 +1,93 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.componentgraph.core + +import com.yahoo.component.{ComponentSpecification, ComponentId} +import com.yahoo.container.di.Osgi.RelativePath +import com.yahoo.container.di.config.RestApiContext +import org.osgi.framework.wiring.BundleWiring +import scala.collection.JavaConverters._ + +import scala.collection.convert.wrapAsJava._ +import RestApiContext.BundleInfo +import JerseyNode._ +import com.yahoo.container.di.Osgi +import org.osgi.framework.Bundle +import java.net.URL + +/** + * Represents an instance of RestApiContext + * + * @author gjoranv + * @author tonytv + * @since 5.15 + */ +class JerseyNode(componentId: ComponentId, + override val configId: String, + clazz: Class[_ <: RestApiContext], + osgi: Osgi) + extends ComponentNode(componentId, configId, clazz) { + + + override protected def newInstance(): RestApiContext = { + val instance = super.newInstance() + val restApiContext = instance.asInstanceOf[RestApiContext] + + val bundles = restApiContext.bundlesConfig.bundles().asScala + bundles foreach ( bundleConfig => { + val bundleClasses = osgi.getBundleClasses( + ComponentSpecification.fromString(bundleConfig.spec()), + bundleConfig.packages().asScala.toSet) + + restApiContext.addBundle( + createBundleInfo(bundleClasses.bundle, bundleClasses.classEntries)) + }) + + componentsToInject foreach ( + component => + restApiContext.addInjectableComponent(component.instanceKey, component.componentId, component.newOrCachedInstance())) + + restApiContext + } + + override def equals(other: Any): Boolean = { + super.equals(other) && (other match { + case that: JerseyNode => componentsToInject == that.componentsToInject + case _ => false + }) + } + +} + +private[core] +object JerseyNode { + val WebInfUrl = "WebInfUrl" + + def createBundleInfo(bundle: Bundle, classEntries: Iterable[RelativePath]): BundleInfo = { + + val bundleInfo = new BundleInfo(bundle.getSymbolicName, + bundle.getVersion, + bundle.getLocation, + webInfUrl(bundle), + bundle.adapt(classOf[BundleWiring]).getClassLoader) + + bundleInfo.setClassEntries(classEntries) + bundleInfo + } + + + private def getBundle(osgi: Osgi, bundleSpec: String): Bundle = { + + val bundle = osgi.getBundle(ComponentSpecification.fromString(bundleSpec)) + if (bundle == null) + throw new IllegalArgumentException("Bundle not found: " + bundleSpec) + bundle + } + + private def webInfUrl(bundle: Bundle): URL = { + val strWebInfUrl = bundle.getHeaders.get(WebInfUrl) + + if (strWebInfUrl == null) null + else bundle.getEntry(strWebInfUrl) + } + +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/Node.scala b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/Node.scala new file mode 100644 index 00000000000..9d30552b0aa --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/componentgraph/core/Node.scala @@ -0,0 +1,111 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.componentgraph.core + +import com.yahoo.component.ComponentId +import com.yahoo.container.di.ConfigKeyT +import com.yahoo.container.di.componentgraph.Provider +import com.google.inject.Key +import Node._ + + + +/** + * @author tonytv + * @author gjoranv + */ +abstract class Node(val componentId: ComponentId) { + + def instanceKey: Key[AnyRef] + + var instance : Option[AnyRef] = None + + var componentsToInject = List[Node]() + + /** + * The components actually used by this node. + * Consist of a subset of the injected nodes + subset of the global nodes. + */ + def usedComponents: List[Node] + + protected def newInstance() : AnyRef + + def newOrCachedInstance() : AnyRef = { + component( + instance.getOrElse { + instance = Some(newInstance()) + instance.get + }) + } + + private def component(instance: AnyRef) = instance match { + case provider: Provider[_] => provider.get().asInstanceOf[AnyRef] + case other => other + } + + def configKeys: Set[ConfigKeyT] + + def inject(component: Node) { + componentsToInject ::= component + } + + def injectAll(componentNodes: Traversable[ComponentNode]) { + componentNodes.foreach(inject(_)) + } + + def instanceType: Class[_ <: AnyRef] + def componentType: Class[_ <: AnyRef] + + override def equals(other: Any) = { + other match { + case that: Node => + getClass == that.getClass && + componentId == that.componentId && + instanceType == that.instanceType && + equalEdges(usedComponents, that.usedComponents) + case _ => false + } + } + + def label: String + + def idAndType = { + val className = instanceType.getName + + if (className == componentId.getName) s"'$componentId'" + else s"'$componentId of type '$className'" + } + +} + +object Node { + + def equalEdges(edges1: List[AnyRef], edges2: List[AnyRef]): Boolean = { + def compare(objects: (AnyRef, AnyRef)): Boolean = { + objects match { + case (edge1: Node, edge2: Node) => equalEdge(edge1, edge2) + case (o1, o2) => o1 == o2 + } + } + + def equalEdge(e1: Node, e2: Node) = e1.componentId == e2.componentId + + (edges1 zip edges2).forall(compare) + } + + /** + * @param identityObject The identifying object that makes the Node unique + */ + private[componentgraph] + def syntheticComponentId(className: String, identityObject: AnyRef, namespace: ComponentId) = { + val name = className + "_" + System.identityHashCode(identityObject) + ComponentId.fromString(name).nestInNamespace(namespace) + } + + + def packageName(componentClass: Class[_]) = { + def nullIfNotFound(index : Int) = if (index == -1) 0 else index + + val fullClassName = componentClass.getName + fullClassName.substring(0, nullIfNotFound(fullClassName.lastIndexOf("."))) + } +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/osgi/OsgiUtil.scala b/container-di/src/main/scala/com/yahoo/container/di/osgi/OsgiUtil.scala new file mode 100644 index 00000000000..dee95b84fe3 --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/osgi/OsgiUtil.scala @@ -0,0 +1,135 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container.di.osgi + +import java.nio.file.{Files, Path, Paths} +import java.util.function.Predicate +import java.util.jar.{JarEntry, JarFile} +import java.util.logging.{Level, Logger} +import java.util.stream.Collectors + +import com.yahoo.component.ComponentSpecification +import com.yahoo.container.di.Osgi.RelativePath +import com.yahoo.vespa.scalalib.arm.Using.using +import com.yahoo.vespa.scalalib.osgi.maven.ProjectBundleClassPaths +import com.yahoo.vespa.scalalib.osgi.maven.ProjectBundleClassPaths.BundleClasspathMapping +import org.osgi.framework.Bundle +import org.osgi.framework.wiring.BundleWiring + +import com.google.common.io.Files.fileTreeTraverser + +import scala.collection.convert.decorateAsScala._ + +import com.yahoo.vespa.scalalib.java.function.FunctionConverters._ + +/** + * Tested by com.yahoo.application.container.jersey.JerseyTest + * @author tonytv + */ +object OsgiUtil { + private val log = Logger.getLogger(getClass.getName) + private val classFileTypeSuffix = ".class" + + def getClassEntriesInBundleClassPath(bundle: Bundle, packagesToScan: Set[String]) = { + val bundleWiring = bundle.adapt(classOf[BundleWiring]) + + def listClasses(path: String, recurse: Boolean): Iterable[RelativePath] = { + val options = + if (recurse) BundleWiring.LISTRESOURCES_LOCAL | BundleWiring.LISTRESOURCES_RECURSE + else BundleWiring.LISTRESOURCES_LOCAL + + bundleWiring.listResources(path, "*" + classFileTypeSuffix, options).asScala + } + + if (packagesToScan.isEmpty) listClasses("/", recurse = true) + else packagesToScan flatMap { packageName => listClasses(packageToPath(packageName), recurse = false) } + } + + def getClassEntriesForBundleUsingProjectClassPathMappings(classLoader: ClassLoader, + bundleSpec: ComponentSpecification, + packagesToScan: Set[String]) = { + classEntriesFrom( + bundleClassPathMapping(bundleSpec, classLoader).classPathElements, + packagesToScan) + } + + private def bundleClassPathMapping(bundleSpec: ComponentSpecification, + classLoader: ClassLoader): BundleClasspathMapping = { + + val projectBundleClassPaths = loadProjectBundleClassPaths(classLoader) + + if (projectBundleClassPaths.mainBundle.bundleSymbolicName == bundleSpec.getName) { + projectBundleClassPaths.mainBundle + } else { + log.log(Level.WARNING, s"Dependencies of the bundle $bundleSpec will not be scanned. Please file a feature request if you need this" ) + matchingBundleClassPathMapping(bundleSpec, projectBundleClassPaths.providedDependencies) + } + } + + def matchingBundleClassPathMapping(bundleSpec: ComponentSpecification, + providedBundlesClassPathMappings: List[BundleClasspathMapping]): BundleClasspathMapping = { + providedBundlesClassPathMappings. + find(_.bundleSymbolicName == bundleSpec.getName). + getOrElse(throw new RuntimeException("No such bundle: " + bundleSpec)) + } + + private def loadProjectBundleClassPaths(classLoader: ClassLoader): ProjectBundleClassPaths = { + val classPathMappingsFileLocation = classLoader.getResource(ProjectBundleClassPaths.classPathMappingsFileName) + if (classPathMappingsFileLocation == null) + throw new RuntimeException(s"Couldn't find ${ProjectBundleClassPaths.classPathMappingsFileName} in the class path.") + + ProjectBundleClassPaths.load(Paths.get(classPathMappingsFileLocation.toURI)) + } + + private def classEntriesFrom(classPathEntries: List[String], packagesToScan: Set[String]): Iterable[RelativePath] = { + val packagePathsToScan = packagesToScan map packageToPath + + classPathEntries.flatMap { entry => + val path = Paths.get(entry) + if (Files.isDirectory(path)) classEntriesInPath(path, packagePathsToScan) + else if (Files.isRegularFile(path) && path.getFileName.toString.endsWith(".jar")) classEntriesInJar(path, packagePathsToScan) + else throw new RuntimeException("Unsupported path " + path + " in the class path") + } + } + + private def classEntriesInPath(rootPath: Path, packagePathsToScan: Traversable[String]): Traversable[RelativePath] = { + def relativePathToClass(pathToClass: Path): RelativePath = { + val relativePath = rootPath.relativize(pathToClass) + relativePath.toString + } + + val fileIterator = + if (packagePathsToScan.isEmpty) fileTreeTraverser().preOrderTraversal(rootPath.toFile).asScala + else packagePathsToScan.view flatMap { packagePath => fileTreeTraverser().children(rootPath.resolve(packagePath).toFile).asScala } + + for { + file <- fileIterator + if file.isFile + if file.getName.endsWith(classFileTypeSuffix) + } yield relativePathToClass(file.toPath) + } + + + private def classEntriesInJar(jarPath: Path, packagePathsToScan: Set[String]): Traversable[RelativePath] = { + def packagePath(name: String) = { + name.lastIndexOf('/') match { + case -1 => name + case n => name.substring(0, n) + } + } + + val acceptedPackage: Predicate[String] = + if (packagePathsToScan.isEmpty) (name: String) => true + else (name: String) => packagePathsToScan(packagePath(name)) + + using(new JarFile(jarPath.toFile)) { jarFile => + jarFile.stream(). + map[String] { entry: JarEntry => entry.getName}. + filter { name: String => name.endsWith(classFileTypeSuffix)}. + filter(acceptedPackage). + collect(Collectors.toList()). + asScala + } + } + + def packageToPath(packageName: String) = packageName.replaceAllLiterally(".", "/") +} diff --git a/container-di/src/main/scala/com/yahoo/container/di/package.scala b/container-di/src/main/scala/com/yahoo/container/di/package.scala new file mode 100644 index 00000000000..f1e2a35797e --- /dev/null +++ b/container-di/src/main/scala/com/yahoo/container/di/package.scala @@ -0,0 +1,41 @@ +// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. +package com.yahoo.container + +import com.yahoo.config.ConfigInstance +import com.yahoo.vespa.config.ConfigKey +import java.lang.reflect.Type +import com.google.inject.Key + +/** + * + * @author gjoranv + * @author tonytv + */ +package object di { + type ConfigKeyT = ConfigKey[_ <: ConfigInstance] + type GuiceInjector = com.google.inject.Injector + type JavaAnnotation = java.lang.annotation.Annotation + + def createKey(instanceType: Type, annotation: JavaAnnotation) = { + {if (annotation == null) + Key.get(instanceType) + else + Key.get(instanceType, annotation) + }.asInstanceOf[Key[AnyRef]] + } + + implicit def makeClassCovariant[SUB, SUPER >: SUB](clazz: Class[SUB]) : Class[SUPER] = { + clazz.asInstanceOf[Class[SUPER]] + } + + def removeStackTrace(exception: RuntimeException): RuntimeException = { + if (preserveStackTrace) exception + else { + exception.setStackTrace(Array()) + exception + } + } + + //For debug purposes only + val preserveStackTrace: Boolean = Option(System.getProperty("jdisc.container.preserveStackTrace")).filterNot(_.isEmpty).isDefined +}
\ No newline at end of file |