summaryrefslogtreecommitdiffstats
path: root/component
diff options
context:
space:
mode:
authorJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
committerJon Bratseth <bratseth@yahoo-inc.com>2016-06-15 23:09:44 +0200
commit72231250ed81e10d66bfe70701e64fa5fe50f712 (patch)
tree2728bba1131a6f6e5bdf95afec7d7ff9358dac50 /component
Publish
Diffstat (limited to 'component')
-rw-r--r--component/.gitignore2
-rw-r--r--component/OWNERS1
-rwxr-xr-xcomponent/pom.xml99
-rw-r--r--component/src/main/java/com/yahoo/component/AbstractComponent.java144
-rw-r--r--component/src/main/java/com/yahoo/component/Component.java20
-rw-r--r--component/src/main/java/com/yahoo/component/ComponentId.java230
-rw-r--r--component/src/main/java/com/yahoo/component/ComponentSpecification.java159
-rw-r--r--component/src/main/java/com/yahoo/component/Spec.java95
-rw-r--r--component/src/main/java/com/yahoo/component/SpecSplitter.java44
-rw-r--r--component/src/main/java/com/yahoo/component/Version.java352
-rw-r--r--component/src/main/java/com/yahoo/component/VersionSpecification.java366
-rw-r--r--component/src/main/java/com/yahoo/component/package-info.java7
-rw-r--r--component/src/main/java/com/yahoo/component/provider/ComponentClass.java253
-rw-r--r--component/src/main/java/com/yahoo/component/provider/ComponentRegistry.java190
-rw-r--r--component/src/main/java/com/yahoo/component/provider/Freezable.java29
-rw-r--r--component/src/main/java/com/yahoo/component/provider/FreezableClass.java44
-rw-r--r--component/src/main/java/com/yahoo/component/provider/FreezableComponent.java47
-rw-r--r--component/src/main/java/com/yahoo/component/provider/FreezableSimpleComponent.java47
-rw-r--r--component/src/main/java/com/yahoo/component/provider/ListenableFreezable.java15
-rw-r--r--component/src/main/java/com/yahoo/component/provider/ListenableFreezableClass.java43
-rw-r--r--component/src/main/java/com/yahoo/component/provider/package-info.java7
-rw-r--r--component/src/main/java/com/yahoo/container/util/Util.java27
-rw-r--r--component/src/main/java/com/yahoo/container/util/package-info.java5
-rw-r--r--component/src/test/java/com/yahoo/component/VersionSpecificationTestCase.java142
-rw-r--r--component/src/test/java/com/yahoo/component/VersionTestCase.java81
25 files changed, 2449 insertions, 0 deletions
diff --git a/component/.gitignore b/component/.gitignore
new file mode 100644
index 00000000000..3cc25b51fc4
--- /dev/null
+++ b/component/.gitignore
@@ -0,0 +1,2 @@
+/pom.xml.build
+/target
diff --git a/component/OWNERS b/component/OWNERS
new file mode 100644
index 00000000000..3b2ba1ede81
--- /dev/null
+++ b/component/OWNERS
@@ -0,0 +1 @@
+gjoranv
diff --git a/component/pom.xml b/component/pom.xml
new file mode 100755
index 00000000000..038e154f20f
--- /dev/null
+++ b/component/pom.xml
@@ -0,0 +1,99 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+ xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
+ http://maven.apache.org/xsd/maven-4.0.0.xsd">
+ <modelVersion>4.0.0</modelVersion>
+ <parent>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>parent</artifactId>
+ <version>6-SNAPSHOT</version>
+ <relativePath>../parent/pom.xml</relativePath>
+ </parent>
+ <artifactId>component</artifactId>
+ <packaging>container-plugin</packaging>
+ <version>6-SNAPSHOT</version>
+ <dependencies>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-bundle</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>vespajlib</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.google.guava</groupId>
+ <artifactId>guava</artifactId>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-lib</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</scope>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>bundle-plugin</artifactId>
+ <extensions>true</extensions>
+ </plugin>
+ <plugin>
+ <groupId>org.codehaus.mojo</groupId>
+ <artifactId>exec-maven-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>generate-vtag</id>
+ <phase>compile</phase>
+ <goals>
+ <goal>java</goal>
+ </goals>
+ <configuration>
+ <mainClass>com.yahoo.vespa.VersionTagger</mainClass>
+ <arguments>
+ <argument>${project.basedir}/../dist/vtag.map</argument>
+ <argument>com.yahoo.component</argument>
+ <argument>${project.build.directory}/generated-sources/vtag</argument>
+ <argument>vtag</argument>
+ </arguments>
+ <sourceRoot>${project.build.directory}/generated-sources/vtag</sourceRoot>
+ <classpathScope>compile</classpathScope>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ <plugin>
+ <groupId>org.apache.maven.plugins</groupId>
+ <artifactId>maven-compiler-plugin</artifactId>
+ <executions>
+ <execution>
+ <id>compile-vtag</id>
+ <phase>compile</phase>
+ <goals>
+ <goal>compile</goal>
+ </goals>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/component/src/main/java/com/yahoo/component/AbstractComponent.java b/component/src/main/java/com/yahoo/component/AbstractComponent.java
new file mode 100644
index 00000000000..2fe11425d20
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/AbstractComponent.java
@@ -0,0 +1,144 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+import java.lang.reflect.Method;
+
+/**
+ * Superclass of components. You must use this instead of subclassing Component if your component
+ * needs to be called on destruction.
+ *
+ * @author bratseth
+ */
+public class AbstractComponent implements Component {
+
+ // All accesses to id MUST go through getId.
+ private ComponentId id;
+
+ // We must store the class name, as this.getClass() will yield an exception when a bundled component's
+ // bundle has been uninstalled.
+ private String className = getClass().getName();
+ protected final boolean isDeconstructable;
+
+ /**
+ * Creates a new component with an id.
+ *
+ * @throws NullPointerException if the given id is null
+ */
+ protected AbstractComponent(ComponentId id) {
+ initId(id);
+ isDeconstructable = setIsDeconstructable();
+ }
+
+ /** Creates a new component which is invalid until {@link #initId} is called on it. */
+ protected AbstractComponent() {
+ isDeconstructable = setIsDeconstructable();
+ }
+
+ /** Initializes this. Always called from a constructor or the framework. Do not call. */
+ public final void initId(ComponentId id) {
+ if (this.id != null && !this.id.equals(id))
+ throw new RuntimeException("Can't change component id: " + this.id + " -> " + id);
+
+ if (id==null) throw new NullPointerException("A component cannot be created with a null id");
+ this.id=id;
+ }
+
+ /** Do NOT call at construction time. Returns the id of this component. */
+ public final ComponentId getId() {
+ if (id == null) {
+ setTestId();
+ }
+ return id;
+ }
+
+ //This should only happen in tests, so thread safety should not be an issue.
+ private void setTestId() {
+ id = ComponentId.createAnonymousComponentId("test_" + getClass().getName());
+ }
+
+ /**
+ * DO NOT CALL, for internal use only,
+ */
+ public final boolean hasInitializedId() {
+ return id != null;
+ }
+
+ /**
+ * DO NOT CALL, for internal use only,
+ */
+ public final String getIdString() {
+ if (hasInitializedId())
+ return getId().toString();
+
+ return "(anonymous)";
+ }
+
+ public String getClassName() {
+ return className;
+ }
+
+ @Override
+ public String toString() {
+ return "'" + getIdString() + "' of class '" + className + "'";
+ }
+
+ /**
+ * Clones this by returning a new instance <i>which does not have an id</i>.
+ * An id can subsequently be assigned by calling {@link #initId}.
+ * Note that even though this implements clone, the component subclass may
+ * not in fact be clonable.
+ *
+ * @throws RuntimeException if the component is not clonable
+ */
+ @Override
+ public AbstractComponent clone() {
+ try {
+ AbstractComponent clone=(AbstractComponent)super.clone();
+ clone.id=null;
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException("'" + this + "' is not clonable",e);
+ }
+
+ }
+
+ /** Order by id order. It is permissible to change the order definition in subclasses */
+ @Override
+ public int compareTo(Component other) {
+ return id.compareTo(other.getId());
+ }
+
+ /**
+ * Implement this to perform any cleanup of structures or resources allocated in the constructor,
+ * before this component is removed.
+ * <p>
+ * All other calls to this component is completed before this method is called.
+ * It will only be called once. It should block while doing cleanup tasks and return when
+ * this class is ready for garbage collection.
+ * <p>
+ * This default implementation does nothing.
+ */
+ public void deconstruct() { }
+
+ /**
+ * @return true if this component has a non-default implementation of the {@link #deconstruct} method.
+ */
+ public final boolean isDeconstructable() {
+ return isDeconstructable;
+ }
+
+ protected boolean setIsDeconstructable() {
+ try {
+ Method deconstruct = getClass().getMethod("deconstruct");
+ @SuppressWarnings("rawtypes")
+ Class declaringClass = deconstruct.getDeclaringClass();
+ if (declaringClass != AbstractComponent.class) {
+ return true;
+ }
+ } catch (NoSuchMethodException e) {
+ com.yahoo.protect.Process.logAndDie("Component " + this + " does not have method deconstruct() - impossible!");
+ }
+ return false;
+ }
+}
diff --git a/component/src/main/java/com/yahoo/component/Component.java b/component/src/main/java/com/yahoo/component/Component.java
new file mode 100644
index 00000000000..e19993d657d
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/Component.java
@@ -0,0 +1,20 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+
+/**
+ * A named, versioned, identifiable component.
+ * <p>
+ * Components can by default be ordered by their id order. Their identity is defined by the id.
+ * Prefer extending AbstractComponent instead of implementing this interface directly.
+ *
+ * @author bratseth
+ */
+public interface Component extends Comparable<Component> {
+
+ /** Initializes this. Always called from a constructor or the framework. Do not call. */
+ public void initId(ComponentId id);
+
+ /** Returns the id of this component */
+ public ComponentId getId();
+}
diff --git a/component/src/main/java/com/yahoo/component/ComponentId.java b/component/src/main/java/com/yahoo/component/ComponentId.java
new file mode 100644
index 00000000000..50417c15b5e
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/ComponentId.java
@@ -0,0 +1,230 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+import java.util.concurrent.atomic.AtomicInteger;
+
+/**
+ * The id of a component.
+ * Consists of a name, optionally a version, and optionally a namespace.
+ * This is an immutable value object.
+ *
+ * @author bratseth
+ * @author tonytv
+ */
+public final class ComponentId implements Comparable<ComponentId> {
+
+ private final class VersionHandler implements Spec.VersionHandler<Version> {
+
+ @Override
+ public Version emptyVersion() {
+ return Version.emptyVersion;
+ }
+
+ @Override
+ public int compare(Version v1, Version v2) {
+ return v1.compareTo(v2);
+ }
+ }
+
+ private final Spec<Version> spec;
+ private final boolean anonymous;
+ private final static class Counter {
+ private int count = 0;
+ public int getAndIncrement() { return count++; }
+ }
+ private static ThreadLocal<Counter> gid = new ThreadLocal<Counter>() {
+ @Override protected Counter initialValue() {
+ return new Counter();
+ }
+ };
+ private static AtomicInteger uniqueTid = new AtomicInteger(0);
+ private static ThreadLocal<String> tid = new ThreadLocal<String>() {
+ @Override protected String initialValue() {
+ return new String("_"+uniqueTid.getAndIncrement()+"_");
+ }
+ };
+
+ /** Precomputed string value */
+ private final String stringValue;
+
+ private ComponentId(String name, Version version, ComponentId namespace, boolean anonymous) {
+ if (anonymous) {
+ name = createAnonymousName(name);
+ }
+ spec = new Spec<>(new VersionHandler(), name, version, namespace);
+ this.anonymous = anonymous;
+
+ stringValue = spec.createStringValue();
+ }
+
+ private String createAnonymousName(String name) {
+ return new StringBuilder(name).append(tid.get()).append(gid.get().getAndIncrement()).toString();
+ }
+
+ public ComponentId(String name, Version version, ComponentId namespace) {
+ this(name, version, namespace, false);
+ }
+
+ /** Creates a component id from a name and version. The version may be null */
+ public ComponentId(String name, Version version) {
+ this(name, version, null);
+ }
+
+ /**
+ * Creates a component id from the id string form: name(:version)?(@namespace)?,
+ * where version has the form 1(.2(.3(.identifier)?)?)?
+ * and namespace is a component id
+ */
+ public ComponentId(String id) {
+ this(new SpecSplitter(id));
+ }
+
+ private ComponentId(SpecSplitter splitter) {
+ this(splitter.name, Version.fromString(splitter.version), splitter.namespace);
+ }
+
+ public ComponentId nestInNamespace(ComponentId namespace) {
+ if (namespace == null) {
+ return this;
+ } else {
+ ComponentId newNamespace = getNamespace() == null ?
+ namespace :
+ getNamespace().nestInNamespace(namespace);
+ return new ComponentId(getName(), getVersion(), newNamespace);
+ }
+ }
+
+ /** Returns the name of this. This is never null */
+ public String getName() { return spec.name; }
+
+ /** Returns the version of this id, or emptyVersion if no version is specified */
+ public Version getVersion() { return spec.version; }
+
+ /** The namespace is null if this is a top level component id **/
+ public ComponentId getNamespace() { return spec.namespace; }
+
+ /**
+ * Returns the string value of this id.
+ * If no version is given, this is simply the name.
+ * If a version is given, it is name:version.
+ * Trailing ".0"'s are stripped from the version part.
+ */
+ public String stringValue() { return stringValue; }
+
+ public @Override String toString() {
+ return spec.toString();
+ }
+
+ public boolean equals(Object o) {
+ if (o==this) return true;
+ if ( ! (o instanceof ComponentId)) return false;
+
+ ComponentId c = (ComponentId) o;
+ if (isAnonymous() || c.isAnonymous())
+ return false;
+
+ return c.stringValue().equals(stringValue);
+ }
+
+ public @Override int hashCode() {
+ return stringValue.hashCode();
+ }
+
+ public ComponentSpecification toSpecification() {
+ if (isAnonymous())
+ throw new RuntimeException("Can't generate a specification for an anonymous component id.");
+ return new ComponentSpecification(getName(),
+ getVersion().toSpecification(), getNamespace());
+ }
+
+ public int compareTo(ComponentId other) {
+ //anonymous must never be equal to non-anonymous
+ if (isAnonymous() ^ other.isAnonymous()) {
+ return isAnonymous() ? -1 : 1;
+ }
+
+ return spec.compareTo(other.spec);
+ }
+
+ /**
+ * Creates a componentId that is unique for this run-time instance
+ */
+ // TODO: Check if we really need this. -JB
+ public static ComponentId createAnonymousComponentId(String baseName) {
+ return new ComponentId(baseName, null, null, true);
+ }
+
+ public boolean isAnonymous() {
+ return anonymous;
+ }
+
+ /** Returns a copy of this id with namespace set to null **/
+ public ComponentId withoutNamespace() {
+ return new ComponentId(getName(), getVersion(), null);
+ }
+
+ /**
+ * Creates a component id from the id string form: name(:version)?(@namespace)?,
+ * where version has the form 1(.2(.3(.identifier)?)?)?
+ * and namespace is a component id.
+ *
+ * @return new ComponentId(componentId), or null if the input string is null
+ */
+ public static ComponentId fromString(String componentId) {
+ try {
+ return (componentId != null) ? new ComponentId(componentId) : null;
+ } catch(Exception e) {
+ throw new IllegalArgumentException("Illegal component id: '" + componentId + "'", e);
+ }
+ }
+
+ /**
+ * Returns this id's stringValue (i.e the id without trailing ".0"'s) translated to a file name using the
+ * <i>standard translation:</i>
+ * <pre><code>
+ * : → -
+ * / → _
+ * </code></pre>
+ */
+ public String toFileName() {
+ return stringValue.replace(":","-").replace("/",".");
+ }
+
+ /**
+ * Creates an id from a file <b>first</b> name string encoded in the standard translation (see {@link #toFileName}).
+ * <b>Note</b> that any file last name, like e.g ".xml" must be stripped off before handoff to this method.
+ */
+ public static ComponentId fromFileName(final String fileName) {
+ // Initial assumptions
+ String id=fileName;
+ Version version =null;
+ ComponentId namespace=null;
+
+ // Split out namespace, if any
+ int at=id.indexOf("@");
+ if (at>0) {
+ String newId=id.substring(0,at);
+ namespace=ComponentId.fromString(id.substring(at+1));
+ id=newId;
+ }
+
+ // Split out version, if any
+ int dash=id.lastIndexOf("-");
+ if (dash>0) {
+ String newId=id.substring(0,dash);
+ try {
+ version=new Version(id.substring(dash+1));
+ id=newId;
+ }
+ catch (IllegalArgumentException e) {
+ // don't interpret the text following the dash as a version
+ }
+ }
+
+ // Convert dots in id portion back - this is the part which prevents us from
+ id=id.replace(".","/");
+
+ return new ComponentId(id,version,namespace);
+ }
+
+}
diff --git a/component/src/main/java/com/yahoo/component/ComponentSpecification.java b/component/src/main/java/com/yahoo/component/ComponentSpecification.java
new file mode 100644
index 00000000000..c2c31c97f8b
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/ComponentSpecification.java
@@ -0,0 +1,159 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+/**
+ * The specification of a wanted component.
+ * Consists of a name, optionally a version specification, and optionally a namespace.
+ * This is an immutable value object
+ *
+ * @author Arne Bergene Fossaa
+ * @author tonytv
+ */
+public final class ComponentSpecification {
+ private final class VersionHandler implements Spec.VersionHandler<VersionSpecification> {
+ @Override
+ public VersionSpecification emptyVersion() {
+ return VersionSpecification.emptyVersionSpecification;
+ }
+
+ @Override
+ public int compare(VersionSpecification v1, VersionSpecification v2) {
+ return v1.compareTo(v2);
+ }
+ }
+
+ private final Spec<VersionSpecification> spec;
+ /** Precomputed string value */
+ private final String stringValue;
+
+ /**
+ * Creates a component id from the id string form: name(:version?),
+ * where version has the form 1(.2(.3(.identifier)?)?)?
+ *
+ * @return null iff componentSpecification == null
+ */
+ public static ComponentSpecification fromString(String componentSpecification) {
+ try {
+ return (componentSpecification != null) ?
+ new ComponentSpecification(componentSpecification) :
+ null;
+ } catch (Exception e) {
+ throw new IllegalArgumentException("Illegal component specification: '" + componentSpecification + "'", e);
+ }
+ }
+
+ public ComponentSpecification(String name, VersionSpecification versionSpecification, ComponentId namespace) {
+ spec = new Spec<>(new VersionHandler(),
+ name, versionSpecification, namespace);
+ stringValue = spec.createStringValue();
+ }
+
+ /** Creates a component id from a name and version. The version may be null */
+ public ComponentSpecification(String name, VersionSpecification versionSpec) {
+ this(name, versionSpec, null);
+ }
+
+ /**
+ * Creates a component id from the id string form: name(:version?),
+ * where version has the form 1(.2(.3(.identifier)?)?)?
+ */
+ public ComponentSpecification(String id) {
+ this(new SpecSplitter((id)));
+ }
+
+ private ComponentSpecification(SpecSplitter splitter) {
+ this(splitter.name, VersionSpecification.fromString(splitter.version), splitter.namespace);
+ }
+
+ public ComponentSpecification nestInNamespace(ComponentId namespace) {
+ ComponentId newNameSpace =
+ (getNamespace() == null) ?
+ namespace :
+ getNamespace().nestInNamespace(namespace);
+ return new ComponentSpecification(getName(), getVersionSpecification(), newNameSpace);
+ }
+
+ /** The namespace is null if this is to match a top level component id **/
+ public ComponentId getNamespace() { return spec.namespace; }
+
+ /** Returns the name of this. This is never null */
+ public String getName() { return spec.name; }
+
+ /** Returns the version of this id, or null if no version is specified */
+ public VersionSpecification getVersionSpecification() {
+ return spec.version;
+ }
+
+ /**
+ * Returns the string value of this id.
+ * If no version is given, this is simply the name.
+ * If a version is given, it is name:version.
+ * Trailing ".0"'s are stripped from the version part.
+ */
+ public String stringValue() { return stringValue; }
+
+ public @Override String toString() {
+ return toId().toString();
+ }
+
+ public boolean equals(Object o) {
+ if (o==this) return true;
+ if ( ! (o instanceof ComponentSpecification)) return false;
+ ComponentSpecification c = (ComponentSpecification) o;
+ return c.stringValue.equals(this.stringValue());
+ }
+
+ public @Override int hashCode() {
+ return stringValue.hashCode();
+ }
+
+ /** Converts the specification to an id */
+ public ComponentId toId() {
+ Version version =
+ (getVersionSpecification() == VersionSpecification.emptyVersionSpecification) ?
+ Version.emptyVersion :
+ getVersionSpecification().lowestMatchingVersion();
+
+ return new ComponentId(getName(), version, getNamespace());
+ }
+
+ /**
+ * Checks if a componentId matches a given spec
+ */
+ public boolean matches(ComponentId id) {
+ boolean versionMatch = getVersionSpecification().matches(id.getVersion());
+ return getName().equals(id.getName())
+ && versionMatch
+ && namespaceMatch(id.getNamespace());
+ }
+
+ public ComponentSpecification intersect(ComponentSpecification other) {
+ if (!getName().equals(other.getName())) {
+ throw new RuntimeException("The names of the component specifications does not match("
+ + getName() + "!=" + other.getName() + ").");
+ }
+ if (!namespaceMatch(other.getNamespace())) {
+ throw new RuntimeException("The namespaces of the component specifications does not match("
+ + this + ", " + other +")");
+ }
+
+ return new ComponentSpecification(getName(),
+ getVersionSpecification().intersect(other.getVersionSpecification()),
+ getNamespace());
+ }
+
+ /** Returns a copy of this spec with namespace set to null **/
+ public ComponentSpecification withoutNamespace() {
+ return new ComponentSpecification(getName(), getVersionSpecification(), null);
+ }
+
+ private boolean namespaceMatch(ComponentId otherNamespace) {
+ if (getNamespace() == otherNamespace) {
+ return true;
+ } else if (getNamespace() == null || otherNamespace == null){
+ return false;
+ } else {
+ return getNamespace().equals(otherNamespace);
+ }
+ }
+}
diff --git a/component/src/main/java/com/yahoo/component/Spec.java b/component/src/main/java/com/yahoo/component/Spec.java
new file mode 100644
index 00000000000..7911b53d23c
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/Spec.java
@@ -0,0 +1,95 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+import static com.yahoo.container.util.Util.firstNonNull;
+
+/**
+ * Code common to ComponentId and ComponentSpecification
+ *
+ * @author tonytv
+ */
+final class Spec<VERSION> {
+ private final VersionHandler<VERSION> versionHandler;
+
+ interface VersionHandler<VERSION> {
+ VERSION emptyVersion();
+ int compare(VERSION v1, VERSION v2);
+ }
+
+ final String name;
+ final VERSION version;
+ final ComponentId namespace;
+
+ @SuppressWarnings("unchecked")
+ Spec(VersionHandler<VERSION> versionHandler,
+ String name, VERSION version, ComponentId namespace) {
+ assert (name != null);
+ validateName(name);
+
+ this.versionHandler = versionHandler;
+ this.name = name;
+ this.version = firstNonNull(version, versionHandler.emptyVersion());
+ this.namespace = namespace;
+ }
+
+ String createStringValue() {
+ if (isNonEmpty(version) || (namespace != null)) {
+ StringBuilder builder = new StringBuilder(name);
+ if (isNonEmpty(version))
+ builder.append(':').append(version);
+ if (namespace != null)
+ builder.append('@').append(namespace.stringValue());
+ return builder.toString();
+ } else {
+ return name;
+ }
+ }
+
+ private void validateName(String name) {
+ if ( name == null || name.isEmpty() || name.contains("@") || name.contains(":")) {
+ throw new IllegalArgumentException("The name '" + name + "' is expected to be non-empty and not contain {:, @}");
+ }
+ }
+
+ public @Override String toString() {
+ if (isNonEmpty(version) || (namespace != null)) {
+ StringBuilder builder = new StringBuilder(name);
+ if (isNonEmpty(version)) {
+ builder.append(':').append(version);
+ }
+ if (namespace != null) {
+ builder.append(" in ").append(namespace.toString());
+ }
+ return builder.toString();
+ } else {
+ return name;
+ }
+ }
+
+ private boolean isNonEmpty(VERSION version) {
+ return ! version.equals(versionHandler.emptyVersion());
+ }
+
+ public int compareTo(Spec<VERSION> other) {
+ int result = name.compareTo(other.name);
+ if (result != 0)
+ return result;
+
+ result = versionHandler.compare(version, other.version);
+ if (result != 0)
+ return result;
+
+ return compare(namespace, other.namespace);
+ }
+
+ private int compare(ComponentId n1, ComponentId n2) {
+ if (n1 == null && n2 == null)
+ return 0;
+ if (n1 == null)
+ return -1;
+ if (n2 == null)
+ return 1;
+
+ return n1.compareTo(n2);
+ }
+}
diff --git a/component/src/main/java/com/yahoo/component/SpecSplitter.java b/component/src/main/java/com/yahoo/component/SpecSplitter.java
new file mode 100644
index 00000000000..0517bbaceef
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/SpecSplitter.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Splits and component id or component specification string
+ * into their constituent parts.
+ * @author tonytv
+ */
+class SpecSplitter {
+ final String name;
+ final String version;
+ final ComponentId namespace;
+
+ SpecSplitter(String spec) {
+ List<String> idAndNamespace = splitFirst(spec, '@');
+ List<String> nameAndVersion = splitFirst(idAndNamespace.get(0), ':');
+
+ name = nameAndVersion.get(0);
+ version = second(nameAndVersion);
+ namespace = ComponentId.fromString(second(idAndNamespace));
+ }
+
+ private String second(List<String> components) {
+ return components.size() == 2?
+ components.get(1) :
+ null;
+ }
+
+ private static List<String> splitFirst(String string, char c) {
+ int index = string.indexOf(c);
+ if (index != -1) {
+ if (index == string.length() - 1) {
+ throw new RuntimeException("Expected characters after '" + c + "'");
+ }
+ return Arrays.asList(string.substring(0, index),
+ string.substring(index + 1));
+ } else {
+ return Arrays.asList(string, null);
+ }
+ }
+}
diff --git a/component/src/main/java/com/yahoo/component/Version.java b/component/src/main/java/com/yahoo/component/Version.java
new file mode 100644
index 00000000000..9f874716bc0
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/Version.java
@@ -0,0 +1,352 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+import com.yahoo.text.Utf8;
+import com.yahoo.text.Utf8Array;
+import com.yahoo.text.Utf8String;
+
+import java.nio.ByteBuffer;
+
+/**
+ * A component version
+ * <p>
+ * Version identifiers have four components.
+ * <ol>
+ * <li>Major version. A non-negative integer.</li>
+ * <li>Minor version. A non-negative integer.</li>
+ * <li>Micro version. A non-negative integer.</li>
+ * <li>Qualifier. An ascii text string. See <code>Version(String)</code> for the
+ * format of the qualifier string.</li>
+ * </ol>
+ *
+ * <p>
+ * Unspecified version component is equivalent to 0 (or the empty string for qualifier).
+ *
+ * <p>
+ * <code>Version</code> objects are immutable.
+ *
+ * @author bratseth
+ */
+public final class Version implements Comparable<Version> {
+
+ private int major = 0;
+ private int minor = 0;
+ private int micro = 0;
+ private String qualifier = "";
+ private String stringValue;
+
+ /** The empty version */
+ public static final Version emptyVersion = new Version();
+
+ /** Creates an empty version */
+ public Version() {
+ this(0, 0, 0, null);
+ }
+
+ /**
+ * Creates a version identifier from the specified numerical components.
+ *
+ * @param major major component of the version identifier
+ * @throws IllegalArgumentException If the numerical components are
+ * negative.
+ */
+ public Version(int major) {
+ this(major, 0, 0, null);
+ }
+
+ /**
+ * Creates a version identifier from the specified numerical components.
+ *
+ * @param major major component of the version identifier
+ * @param minor minor component of the version identifier
+ * @throws IllegalArgumentException If the numerical components are
+ * negative.
+ */
+ public Version(int major, int minor) {
+ this(major, minor, 0, null);
+ }
+
+ /**
+ * Creates a version identifier from the specified numerical components.
+ *
+ * @param major major component of the version identifier
+ * @param minor minor component of the version identifier
+ * @param micro micro component of the version identifier
+ * @throws IllegalArgumentException If the numerical components are
+ * negative.
+ */
+ public Version(int major, int minor, int micro) {
+ this(major, minor, micro, null);
+ }
+
+ /**
+ * Creates a version identifier from the specified components.
+ *
+ * @param major major component of the version identifier
+ * @param minor minor component of the version identifier
+ * @param micro micro component of the version identifier
+ * @param qualifier Qualifier component of the version identifier, or null if not specified
+ * @throws IllegalArgumentException if the numerical components are negative
+ * the qualifier string contains non-word/digit-characters, or
+ * an earlier component is not specified but a later one is
+ */
+ public Version(int major, int minor, int micro, String qualifier) {
+ this.major = major;
+ this.minor = minor;
+ this.micro = micro;
+ if (qualifier != null) this.qualifier = qualifier;
+ stringValue = toStringValue();
+ verify();
+ }
+
+ /**
+ * Creates a version identifier from the specified string.
+ *
+ * <p>
+ * Version strings follows this grammar (same as Osgi versions):
+ *
+ * <pre>
+ * version ::= major('.'minor('.'micro('.'qualifier)?)?)?
+ * major ::= digit+
+ * minor ::= digit+
+ * micro ::= digit+
+ * qualifier ::= (alpha|digit|'_'|'-')+
+ * digit ::= [0..9]
+ * alpha ::= [a..zA..Z]
+ * </pre>
+ *
+ * @param versionString String representation of the version identifier
+ * @throws IllegalArgumentException If <code>version</code> is improperly
+ * formatted.
+ */
+ public Version(String versionString) {
+ if (! "".equals(versionString)) {
+ String[] components=versionString.split("\\x2e"); // Split on dot
+
+ if (components.length > 0)
+ major = Integer.parseInt(components[0]);
+ if (components.length > 1)
+ minor = Integer.parseInt(components[1]);
+ if (components.length > 2)
+ micro = Integer.parseInt(components[2]);
+ if (components.length > 3)
+ qualifier = components[3];
+ if (components.length > 4)
+ throw new IllegalArgumentException("Too many components in '" + versionString + "'");
+ }
+ stringValue = toStringValue();
+ verify();
+ }
+
+ static private int readInt(ByteBuffer bb) {
+ int accum=0;
+ for (int i=bb.remaining(); i > 0; i--) {
+ byte b=bb.get();
+ if (b >= 0x30 && b <= 0x39) {
+ accum = accum * 10 + (b-0x30);
+ } else if (b == 0x2e) {
+ return accum;
+ } else {
+ throw new IllegalArgumentException("Failed decoding integer from utf8stream. Stream = " + bb.toString());
+ }
+ }
+ return accum;
+ }
+ /**
+ * Creates a version identifier from the specified string.
+ *
+ * <p>
+ * Version strings follows this grammar (same as Osgi versions):
+ *
+ * <pre>
+ * version ::= major('.'minor('.'micro('.'qualifier)?)?)?
+ * major ::= digit+
+ * minor ::= digit+
+ * micro ::= digit+
+ * qualifier ::= (alpha|digit|'_'|'-')+
+ * digit ::= [0..9]
+ * alpha ::= [a..zA..Z]
+ * </pre>
+ *
+ * @param versionString String representation of the version identifier
+ * @throws IllegalArgumentException If <code>version</code> is improperly
+ * formatted.
+ */
+ public Version(Utf8Array versionString) {
+ ByteBuffer bb = versionString.wrap();
+ if (bb.remaining() > 0) {
+ major = readInt(bb);
+ if (bb.remaining() > 0) {
+ minor = readInt(bb);
+ if (bb.remaining() > 0) {
+ micro = readInt(bb);
+ if (bb.remaining() > 0) {
+ qualifier = Utf8.toString(bb);
+ }
+ }
+ }
+ } else {
+ throw new IllegalArgumentException("Empty version specification");
+ }
+
+ stringValue = versionString.toString();
+ verify();
+ }
+
+ /** Returns new Version(versionString), or Version.emptyVersion if the input string is null or "" */
+ public static Version fromString(String versionString) {
+ if (versionString == null) {
+ return emptyVersion;
+ } else {
+ return new Version(versionString);
+ }
+ }
+
+ /**
+ * Must be called on construction after the component values are set
+ *
+ * @throws IllegalArgumentException If the numerical components are negative
+ * or the qualifier string is invalid.
+ */
+ private void verify() {
+ if (major < 0)
+ throw new IllegalArgumentException("Negative major in " + this);
+ if (minor < 0)
+ throw new IllegalArgumentException("Negative minor in " + this);
+ if (micro < 0)
+ throw new IllegalArgumentException("Negative micro in " + this);
+
+ for (int i = 0; i < qualifier.length(); i++) {
+ char c = qualifier.charAt(i);
+ if (!Character.isLetterOrDigit(c))
+ throw new IllegalArgumentException("Invalid qualifier in " + this +
+ ": Invalid character at position " + i + " in qualifier");
+ }
+ }
+
+ private String toStringValue() {
+ StringBuilder b = new StringBuilder();
+ if (! qualifier.equals("")) {
+ b.append(getMajor());
+ b.append(".");
+ b.append(getMinor());
+ b.append(".");
+ b.append(getMicro());
+ b.append(".");
+ b.append(qualifier);
+ } else if (getMicro() != 0) {
+ b.append(getMajor());
+ b.append(".");
+ b.append(getMinor());
+ b.append(".");
+ b.append(getMicro());
+ } else if (getMinor() != 0) {
+ b.append(getMajor());
+ b.append(".");
+ b.append(getMinor());
+ } else if (getMajor() != 0) {
+ b.append(getMajor());
+ }
+ return b.toString();
+ }
+
+ /** Returns the major component of this version, or 0 if not specified */
+ public int getMajor() { return major; }
+
+ /** Returns the minor component of this version, or 0 if not specified */
+ public int getMinor() { return minor; }
+
+ /** Returns the micro component of this version, or 0 if not specified */
+ public int getMicro() { return micro; }
+
+ /** Returns the qualifier component of this version, or "" if not specified */
+ public String getQualifier() { return qualifier; }
+
+ /**
+ * Returns the string representation of this version identifier as major.minor.micro.qualifier,
+ * omitting .qualifier if qualifier was empty or unspecified
+ */
+ public String toString() { return stringValue; }
+
+ public int hashCode() { return stringValue.hashCode(); }
+
+ /**
+ * Compares this <code>Version</code> to another.
+ *
+ * <p>
+ * A version is considered to be <b>equal to </b> another version if the
+ * major, minor and micro components are equal and the qualifier component
+ * is equal (using <code>String.equals</code>).
+ * <p>
+ *
+ * @param object The <code>Version</code> object to be compared.
+ * @return <code>true</code> if <code>object</code> is a
+ * <code>Version</code> and is equal to this object;
+ * <code>false</code> otherwise.
+ */
+ public boolean equals(Object object) {
+ if (!(object instanceof Version)) return false;
+ Version other = (Version) object;
+ if (this.major != other.major) return false;
+ if (this.minor != other.minor) return false;
+ if (this.micro != other.micro) return false;
+ return (this.qualifier.equals(other.qualifier));
+ }
+
+ @SuppressWarnings("unused")
+ private boolean equals(Object o1, Object o2) {
+ if (o1==null && o2==null) return true;
+ if (o1==null || o2==null) return false;
+ return o1.equals(o2);
+ }
+
+ /**
+ * Compares this <code>Version</code> object to another version.
+ * <p>
+ * A version is considered to be <b>less than </b> another version if its
+ * major component is less than the other version's major component, or the
+ * major components are equal and its minor component is less than the other
+ * version's minor component, or the major and minor components are equal
+ * and its micro component is less than the other version's micro component,
+ * or the major, minor and micro components are equal and it's qualifier
+ * component is less than the other version's qualifier component (using
+ * <code>String.compareTo</code>).
+ * <p>
+ * A version is considered to be <b>equal to</b> another version if the
+ * major, minor and micro components are equal and the qualifier component
+ * is equal (using <code>String.compareTo</code>).
+ * <p>
+ * Unspecified numeric components are treated as 0, unspecified qualifier is treated as the empty string.
+ *
+ * @param other the <code>Version</code> object to be compared.
+ * @return A negative integer, zero, or a positive integer if this object is
+ * less than, equal to, or greater than the specified <code>Version</code> object.
+ * @throws ClassCastException if the specified object is not a <code>Version</code>.
+ */
+ public int compareTo(Version other) {
+ if (other == this) return 0;
+
+ int result = this.getMajor() - other.getMajor();
+ if (result != 0) return result;
+
+ result = this.getMinor() - other.getMinor();
+ if (result != 0) return result;
+
+ result = this.getMicro() - other.getMicro();
+ if (result != 0) return result;
+
+ return getQualifier().compareTo(other.getQualifier());
+ }
+
+ /**
+ * Creates a version specification that only matches this version.
+ */
+ public VersionSpecification toSpecification() {
+ if (this == emptyVersion)
+ return VersionSpecification.emptyVersionSpecification;
+ else {
+ return new VersionSpecification(getMajor(), getMinor(), getMicro(),
+ getQualifier());
+ }
+ }
+}
diff --git a/component/src/main/java/com/yahoo/component/VersionSpecification.java b/component/src/main/java/com/yahoo/component/VersionSpecification.java
new file mode 100644
index 00000000000..b242509f9ab
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/VersionSpecification.java
@@ -0,0 +1,366 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+/**
+ * A component version specification.
+ * <p>
+ * Version specifications have four components.
+ * <ol>
+ * <li>Major version. A non-negative integer.</li>
+ * <li>Minor version. A non-negative integer.</li>
+ * <li>Micro version. A non-negative integer.</li>
+ * <li>Qualifier. An ascii text string. See <code>Version(String)</code> for the
+ * format of the qualifier string.</li>
+ * </ol>
+ * <p>
+ * A null version component means "unspecified", i.e any value of that
+ * component matches the specification
+ * <p>
+ * <code>VersionSpecification</code> objects are immutable.
+ *
+ * @author arnej27959
+ * @author bratseth
+ */
+
+public final class VersionSpecification implements Comparable<VersionSpecification> {
+
+ private Integer major = null;
+ private Integer minor = null;
+ private Integer micro = null;
+ private String qualifier = null;
+
+ private String stringValue;
+
+ /** The empty version */
+ public static final VersionSpecification emptyVersionSpecification = new VersionSpecification();
+
+ /** Creates an empty version */
+ public VersionSpecification() {
+ this(null, null, null, null);
+ }
+
+ /**
+ * Creates a version specification from the specified numerical components.
+ *
+ * @param major major component of the version specification, or null if not specified
+ * @throws IllegalArgumentException If the numerical components are
+ * negative.
+ */
+ public VersionSpecification(Integer major) {
+ this(major, null, null, null);
+ }
+
+ /**
+ * Creates a version specification from the specified numerical components.
+ *
+ * @param major major component of the version specification, or null if not specified
+ * @param minor minor component of the version specification, or null if not specified
+ * @throws IllegalArgumentException If the numerical components are
+ * negative.
+ */
+ public VersionSpecification(Integer major, Integer minor) {
+ this(major, minor, null, null);
+ }
+
+ /**
+ * Creates a version specification from the specified numerical components.
+ *
+ * @param major major component of the version specification, or null if not specified
+ * @param minor minor component of the version specification, or null if not specified
+ * @param micro micro component of the version specification, or null if not specified
+ * @throws IllegalArgumentException If the numerical components are
+ * negative.
+ */
+ public VersionSpecification(Integer major, Integer minor, Integer micro) {
+ this(major, minor, micro, null);
+ }
+
+ /**
+ * Creates a version specification from the specifed components.
+ *
+ * @param major major component of the version specification, or null if not specified
+ * @param minor minor component of the version specification, or null if not specified
+ * @param micro micro component of the version specification, or null if not specified
+ * @param qualifier Qualifier component of the version specification, or null if not specified
+ * @throws IllegalArgumentException if the numerical components are negative
+ * the qualifier string contains non-word/digit-characters, or
+ * an earlier component is not specified but a later one is
+ */
+ public VersionSpecification(Integer major, Integer minor, Integer micro, String qualifier) {
+ this.major = major;
+ this.minor = minor;
+ this.micro = micro;
+ this.qualifier = qualifier;
+ initialize();
+ }
+
+ /**
+ * Creates a version specification from the specified string.
+ *
+ * <p>
+ * VersionSpecification strings follows this grammar (same as Osgi versions):
+ *
+ * <pre>
+ * version ::= major('.'minor('.'micro('.'qualifier)?)?)?
+ * major ::= digit+
+ * minor ::= digit+
+ * micro ::= digit+
+ * qualifier ::= (alpha|digit|'_'|'-')+
+ * digit ::= [0..9]
+ * alpha ::= [a..zA..Z]
+ * </pre>
+ *
+ * @param versionString String representation of the version specification
+ * @throws IllegalArgumentException If <code>version</code> is improperly
+ * formatted.
+ */
+ public VersionSpecification(String versionString) {
+ if (! "".equals(versionString)) {
+ String[] components = versionString.split("\\x2e"); // Split on dot
+
+ if (components.length > 0) {
+ String s = components[0];
+ if (! s.equals("*")) {
+ major = new Integer(s);
+ }
+ }
+ if (components.length > 1) {
+ String s = components[1];
+ if (! s.equals("*")) {
+ minor = new Integer(s);
+ }
+ }
+ if (components.length > 2) {
+ String s = components[2];
+ if (! s.equals("*")) {
+ micro = new Integer(s);
+ }
+ }
+ if (components.length > 3) {
+ qualifier = components[3];
+ }
+ if (components.length > 4)
+ throw new IllegalArgumentException("Too many components in " + versionString);
+ }
+ initialize();
+ }
+
+ public static VersionSpecification fromString(String versionString) {
+ if (versionString == null) {
+ return emptyVersionSpecification;
+ } else {
+ return new VersionSpecification(versionString);
+ }
+ }
+
+ /**
+ * Must be called on construction after the component values are set
+ *
+ * @throws IllegalArgumentException If the numerical components are negative
+ * or the qualifier string is invalid.
+ */
+ private void initialize() {
+ stringValue = toStringValue(major, minor, micro, qualifier);
+ ensureUnspecifiedOnlyToTheRight(major, minor, micro, qualifier);
+
+ if (major!=null && major < 0)
+ throw new IllegalArgumentException("Negative major in " + this);
+ if (minor!=null && minor < 0)
+ throw new IllegalArgumentException("Negative minor in " + this);
+ if (micro!=null && micro < 0)
+ throw new IllegalArgumentException("Negative micro in " + this);
+
+ String q = getQualifier();
+ for (int i = 0; i < q.length(); i++) {
+ if ( !Character.isLetterOrDigit(q.charAt(i)) )
+ throw new IllegalArgumentException("Invalid qualifier in " + this +
+ ": Invalid character at position " + i + " in qualifier");
+ }
+ }
+
+ private String toStringValue(Object... objects) {
+ StringBuilder b = new StringBuilder();
+ boolean first = true;
+ for (Object o : objects) {
+ if (o != null) {
+ if (! first)
+ b.append(".");
+ b.append(o);
+ first = false;
+ }
+ }
+ return b.toString();
+ }
+
+
+ private void ensureUnspecifiedOnlyToTheRight(Object... objects) {
+ boolean restMustBeNull=false;
+ for (Object o : objects) {
+ if (restMustBeNull && o!=null)
+ throw new IllegalArgumentException("A component to the left of a specified is unspecified in " + this);
+ if (o==null)
+ restMustBeNull=true;
+ }
+ }
+
+ /** Returns the major component of this version, or 0 if not specified */
+ public int getMajor() { return major!=null ? major : 0; }
+
+ /** Returns the minor component of this version, or 0 if not specified */
+ public int getMinor() { return minor!=null ? minor : 0; }
+
+ /** Returns the micro component of this version, or 0 if not specified */
+ public int getMicro() { return micro!=null ? micro : 0; }
+
+ /** Returns the qualifier component of this version, or "" if not specified */
+ public String getQualifier() { return qualifier!=null ? qualifier : ""; }
+
+ /** Returns the specified major component, which may be null */
+ public Integer getSpecifiedMajor() { return major; }
+
+ /** Returns the specified minor component, which may be null */
+ public Integer getSpecifiedMinor() { return minor; }
+
+ /** Returns the specified micro component, which may be null */
+ public Integer getSpecifiedMicro() { return micro; }
+
+ /** Returns the specified qualifier component, which may be null */
+ public String getSpecifiedQualifier() { return qualifier; }
+
+ /**
+ * Returns the string representation of this version specification as major.minor.micro.qualifier, where
+ * trailing unspecified components are omitted
+ */
+ public String toString() { return stringValue; }
+
+ public int hashCode() { return stringValue.hashCode(); }
+
+ /**
+ * Compares this <code>VersionSpecification</code> to another.
+ *
+ * <p>
+ * A version is considered to be <b>equal to </b> another version if the
+ * major, minor and micro components are equal and the qualifier component
+ * is equal (using <code>String.equals</code>).
+ * <p>
+ * Note that two versions are only equal if they are equally specified, use
+ * {@link #matches} to match a more specified version to a less specified.
+ *
+ * @param object The <code>VersionSpecification</code> object to be compared.
+ * @return <code>true</code> if <code>object</code> is a
+ * <code>VersionSpecification</code> and is equal to this object;
+ * <code>false</code> otherwise.
+ */
+ public boolean equals(Object object) {
+ if (object == this) return true;
+
+ if (!(object instanceof VersionSpecification)) return false;
+ VersionSpecification other = (VersionSpecification) object;
+ if ( ! equals(this.major,other.major)) return false;
+ if ( ! equals(this.minor,other.minor)) return false;
+ if ( ! equals(this.micro,other.micro)) return false;
+ if ( ! equals(this.qualifier,other.qualifier)) return false;
+ return true;
+ }
+
+ private boolean equals(Object o1,Object o2) {
+ if (o1==null && o2==null) return true;
+ if (o1==null || o2==null) return false;
+ return o1.equals(o2);
+ }
+
+ /**
+ * check if the Version specification is equal to the
+ * empty spec ("match anything")
+ **/
+ public boolean isEmpty() {
+ return equals(emptyVersionSpecification);
+ }
+
+ /**
+ * Returns the lowest possible Version object that matches this spec
+ **/
+ public Version lowestMatchingVersion() {
+ return new Version(getMajor(), getMinor(), getMicro(), getQualifier());
+ }
+
+ /**
+ * Returns true if the given version matches this specification.
+ * It matches if all the numeric components specified are the same
+ * as in the version, and both qualifiers are either null or set
+ * to the same value. I.e, a version which includes a qualifier
+ * will only match exactly and will never return true from a
+ * request for an unspecified qualifier.
+ */
+ public boolean matches(Version version) {
+ if (matches(this.major, version.getMajor()) &&
+ matches(this.minor, version.getMinor()) &&
+ matches(this.micro, version.getMicro()))
+ {
+ return (version.getQualifier().equals(this.getQualifier()));
+ } else {
+ return false;
+ }
+ }
+
+ private boolean matches(Integer componentSpec, Integer component) {
+ if (componentSpec == null) return true;
+ return componentSpec.equals(component);
+ }
+
+ /**
+ * Compares this <code>VersionSpecification</code> object to another.
+ * <p>
+ * A version is considered to be <b>less than </b> another version if its
+ * major component is less than the other version's major component, or the
+ * major components are equal and its minor component is less than the other
+ * version's minor component, or the major and minor components are equal
+ * and its micro component is less than the other version's micro component,
+ * or the major, minor and micro components are equal and it's qualifier
+ * component is less than the other version's qualifier component (using
+ * <code>String.compareTo</code>).
+ * <p>
+ * A version is considered to be <b>equal to</b> another version if the
+ * major, minor and micro components are equal and the qualifier component
+ * is equal (using <code>String.compareTo</code>).
+ * <p>
+ * Unspecified numeric components are treated as 0, unspecified qualifier is treated as the empty string.
+ *
+ * @param other the <code>VersionSpecification</code> object to be compared.
+ * @return A negative integer, zero, or a positive integer if this object is
+ * less than, equal to, or greater than the specified <code>VersionSpecification</code> object.
+ * @throws ClassCastException if the specified object is not a <code>VersionSpecification</code>.
+ */
+ public int compareTo(VersionSpecification other) {
+ int result = this.getMajor() - other.getMajor();
+ if (result != 0) return result;
+
+ result = this.getMinor() - other.getMinor();
+ if (result != 0) return result;
+
+ result = this.getMicro() - other.getMicro();
+ if (result != 0) return result;
+
+ return getQualifier().compareTo(other.getQualifier());
+ }
+
+ public VersionSpecification intersect(VersionSpecification other) {
+ return new VersionSpecification(
+ intersect(major, other.major, "major"),
+ intersect(minor, other.minor, "minor"),
+ intersect(micro, other.micro, "micro"),
+ intersect(qualifier, other.qualifier, "qualifier"));
+ }
+
+ private <T> T intersect(T spec1, T spec2, String name) {
+ if (spec1 == null) {
+ return spec2;
+ } else if (spec2 == null) {
+ return spec1;
+ } else if (spec1.equals(spec2)) {
+ return spec1;
+ } else {
+ throw new RuntimeException("The " + name + " component does not match(" + spec1 + "!=" + spec2 + ").");
+ }
+ }
+}
diff --git a/component/src/main/java/com/yahoo/component/package-info.java b/component/src/main/java/com/yahoo/component/package-info.java
new file mode 100644
index 00000000000..e585ef1e0de
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/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.component;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/component/src/main/java/com/yahoo/component/provider/ComponentClass.java b/component/src/main/java/com/yahoo/component/provider/ComponentClass.java
new file mode 100644
index 00000000000..155b6bad783
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/provider/ComponentClass.java
@@ -0,0 +1,253 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+import com.yahoo.config.ConfigInstance;
+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.Type;
+import java.util.*;
+import java.util.logging.Logger;
+
+/**
+ * Encapsulates the class of a component to be created, along with the constructor that will be used.
+ *
+ * @author gjoranv
+ * @author bratseth
+ */
+public class ComponentClass<T extends AbstractComponent> {
+ private static Logger log = Logger.getLogger(ComponentClass.class.getName());
+
+ private final Class<T> clazz;
+ private final ComponentConstructor<T> constructor;
+
+ public ComponentClass(Class<T> clazz) {
+ this.clazz = clazz;
+ constructor = findPreferredConstructor();
+ if (! constructor.isLegal) {
+ throw new IllegalArgumentException("Class '" + clazz.getName() + "' must have at least one public " +
+ "constructor with an optional component ID followed by an optional FileAcquirer and " +
+ "zero or more config arguments: " +
+ clazz.getSimpleName() + "([ComponentId] [ConfigInstance ...])");
+ }
+ }
+
+ /**
+ * Create an instance of this ComponentClass with the given configId. The configs needed by the component
+ * must exist in the provided set of {@link com.yahoo.config.ConfigInstance}s.
+ *
+ * @param id The id of the component to create, never null.
+ * @param availableConfigs The set of available config instances.
+ * @param configId The config ID of the component, nullable.
+ * @return A new instance of the class represented by this ComponentClass.
+ */
+ @SuppressWarnings({"rawtypes", "unchecked"})
+ public T createComponent(ComponentId id, Map<ConfigKey, ConfigInstance> availableConfigs, String configId) {
+ if (configId == null) {
+ configId = System.getProperty("config.id");
+ }
+
+ boolean hasId = false;
+ List<Object> params = new LinkedList<>();
+ for (Class cc : constructor.parameters) {
+ if (cc.equals(ComponentId.class)) {
+ params.add(id);
+ hasId = true;
+ } else if (cc.getSuperclass().equals(ConfigInstance.class)) {
+ ConfigKey key = new ConfigKey(cc, configId);
+ if ((availableConfigs == null) || ! availableConfigs.containsKey(key)) {
+ throw new IllegalStateException
+ ("Could not resolve config instance '" + key + "' required to instantiate " + clazz);
+ }
+ params.add(availableConfigs.get(key));
+ }
+ }
+ T component = construct(params.toArray());
+
+ if (hasId && component.hasInitializedId() && !id.equals(component.getId())) {
+ log.warning("Component with id '" + id + "' tried to set illegal component id: '" + component.getId() +
+ "', or the component takes ComponentId as a constructor arg without calling super(id).");
+ }
+ // Enforce correct id - see bug #4036397
+ component.initId(id);
+
+ return component;
+ }
+
+ public ComponentConstructor<T> getPreferredConstructor() {
+ return constructor;
+ }
+
+ /**
+ * Creates an instance of this class. Due to the error-prone Object varargs, this method must be used with
+ * caution, and never from outside this class.
+ *
+ * @param arguments the arguments to the constructor
+ * @return The new instance.
+ * @throws RuntimeException if construction fails for some reason
+ */
+ private T construct(Object... arguments) {
+ String args = Arrays.toString(arguments);
+ try {
+ return constructor.getConstructor().newInstance(arguments);
+ } catch (InstantiationException e) {
+ throw new RuntimeException("Exception while instantiating " + clazz + " from " + args,e);
+ } catch (IllegalAccessException e) {
+ throw new RuntimeException("Could not access " + constructor + " of " + clazz);
+ } catch (InvocationTargetException e) {
+ throw new RuntimeException("Exception while executing constructor of " + clazz + " with " + args,e);
+ } catch (IllegalArgumentException e) {
+ log.warning(clazz.getName() + " expected ctor arguments:");
+ for (@SuppressWarnings("rawtypes") Class expectedArg : constructor.getConstructor().getParameterTypes())
+ log.warning(" " + expectedArg + " - " + System.identityHashCode(expectedArg));
+
+ log.warning(clazz.getName() + " actual ctor arguments: ");
+ for (Object actualArg : arguments)
+ log.warning(" " + actualArg.getClass() + " - " + System.identityHashCode(actualArg.getClass()));
+ throw new RuntimeException("Exception while executing constructor of " + clazz + " with " + args,e);
+ }
+
+ }
+
+ /**
+ * Returns the preferred constructor of the given class, or null if no satisfactory constructor is present.
+ * The preferred constructor is always the one with the most arguments of type T extends ConfigInstance.
+ *
+ * @return The preferred constructor.
+ */
+ @SuppressWarnings("unchecked")
+ private ComponentConstructor<T> findPreferredConstructor() {
+ @SuppressWarnings("rawtypes")
+ Constructor[] constructors = clazz.getConstructors();
+ if (constructors.length < 1) {
+ throw new RuntimeException("Class has no public constructors: " + clazz.getName());
+ }
+ ComponentConstructor<T> best = new ComponentConstructor<T>(constructors[0]);
+ for (int i = 1; i < constructors.length; i++) {
+ Constructor<T> c = constructors[i];
+ ComponentConstructor<T> cc = new ComponentConstructor<>(c);
+ if (cc.preferredTo(best)) {
+ best = cc;
+ }
+ }
+ return best;
+ }
+
+ /**
+ * Encapsulates a constructor for a ComponentClass. Immutable.
+ */
+ public static class ComponentConstructor<T> {
+
+ // The legal argument classes (except '? extends ConfigInstance' of course)
+ @SuppressWarnings("rawtypes")
+ private static final Set<Class> legalArgs = Collections.singleton((Class) ComponentId.class);
+
+ private final Constructor<T> constructor;
+
+ @SuppressWarnings("rawtypes")
+ private final Class[] parameters;
+ private final List<Class<? extends ConfigInstance>> configArgs;
+
+ public final boolean isLegal;
+ public final boolean hasComponentId;
+
+ public ComponentConstructor(Constructor<T> c) {
+ constructor = c;
+ parameters = c.getParameterTypes();
+
+ isLegal = isLegal(parameters);
+ hasComponentId = hasComponentId(parameters);
+ configArgs = findConfigArgs(parameters);
+ }
+
+ public Constructor<T> getConstructor() {
+ return constructor;
+ }
+
+ /**
+ * Returns true if this constructor is preferred to the other, or if they are equivalent.
+ * False otherwise.
+ * @param other The other constructor.
+ * @return true if this constructor is preferred to the other, false otherwise.
+ */
+ public boolean preferredTo(ComponentConstructor<T> other) {
+ if (this.isLegal && ! other.isLegal)
+ return true;
+ else if (! this.isLegal && other.isLegal)
+ return false;
+
+ // Both are legal
+ if (this.parameters.length > other.parameters.length)
+ return true;
+ else if (this.parameters.length < other.parameters.length)
+ return false;
+
+ // Equal number of args
+ if (this.configArgs.size() > other.configArgs.size())
+ return true;
+ else if (this.configArgs.size() < other.configArgs.size())
+ return false;
+
+ // Equal number of args and config args, prefer ComponentId
+ if (this.hasComponentId && ! other.hasComponentId)
+ return true;
+ else if (! this.hasComponentId && other.hasComponentId)
+ return false;
+
+ // Equivalent
+ return true;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static boolean isLegal(Class[] args) {
+ Set<Class> used = new HashSet<>();
+ for (Class cl : args) {
+ if (legalArgs.contains(cl)) {
+ if (used.contains(cl)) {
+ return false;
+ }
+ if (cl.equals(String.class) || cl.equals(ComponentId.class)) {
+ // Only one of these are allowed, so mark both as used.
+ used.add(String.class);
+ used.add(ComponentId.class);
+ } else {
+ used.add(cl);
+ }
+ } else {
+ // Must be a config arg
+ Class superclass = cl.getSuperclass();
+ if ((superclass == null) || !superclass.equals(com.yahoo.config.ConfigInstance.class))
+ return false;
+ }
+ }
+ return true;
+ }
+
+ @SuppressWarnings("rawtypes")
+ private static boolean hasComponentId(Class[] args) {
+ for (Class cl : args) {
+ if (cl.equals(ComponentId.class))
+ return true;
+ }
+ return false;
+ }
+
+ @SuppressWarnings({"unchecked", "rawtypes"})
+ private static List<Class<? extends ConfigInstance>> findConfigArgs(Class[] args) {
+ List<Class<? extends ConfigInstance>> configs = new ArrayList<>();
+ for (Class cl : args) {
+ Class superclass = cl.getSuperclass();
+ if ((superclass != null) && superclass.equals(ConfigInstance.class)) {
+ configs.add(cl);
+ }
+ }
+ return configs;
+ }
+
+ } // class ComponentConstructor
+
+}
diff --git a/component/src/main/java/com/yahoo/component/provider/ComponentRegistry.java b/component/src/main/java/com/yahoo/component/provider/ComponentRegistry.java
new file mode 100644
index 00000000000..705dcabb110
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/provider/ComponentRegistry.java
@@ -0,0 +1,190 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableMap;
+import com.yahoo.component.Component;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.Version;
+import com.yahoo.component.VersionSpecification;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * A generic superclass for component registries. Supports registration and lookup
+ * of components by id. The registry resolves id requests to the newest matching
+ * component version registered.
+ * <p>
+ * This registry supports the <i>freeze</i> pattern - changes can be made
+ * to this registry until {@link #freeze} is called. Subsequent change attempts will cause an
+ * exception. Freezing a registry after building makes it possible toi avoid locking and memory
+ * synchronization on lookups.
+ *
+ * @author bratseth
+ */
+public class ComponentRegistry<COMPONENT> {
+
+ /** All versions of all components, indexed by name and namespace */
+ private Map<ComponentId, Map<String, Map<Version, COMPONENT>>> componentsByNameByNamespace =
+ new LinkedHashMap<>();
+
+ /** All versions of all components indexed by id */
+ private Map<ComponentId, COMPONENT> componentsById =new LinkedHashMap<>();
+
+ /** True when this cannot be changed any more */
+ private boolean frozen=false;
+
+ /**
+ * Freezes this registry to prevent further changes. Override this to freeze internal data
+ * structures and dependent objects. Overrides must call super.
+ * Calling freeze on an already frozen registry must have no effect.
+ */
+ public synchronized void freeze() { frozen=true; }
+
+ /** returns whether this is currently frozen */
+ public final boolean isFrozen() { return frozen; }
+
+ /**
+ * Registers a component unless this registry is frozen.
+ * This will succeed even if this component name and version is already registered.
+ *
+ * @throws IllegalStateException if this chain is frozen
+ */
+ public void register(ComponentId id, COMPONENT component) {
+ if (frozen) throw new IllegalStateException("Cannot modify a frozen component registry");
+
+ Map<String, Map<Version, COMPONENT>> componentVersionsByName =
+ componentsByNameByNamespace.get(id.getNamespace());
+ if (componentVersionsByName == null) {
+ componentVersionsByName = new LinkedHashMap<>();
+ componentsByNameByNamespace.put(id.getNamespace(), componentVersionsByName);
+ }
+
+ Map<Version, COMPONENT> componentVersions = componentVersionsByName.get(id.getName());
+ if (componentVersions == null) {
+ componentVersions = new LinkedHashMap<>();
+ componentVersionsByName.put(id.getName(), componentVersions);
+ }
+ componentVersions.put(id.getVersion(), component);
+
+ componentsById.put(id, component);
+ }
+
+
+ /**
+ * Unregisters a component unless this registry is frozen.
+ * Note that the component is not deconstructed or otherwise modified in any way, this
+ * is the responsiblity of the caller.
+ *
+ * @param id the id of the component to be unregistered
+ * @return the component that was unregistered, or null if no such component was already registered
+ */
+ public COMPONENT unregister(ComponentId id) {
+ if (frozen) throw new IllegalStateException("Cannot modify a frozen component registry");
+
+ COMPONENT removed = componentsById.remove(id);
+
+ if (removed != null) {
+ //removed is non-null, so it must be present here as well:
+ Map<String, Map<Version, COMPONENT>> componentVersionsByName = componentsByNameByNamespace.get(id.getNamespace());
+ Map<Version, COMPONENT> componentVersions = componentVersionsByName.get(id.getName());
+ COMPONENT removedInner = componentVersions.remove(id.getVersion());
+ assert (removedInner == removed);
+
+ //clean up
+ if (componentVersions.isEmpty()) {
+ componentVersionsByName.remove(id.getName());
+ }
+ if (componentVersionsByName.isEmpty()) {
+ componentsByNameByNamespace.remove(id.getNamespace());
+ }
+ }
+ return removed;
+ }
+
+ /**
+ * See getComponent(ComponentSpecification)
+ * @param componentSpecification a component specification string, see {@link com.yahoo.component.Version}
+ * @return the component or null if no component of this name (and version, if specified) is registered here
+ */
+ public COMPONENT getComponent(String componentSpecification) {
+ return getComponent(new ComponentSpecification(componentSpecification));
+ }
+
+ public COMPONENT getComponent(ComponentId id) {
+ return componentsById.get(id);
+ }
+
+
+ /**
+ * Returns a component. If the id does not specify an (exact) version, the newest (matching) version is returned.
+ * For example, if version 3.1 is specified and we have 3.1.0, 3.1.1 and 3.1.3 registered, 3.1.3 is returned.
+ *
+ * @param id the id of the component to return. May not include a version, or include
+ * an underspecified version, in which case the highest (matching) version which
+ * does not contain a qualifier is returned
+ * @return the search chain or null if no component of this name (and matching version, if specified) is registered
+ */
+ public COMPONENT getComponent(ComponentSpecification id) {
+ Map<String, Map<Version, COMPONENT>> componentVersionsByName = componentsByNameByNamespace.get(id.getNamespace());
+ if (componentVersionsByName == null) return null; // No matching namespace
+
+ Map<Version, COMPONENT> versions = componentVersionsByName.get(id.getName());
+ if (versions==null) return null; // No versions of this component
+
+ Version version=findBestMatch(id.getVersionSpecification(), versions.keySet());
+ //if (version==null) return null; // No matching version
+
+ return versions.get(version);
+ }
+
+ /**
+ * Finds the best (highest) matching version among a set.
+ *
+ * @return the matching version, or null if there are no matches
+ */
+ protected static Version findBestMatch(VersionSpecification versionSpec,Set<Version> versions) {
+ Version bestMatch=null;
+ for (Version version : versions) {
+ //No version is set if getSpecifiedMajor() == null
+ //In that case we allow all versions
+ if (version == null || !versionSpec.matches(version)) continue;
+
+ if (bestMatch==null || bestMatch.compareTo(version)<0)
+ bestMatch=version;
+ }
+ return bestMatch;
+ }
+
+ /**
+ * Returns an unmodifiable snapshot of all components present in this registry.
+ */
+ public List<COMPONENT> allComponents() {
+ return ImmutableList.copyOf(componentsById.values());
+ }
+
+ /**
+ * Returns an unmodifiable snapshot of all components present in this registry, by id.
+ */
+ public Map<ComponentId, COMPONENT> allComponentsById() {
+ return ImmutableMap.copyOf(componentsById);
+ }
+
+ /** Returns the number of components in this */
+ public int getComponentCount() { return componentsById.size(); }
+
+ /** Returns a frozen registry with a single component, for convenience */
+ public static <COMPONENT> ComponentRegistry<COMPONENT> singleton(ComponentId id, COMPONENT component) {
+ ComponentRegistry<COMPONENT> registry = new ComponentRegistry<>();
+ registry.register(id, component);
+ registry.freeze();
+ return registry;
+ }
+
+}
diff --git a/component/src/main/java/com/yahoo/component/provider/Freezable.java b/component/src/main/java/com/yahoo/component/provider/Freezable.java
new file mode 100644
index 00000000000..36734c24e1b
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/provider/Freezable.java
@@ -0,0 +1,29 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider;
+
+/**
+ * A class which may be irreversibly frozen. Any attempt to change the state of this class after
+ * freezing throws an IllegalStateException.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public interface Freezable {
+
+ /**
+ * Freezes this component to prevent further changes. Override this to freeze internal data
+ * structures and dependent objects. Overrides must call super.
+ * Calling freeze on an already frozen class must have no effect.
+ */
+ public void freeze();
+
+
+ /**
+ * Inspect whether this object can be changed. If the object is immutable
+ * from construction, this should return true, even if freeze() never has
+ * been invoked.
+ *
+ * @return true if this instance is in an immutable state, false otherwise
+ * @since 5.1.4
+ */
+ public boolean isFrozen();
+}
diff --git a/component/src/main/java/com/yahoo/component/provider/FreezableClass.java b/component/src/main/java/com/yahoo/component/provider/FreezableClass.java
new file mode 100644
index 00000000000..df819c8c463
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/provider/FreezableClass.java
@@ -0,0 +1,44 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider;
+
+/**
+ * Convenience superclass of non-component freezables
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ */
+public class FreezableClass implements Freezable {
+
+ /** True when this cannot be changed any more */
+ private boolean frozen=false;
+
+ /**
+ * Freezes this class to prevent further changes. Override this to freeze internal data
+ * structures and dependent objects. Overrides must call super.
+ * Calling freeze on an already frozen registry must have no effect.
+ */
+ public synchronized void freeze() {
+ frozen=true;
+ }
+
+ /** Returns whether this is currently frozen */
+ public final boolean isFrozen() { return frozen; }
+
+ /** Throws an IllegalStateException if this is frozen */
+ protected void ensureNotFrozen() {
+ if (frozen)
+ throw new IllegalStateException(this + " is frozen and cannot be modified");
+ }
+
+ /** Clones this. The clone is <i>not</i> frozen */
+ public @Override FreezableClass clone() {
+ try {
+ FreezableClass clone=(FreezableClass)super.clone();
+ clone.frozen = false;
+ return clone;
+ }
+ catch (CloneNotSupportedException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/component/src/main/java/com/yahoo/component/provider/FreezableComponent.java b/component/src/main/java/com/yahoo/component/provider/FreezableComponent.java
new file mode 100644
index 00000000000..9d51b0b5819
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/provider/FreezableComponent.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+
+/**
+ * Superclass for freezable components
+ *
+ * @author bratseth
+ */
+public class FreezableComponent extends AbstractComponent implements Freezable {
+
+ /** True when this cannot be changed any more */
+ private boolean frozen=false;
+
+ protected FreezableComponent(ComponentId id) {
+ super(id);
+ }
+
+ @SuppressWarnings("unused")
+ protected FreezableComponent() {}
+
+ /**
+ * Freezes this component to prevent further changes. Override this to freeze internal data
+ * structures and dependent objects. Overrides must call super.
+ * Calling freeze on an already frozen registry must have no effect.
+ */
+ public synchronized void freeze() { frozen=true; }
+
+ /** Returns whether this is currently frozen */
+ public final boolean isFrozen() { return frozen; }
+
+ /** Throws an IllegalStateException if this is frozen */
+ protected void ensureNotFrozen() {
+ if (frozen)
+ throw new IllegalStateException(this + " is frozen and cannot be modified");
+ }
+
+ /** Clones this. The clone will <i>not</i> be frozen */
+ public @Override FreezableComponent clone() {
+ FreezableComponent clone=(FreezableComponent)super.clone();
+ clone.frozen = false;
+ return clone;
+ }
+
+}
diff --git a/component/src/main/java/com/yahoo/component/provider/FreezableSimpleComponent.java b/component/src/main/java/com/yahoo/component/provider/FreezableSimpleComponent.java
new file mode 100644
index 00000000000..ff979d64904
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/provider/FreezableSimpleComponent.java
@@ -0,0 +1,47 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.AbstractComponent;
+
+/**
+ * Superclass for simple freezable components
+ *
+ * @author bratseth
+ */
+public class FreezableSimpleComponent extends AbstractComponent implements Freezable {
+
+ /** True when this cannot be changed any more */
+ private boolean frozen=false;
+
+ protected FreezableSimpleComponent(ComponentId id) {
+ super(id);
+ }
+
+ protected FreezableSimpleComponent() {}
+
+ /**
+ * Freezes this component to prevent further changes. Override this to freeze internal data
+ * structures and dependent objects. Overrides must call super.
+ * Calling freeze on an already frozen registry must have no effect.
+ */
+ public synchronized void freeze() { frozen=true; }
+
+ /** Returns whether this is currently frozen */
+ public final boolean isFrozen() { return frozen; }
+
+ /** Throws an IllegalStateException if this is frozen */
+ protected void ensureNotFrozen() {
+ if (frozen)
+ throw new IllegalStateException(this + " is frozen and cannot be modified");
+ }
+
+ /** Clones this. The clone will <i>not</i> be frozen */
+ @Override
+ public FreezableSimpleComponent clone() {
+ FreezableSimpleComponent clone=(FreezableSimpleComponent)super.clone();
+ clone.frozen = false;
+ return clone;
+ }
+
+}
diff --git a/component/src/main/java/com/yahoo/component/provider/ListenableFreezable.java b/component/src/main/java/com/yahoo/component/provider/ListenableFreezable.java
new file mode 100644
index 00000000000..3e1bbd4153a
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/provider/ListenableFreezable.java
@@ -0,0 +1,15 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider;
+
+/**
+ * A freezable which supports listening
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ * @since 5.1.13
+ */
+public interface ListenableFreezable extends Freezable {
+
+ /** Adds a listener which will be called when this is frozen */
+ public void addFreezeListener(java.lang.Runnable runnable, java.util.concurrent.Executor executor);
+
+}
diff --git a/component/src/main/java/com/yahoo/component/provider/ListenableFreezableClass.java b/component/src/main/java/com/yahoo/component/provider/ListenableFreezableClass.java
new file mode 100644
index 00000000000..6d876b1fb41
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/provider/ListenableFreezableClass.java
@@ -0,0 +1,43 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.provider;
+
+import com.google.common.util.concurrent.ExecutionList;
+
+import java.util.concurrent.Executor;
+
+/**
+ * A convenience superclass for listenable freezables.
+ *
+ * @author <a href="mailto:bratseth@yahoo-inc.com">Jon Bratseth</a>
+ * @since 5.1.13
+ */
+public class ListenableFreezableClass extends FreezableClass implements ListenableFreezable {
+
+ private ExecutionList executionList = new ExecutionList();
+
+ /**
+ * Freezes this class to prevent further changes. Override this to freeze internal data
+ * structures and dependent objects. Overrides must call super.
+ * Calling freeze on an already frozen registry must have no effect.
+ * <p>
+ * Notifies listeners that freezing has happened.
+ */
+ public synchronized void freeze() {
+ super.freeze();
+ executionList.execute();
+ }
+
+ /** Adds a listener which will be invoked when this has become frozen. */
+ @Override
+ public void addFreezeListener(Runnable runnable, Executor executor) {
+ executionList.add(runnable,executor);
+ }
+
+ /** Clones this. The clone is <i>not</i> frozen and has no listeners. */
+ public @Override ListenableFreezableClass clone() {
+ ListenableFreezableClass clone=(ListenableFreezableClass)super.clone();
+ clone.executionList = new ExecutionList();
+ return clone;
+ }
+
+}
diff --git a/component/src/main/java/com/yahoo/component/provider/package-info.java b/component/src/main/java/com/yahoo/component/provider/package-info.java
new file mode 100644
index 00000000000..cea156b6b92
--- /dev/null
+++ b/component/src/main/java/com/yahoo/component/provider/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.component.provider;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/component/src/main/java/com/yahoo/container/util/Util.java b/component/src/main/java/com/yahoo/container/util/Util.java
new file mode 100644
index 00000000000..4a8923ac218
--- /dev/null
+++ b/component/src/main/java/com/yahoo/container/util/Util.java
@@ -0,0 +1,27 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.container.util;
+
+/**
+ * TODO: What is this?
+ *
+ * @author tonytv
+ */
+// TODO: Move to a a more appropriate package in vespajlib
+// TODO: Fix name
+public class Util {
+
+ // TODO: What is this?
+ @SafeVarargs
+ public static <T> T firstNonNull(T... args) {
+ for (T arg : args) {
+ if (arg != null)
+ return arg;
+ }
+ return null;
+ }
+
+ // TODO: What is this?
+ public static String quote(Object object) {
+ return "'" + object + "'";
+ }
+}
diff --git a/component/src/main/java/com/yahoo/container/util/package-info.java b/component/src/main/java/com/yahoo/container/util/package-info.java
new file mode 100644
index 00000000000..08d585d2489
--- /dev/null
+++ b/component/src/main/java/com/yahoo/container/util/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.util;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/component/src/test/java/com/yahoo/component/VersionSpecificationTestCase.java b/component/src/test/java/com/yahoo/component/VersionSpecificationTestCase.java
new file mode 100644
index 00000000000..6a8d0c569e7
--- /dev/null
+++ b/component/src/test/java/com/yahoo/component/VersionSpecificationTestCase.java
@@ -0,0 +1,142 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+import org.junit.Test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author bratseth
+ */
+public class VersionSpecificationTestCase {
+
+ @Test
+ public void testPrimitiveCreation() {
+ VersionSpecification version=new VersionSpecification(1,2,3,"qualifier");
+ assertEquals(1, (int)version.getSpecifiedMajor());
+ assertEquals(2, (int)version.getSpecifiedMinor());
+ assertEquals(3, (int)version.getSpecifiedMicro());
+ assertEquals("qualifier",version.getSpecifiedQualifier());
+ assertEquals(1, version.getMajor());
+ assertEquals(2, version.getMinor());
+ assertEquals(3, version.getMicro());
+ assertEquals("qualifier",version.getQualifier());
+ }
+
+ @Test
+ public void testUnderspecifiedPrimitiveCreation() {
+ VersionSpecification version=new VersionSpecification(1);
+ assertEquals(1,(int)version.getSpecifiedMajor());
+ assertEquals(null,version.getSpecifiedMinor());
+ assertEquals(null,version.getSpecifiedMicro());
+ assertEquals(null,version.getSpecifiedQualifier());
+ assertEquals(1, version.getMajor());
+ assertEquals(0, version.getMinor());
+ assertEquals(0, version.getMicro());
+ assertEquals("",version.getQualifier());
+ }
+
+ @Test
+ public void testStringCreation() {
+ VersionSpecification version=new VersionSpecification("1.2.3.qualifier");
+ assertEquals(1,(int)version.getSpecifiedMajor());
+ assertEquals(2,(int)version.getSpecifiedMinor());
+ assertEquals(3,(int)version.getSpecifiedMicro());
+ assertEquals("qualifier",version.getSpecifiedQualifier());
+ }
+
+ @Test
+ public void testUnderspecifiedStringCreation() {
+ VersionSpecification version=new VersionSpecification("1");
+ assertEquals(1,(int)version.getSpecifiedMajor());
+ assertEquals(null,version.getSpecifiedMinor());
+ assertEquals(null,version.getSpecifiedMicro());
+ assertEquals(null,version.getSpecifiedQualifier());
+ assertEquals(1, version.getMajor());
+ assertEquals(0, version.getMinor());
+ assertEquals(0, version.getMicro());
+ assertEquals("",version.getQualifier());
+ }
+
+ @Test
+ public void testEquality() {
+ assertEquals(new VersionSpecification(),VersionSpecification.emptyVersionSpecification);
+ assertEquals(new VersionSpecification(),new VersionSpecification(""));
+ assertEquals(new VersionSpecification(1),new VersionSpecification("1"));
+ assertEquals(new VersionSpecification(1,2),new VersionSpecification("1.2"));
+ assertEquals(new VersionSpecification(1,2,3),new VersionSpecification("1.2.3"));
+ assertEquals(new VersionSpecification(1,2,3,"qualifier"),new VersionSpecification("1.2.3.qualifier"));
+ }
+
+ @Test
+ public void testToString() {
+ assertEquals("",new VersionSpecification().toString());
+ assertEquals("1",new VersionSpecification(1).toString());
+ assertEquals("1.2",new VersionSpecification(1,2).toString());
+ assertEquals("1.2.3",new VersionSpecification(1,2,3).toString());
+ assertEquals("1.2.3.qualifier",new VersionSpecification(1,2,3,"qualifier").toString());
+ }
+
+ @Test
+ public void testMatches() {
+ assertTrue(new VersionSpecification("").matches(new Version("1")));
+ assertTrue(new VersionSpecification("1").matches(new Version("1")));
+ assertFalse(new VersionSpecification("1").matches(new Version("2")));
+ assertTrue(new VersionSpecification("").matches(new Version("1.2.3")));
+ assertFalse(new VersionSpecification("").matches(new Version("1.2.3.qualifier"))); // qualifier requires exact match
+
+ assertTrue(new VersionSpecification("1.2").matches(new Version("1.2")));
+ assertTrue(new VersionSpecification("1").matches(new Version("1.2")));
+ assertFalse(new VersionSpecification("1.2").matches(new Version("1.3")));
+ assertFalse(new VersionSpecification("1.2").matches(new Version("2")));
+
+ assertTrue(new VersionSpecification("1.2.3").matches(new Version("1.2.3")));
+ assertTrue(new VersionSpecification("1.2").matches(new Version("1.2.3")));
+ assertTrue(new VersionSpecification("1").matches(new Version("1.2.3")));
+ assertFalse(new VersionSpecification("1.2.3").matches(new Version("1.2.4")));
+ assertFalse(new VersionSpecification("1.3").matches(new Version("1.2.3")));
+ assertFalse(new VersionSpecification("2").matches(new Version("1.2.3")));
+
+ assertTrue(new VersionSpecification("1.2.3.qualifier").matches(new Version("1.2.3.qualifier")));
+ assertFalse(new VersionSpecification("1.2.3.qualifier1").matches(new Version("1.2.3.qualifier2")));
+ assertFalse(new VersionSpecification("1.2.3.qualifier").matches(new Version("1.2.3")));
+ assertFalse(new VersionSpecification("1.2.3.qualifier").matches(new Version("1.2")));
+ assertFalse(new VersionSpecification("1.2.3.qualifier").matches(new Version("1")));
+ assertFalse(new VersionSpecification("1.2.3.qualifier").matches(new Version("")));
+
+ assertFalse(new VersionSpecification(1, null, null, null).matches(new Version("1.2.3.qualifier")));
+ assertFalse(new VersionSpecification(1, 2, 0, "qualifier").matches(new Version("1.2.3.qualifier")));
+ assertFalse(new VersionSpecification(1, 2, 3).matches(new Version("1.2.3.qualifier")));
+ }
+
+ @Test
+ public void testOrder() {
+ assertTrue(new VersionSpecification("1.2.3").compareTo(new VersionSpecification("1.2.3"))==0);
+ assertTrue(new VersionSpecification("1.2.3").compareTo(new VersionSpecification("1.2.4"))<0);
+ assertTrue(new VersionSpecification("1.2.3").compareTo(new VersionSpecification("1.2.2"))>0);
+
+ assertTrue(new VersionSpecification("1.2.3").compareTo(new VersionSpecification("2"))<0);
+ assertTrue(new VersionSpecification("1.2.3").compareTo(new VersionSpecification("1.3"))<0);
+
+ assertTrue(new VersionSpecification("1.0.0").compareTo(new VersionSpecification("1"))==0);
+ }
+
+ @Test
+ public void testValidIntersect() {
+ VersionSpecification mostSpecific = new VersionSpecification(4, 2, 1);
+ VersionSpecification leastSpecific = new VersionSpecification(4, 2);
+
+ assertEquals(mostSpecific,
+ mostSpecific.intersect(leastSpecific));
+ assertEquals(mostSpecific,
+ leastSpecific.intersect(mostSpecific));
+ }
+
+ @Test(expected=RuntimeException.class)
+ public void testInvalidIntersect() {
+ new VersionSpecification(4, 1).intersect(
+ new VersionSpecification(4, 2));
+ }
+}
diff --git a/component/src/test/java/com/yahoo/component/VersionTestCase.java b/component/src/test/java/com/yahoo/component/VersionTestCase.java
new file mode 100644
index 00000000000..1d28de853a8
--- /dev/null
+++ b/component/src/test/java/com/yahoo/component/VersionTestCase.java
@@ -0,0 +1,81 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component;
+
+import com.yahoo.text.Utf8Array;
+import com.yahoo.text.Utf8String;
+
+/**
+ * @author bratseth
+ */
+public class VersionTestCase extends junit.framework.TestCase {
+
+ public void testPrimitiveCreation() {
+ Version version=new Version(1,2,3,"qualifier");
+ assertEquals(1,version.getMajor());
+ assertEquals(2,version.getMinor());
+ assertEquals(3,version.getMicro());
+ assertEquals("qualifier",version.getQualifier());
+ }
+
+ public void testUnderspecifiedPrimitiveCreation() {
+ Version version=new Version(1);
+ assertEquals(1,version.getMajor());
+ assertEquals(1,version.getMajor());
+ assertEquals(0,version.getMinor());
+ assertEquals(0,version.getMicro());
+ assertEquals("",version.getQualifier());
+ }
+
+ public void testStringCreation() {
+ Version version=new Version("1.2.3.qualifier");
+ assertEquals(1,version.getMajor());
+ assertEquals(2,version.getMinor());
+ assertEquals(3,version.getMicro());
+ assertEquals("qualifier",version.getQualifier());
+ }
+ public void testUtf8StringCreation() {
+ Version version=new Version((Utf8Array)new Utf8String("1.2.3.qualifier"));
+ assertEquals(1,version.getMajor());
+ assertEquals(2,version.getMinor());
+ assertEquals(3,version.getMicro());
+ assertEquals("qualifier",version.getQualifier());
+ }
+
+ public void testUnderspecifiedStringCreation() {
+ Version version=new Version("1");
+ assertEquals(1,version.getMajor());
+ assertEquals(0,version.getMinor());
+ assertEquals(0,version.getMicro());
+ assertEquals("",version.getQualifier());
+ }
+
+ public void testEquality() {
+ assertEquals(new Version(),Version.emptyVersion);
+ assertEquals(new Version(),new Version(""));
+ assertEquals(new Version(0,0,0),Version.emptyVersion);
+ assertEquals(new Version(1),new Version("1"));
+ assertEquals(new Version(1,2),new Version("1.2"));
+ assertEquals(new Version(1,2,3),new Version("1.2.3"));
+ assertEquals(new Version(1,2,3,"qualifier"),new Version("1.2.3.qualifier"));
+ }
+
+ public void testToString() {
+ assertEquals("",new Version().toString());
+ assertEquals("1",new Version(1).toString());
+ assertEquals("1.2",new Version(1,2).toString());
+ assertEquals("1.2.3",new Version(1,2,3).toString());
+ assertEquals("1.2.3.qualifier",new Version(1,2,3,"qualifier").toString());
+ }
+
+ public void testOrder() {
+ assertTrue(new Version("1.2.3").compareTo(new Version("1.2.3"))==0);
+ assertTrue(new Version("1.2.3").compareTo(new Version("1.2.4"))<0);
+ assertTrue(new Version("1.2.3").compareTo(new Version("1.2.2"))>0);
+
+ assertTrue(new Version("1.2.3").compareTo(new Version("2"))<0);
+ assertTrue(new Version("1.2.3").compareTo(new Version("1.3"))<0);
+
+ assertTrue(new Version("1.0.0").compareTo(new Version("1"))==0);
+ }
+
+}