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 /component/src |
Publish
Diffstat (limited to 'component/src')
22 files changed, 2347 insertions, 0 deletions
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); + } + +} |