summaryrefslogtreecommitdiffstats
path: root/chain
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 /chain
Publish
Diffstat (limited to 'chain')
-rw-r--r--chain/.gitignore4
-rw-r--r--chain/OWNERS2
-rwxr-xr-xchain/pom.xml109
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/Chain.java131
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/ChainedComponent.java100
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/ChainsConfigurer.java84
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/Phase.java52
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/After.java20
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/Before.java20
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/Dependencies.java73
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/Provides.java18
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilder.java169
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNameProvider.java63
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNode.java35
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ConflictingNodeTypeException.java16
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/CycleDependenciesException.java45
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/NameProvider.java29
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/Node.java85
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodes.java39
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/PhaseNameProvider.java28
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/dependencies/package-info.java7
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/model/ChainSpecification.java221
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/model/ChainedComponentModel.java31
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/model/ChainsModel.java83
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/model/ChainsModelBuilder.java84
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/model/ComponentAdaptor.java31
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/model/Resolver.java13
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/model/package-info.java5
-rw-r--r--chain/src/main/java/com/yahoo/component/chain/package-info.java7
-rw-r--r--chain/src/main/resources/configdefinitions/chains.def43
-rw-r--r--chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilderTest.java248
-rw-r--r--chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodesTest.java104
-rw-r--r--chain/src/test/java/com/yahoo/component/chain/model/ChainsModelBuilderTest.java72
33 files changed, 2071 insertions, 0 deletions
diff --git a/chain/.gitignore b/chain/.gitignore
new file mode 100644
index 00000000000..c0dd71fac68
--- /dev/null
+++ b/chain/.gitignore
@@ -0,0 +1,4 @@
+chain.iml
+target
+
+/pom.xml.build
diff --git a/chain/OWNERS b/chain/OWNERS
new file mode 100644
index 00000000000..34deebf8e0b
--- /dev/null
+++ b/chain/OWNERS
@@ -0,0 +1,2 @@
+gjoranv
+bratseth
diff --git a/chain/pom.xml b/chain/pom.xml
new file mode 100755
index 00000000000..fe83d2bfe74
--- /dev/null
+++ b/chain/pom.xml
@@ -0,0 +1,109 @@
+<?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>chain</artifactId>
+ <packaging>jar</packaging>
+ <version>6-SNAPSHOT</version>
+ <dependencies>
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>annotations</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.google.code.findbugs</groupId>
+ <artifactId>jsr305</artifactId>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>annotations</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>junit</groupId>
+ <artifactId>junit</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.hamcrest</groupId>
+ <artifactId>hamcrest-library</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>org.mockito</groupId>
+ <artifactId>mockito-core</artifactId>
+ <scope>test</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>provided-dependencies</artifactId>
+ <version>${project.version}</version>
+ <type>pom</type>
+ <scope>provided</scope>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>container-di</artifactId>
+ <version>${project.version}</version>
+ </dependency>
+ <dependency>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>component</artifactId>
+ <version>${project.version}</version>
+ <scope>provided</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>
+ </dependency>
+ <dependency>
+ <groupId>net.jcip</groupId>
+ <artifactId>jcip-annotations</artifactId>
+ <version>1.0</version>
+ </dependency>
+ </dependencies>
+ <build>
+ <plugins>
+ <plugin>
+ <groupId>com.yahoo.vespa</groupId>
+ <artifactId>config-class-plugin</artifactId>
+ <version>${project.version}</version>
+ <executions>
+ <execution>
+ <goals>
+ <goal>config-gen</goal>
+ </goals>
+ </execution>
+ <execution>
+ <id>configgen-test-defs</id>
+ <phase>generate-test-sources</phase>
+ <goals>
+ <goal>config-gen</goal>
+ </goals>
+ <configuration>
+ <defFilesDirectories>src/test/vespa-configdef</defFilesDirectories>
+ <outputDirectory>target/generated-test-sources/vespa-configgen-plugin</outputDirectory>
+ <testConfig>true</testConfig>
+ </configuration>
+ </execution>
+ </executions>
+ </plugin>
+ </plugins>
+ </build>
+</project>
diff --git a/chain/src/main/java/com/yahoo/component/chain/Chain.java b/chain/src/main/java/com/yahoo/component/chain/Chain.java
new file mode 100644
index 00000000000..e067b934f03
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/Chain.java
@@ -0,0 +1,131 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain;
+
+import com.google.common.collect.ImmutableList;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.chain.dependencies.ordering.ChainBuilder;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * An immutable ordered list of components
+ *
+ * @author tonytv
+ */
+public class Chain<COMPONENT extends ChainedComponent> {
+ final private List<COMPONENT> componentList;
+ private final ComponentId id;
+
+ /** Create a chain directly. This will NOT order the chain by the ordering constraints. */
+ public Chain(String id, List<COMPONENT> componentList) {
+ this(new ComponentId(id), componentList);
+ }
+
+ /** Create a chain directly. This will NOT order the chain by the ordering constraints. */
+ public Chain(ComponentId id, List<COMPONENT> componentList) {
+ this.id = id;
+ this.componentList = ImmutableList.copyOf(componentList);
+ }
+
+ /** Create a chain directly. This will NOT order the chain by the ordering constraints. */
+ public Chain(List<COMPONENT> componentList) {
+ this(new ComponentId("anonymous chain"), componentList);
+ }
+
+ /** Create a chain directly. This will NOT order the chain by the ordering constraints. */
+ @SafeVarargs
+ public Chain(COMPONENT... components) {
+ this("anonymous chain", components);
+ }
+
+ /** Create a chain directly. This will NOT order the chain by the ordering constraints. */
+ @SafeVarargs
+ public Chain(String id, COMPONENT... components) {
+ this(new ComponentId(id), components);
+ }
+
+ /** Create a chain directly. This will NOT order the chain by the ordering constraints. */
+ @SafeVarargs
+ public Chain(ComponentId id, COMPONENT... components) {
+ this(id, Arrays.<COMPONENT>asList(components));
+ }
+
+ /** Create a chain by using a builder. This will order the chain by the ordering constraints. */
+ public Chain(ComponentId id, Collection<COMPONENT> components, Collection<Phase> phases) {
+ this(id, buildChain(
+ emptyListIfNull(components),
+ emptyListIfNull(phases)).components());
+
+ }
+
+ public ComponentId getId() {
+ return id;
+ }
+
+ private static <T> Collection<T> emptyListIfNull(Collection<T> collection) {
+ return collection == null ? Collections.<T>emptyList() : collection;
+ }
+
+ private static <T extends ChainedComponent> Chain<T> buildChain(Collection<T> components, Collection<Phase> phases) {
+ ChainBuilder<T> builder = new ChainBuilder<>(new ComponentId("temp"));
+ for (Phase phase : phases) {
+ builder.addPhase(phase);
+ }
+
+ for (T component : components) {
+ builder.addComponent(component);
+ }
+
+ return builder.orderNodes();
+ }
+
+ public List<COMPONENT> components() {
+ return componentList;
+ }
+
+ public
+ @Override
+ String toString() {
+ StringBuilder b = new StringBuilder("chain '");
+ b.append(getId().stringValue());
+ b.append("' [");
+ appendComponent(0, b);
+ appendComponent(1, b);
+ if (components().size() > 3)
+ b.append("... -> ");
+ if (components().size() > 2)
+ appendComponent(components().size() - 1, b);
+ b.append("]");
+ return b.toString();
+ }
+
+ private void appendComponent(int i, StringBuilder b) {
+ if (i >= components().size()) return;
+ b.append(components().get(i).getId().stringValue());
+ if (i < components().size() - 1)
+ b.append(" -> ");
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+
+ Chain chain = (Chain) o;
+
+ if (!componentList.equals(chain.componentList)) return false;
+ if (!id.equals(chain.id)) return false;
+
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int result = componentList.hashCode();
+ result = 31 * result + id.hashCode();
+ return result;
+ }
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/ChainedComponent.java b/chain/src/main/java/com/yahoo/component/chain/ChainedComponent.java
new file mode 100644
index 00000000000..3a31b07adb7
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/ChainedComponent.java
@@ -0,0 +1,100 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.chain.dependencies.After;
+import com.yahoo.component.chain.dependencies.Before;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.component.chain.dependencies.Provides;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.InvocationTargetException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+/**
+ * Component with dependencies.
+ *
+ * @author tonytv
+ */
+public abstract class ChainedComponent extends AbstractComponent {
+
+ /** The immutable set of dependencies of this. NOTE: the default is only for unit testing. */
+ private Dependencies dependencies = getDefaultAnnotatedDependencies();
+
+ public ChainedComponent(ComponentId id) {
+ super(id);
+ }
+
+ protected ChainedComponent() {}
+
+ /**
+ * Called by the container to assign the full set of dependencies to this class (configured and declared).
+ * This is called once before this is started.
+ * @param dependencies The configured dependencies, that this method will merge with annotated dependencies.
+ */
+ public void initDependencies(Dependencies dependencies) {
+ this.dependencies = dependencies.union(getDefaultAnnotatedDependencies());
+ }
+
+ /** Returns the configured and declared dependencies of this chainedcomponent */
+ public Dependencies getDependencies() { return dependencies; }
+
+ /** This method is here only for legacy reasons, do not override. */
+ protected Dependencies getDefaultAnnotatedDependencies() {
+ Dependencies dependencies = getAnnotatedDependencies(com.yahoo.yolean.chain.Provides.class, com.yahoo.yolean.chain.Before.class, com.yahoo.yolean.chain.After.class);
+ Dependencies legacyDependencies = getAnnotatedDependencies(Provides.class, Before.class, After.class);
+
+ return dependencies.union(legacyDependencies);
+ }
+
+ /**
+ * @param providesClass The annotation class representing 'provides'.
+ * @param beforeClass The annotation class representing 'before'.
+ * @param afterClass The annotation class representing 'after'.
+ * @return a new {@link Dependencies} created from the annotations given in this component's class.
+ */
+ protected Dependencies getAnnotatedDependencies(Class<? extends Annotation> providesClass,
+ Class<? extends Annotation> beforeClass,
+ Class<? extends Annotation> afterClass) {
+ return new Dependencies(
+ allOf(getSymbols(this, providesClass), this.getClass().getSimpleName(), this.getClass().getName()),
+ getSymbols(this, beforeClass),
+ getSymbols(this, afterClass));
+ }
+
+ // TODO: move to vespajlib.
+ private static List<String> allOf(List<String> symbols, String... otherSymbols) {
+ List<String> result = new ArrayList<>(symbols);
+ result.addAll(Arrays.asList(otherSymbols));
+ return result;
+ }
+
+
+ private static List<String> getSymbols(ChainedComponent component, Class<? extends Annotation> annotationClass) {
+ List<String> result = new ArrayList<>();
+
+ result.addAll(annotationSymbols(component, annotationClass));
+ return result;
+ }
+
+ private static Collection<String> annotationSymbols(ChainedComponent component, Class<? extends Annotation> annotationClass) {
+
+ try {
+ Annotation annotation = component.getClass().getAnnotation(annotationClass);
+ if (annotation != null) {
+ Object values = annotationClass.getMethod("value").invoke(annotation);
+ return Arrays.asList((String[])values);
+ }
+ return Collections.emptyList();
+
+ } catch (IllegalAccessException | NoSuchMethodException | InvocationTargetException e) {
+ throw new RuntimeException(e);
+ }
+ }
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/ChainsConfigurer.java b/chain/src/main/java/com/yahoo/component/chain/ChainsConfigurer.java
new file mode 100644
index 00000000000..4c55c061f18
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/ChainsConfigurer.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.model.ChainSpecification;
+import com.yahoo.component.chain.model.ChainedComponentModel;
+import com.yahoo.component.chain.model.ChainsModel;
+import com.yahoo.component.provider.ComponentRegistry;
+import com.yahoo.config.ConfigurationRuntimeException;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+/**
+ * Configures a registry of chains.
+ *
+ * @author bratseth
+ * @author gjoranv
+ */
+public class ChainsConfigurer {
+
+ public static <COMPONENT extends ChainedComponent> void prepareChainRegistry(
+ ComponentRegistry<Chain<COMPONENT>> registry,
+ ChainsModel model,
+ ComponentRegistry<COMPONENT> allComponents) {
+
+ initDependencies(model, allComponents);
+ instantiateChains(registry, model, allComponents);
+ }
+
+ private static <COMPONENT extends ChainedComponent> void initDependencies(
+ ChainsModel model,
+ ComponentRegistry<COMPONENT> allComponents) {
+
+ for (ChainedComponentModel componentModel : model.allComponents()) {
+ COMPONENT component = getComponentOrThrow(allComponents, componentModel.getComponentId().toSpecification());
+ component.initDependencies(componentModel.dependencies);
+ }
+ }
+
+ private static <COMPONENT extends ChainedComponent> COMPONENT getComponentOrThrow(
+ ComponentRegistry<COMPONENT> registry,
+ ComponentSpecification specification) {
+
+ COMPONENT component = registry.getComponent(specification);
+ if (component == null) {
+ throw new ConfigurationRuntimeException("No such component '" + specification + "'");
+ }
+
+ return component;
+ }
+
+ private static <COMPONENT extends ChainedComponent> void instantiateChains(
+ ComponentRegistry<Chain<COMPONENT>> chainRegistry,
+ ChainsModel model,
+ ComponentRegistry<COMPONENT> allComponents) {
+
+ for (ChainSpecification chain : model.allChainsFlattened()) {
+ try {
+ Chain<COMPONENT> componentChain = new Chain<>(chain.componentId,
+ resolveComponents(chain.componentReferences, allComponents),
+ chain.phases());
+ chainRegistry.register(chain.componentId, componentChain);
+ } catch (Exception e) {
+ throw new ConfigurationRuntimeException("Invalid chain '" + chain.componentId + "'", e);
+ }
+ }
+ }
+
+ private static <T extends ChainedComponent> List<T> resolveComponents(
+ Set<ComponentSpecification> componentSpecifications,
+ ComponentRegistry<T> allComponents) {
+
+ List<T> components = new ArrayList<>(componentSpecifications.size());
+ for (ComponentSpecification componentSpec : componentSpecifications) {
+ T component = getComponentOrThrow(allComponents, componentSpec);
+ components.add(component);
+ }
+ return components;
+ }
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/Phase.java b/chain/src/main/java/com/yahoo/component/chain/Phase.java
new file mode 100644
index 00000000000..fcc1255a5cc
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/Phase.java
@@ -0,0 +1,52 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain;
+
+import com.yahoo.component.chain.dependencies.Dependencies;
+import net.jcip.annotations.Immutable;
+
+import java.util.Set;
+import java.util.TreeSet;
+
+/**
+ * Used for many to many constraints on searcher ordering.
+ *
+ * @author tonytv
+ */
+@Immutable
+public class Phase {
+ public final Dependencies dependencies;
+
+ public Phase(String name, Set<String> before, Set<String> after) {
+ dependencies = new Dependencies(provides(name), before, after);
+ }
+
+ public Phase(String name, Dependencies dependencies) {
+ this(name, dependencies.before(), dependencies.after());
+ assert(dependencies.provides().isEmpty());
+ }
+
+ private Set<String> provides(String name) {
+ Set<String> provides = new TreeSet<>();
+ provides.add(name);
+ return provides;
+ }
+
+ public String getName() {
+ return dependencies.provides().iterator().next();
+ }
+
+ public Set<String> before() {
+ return dependencies.before();
+ }
+
+ public Set<String> after() {
+ return dependencies.after();
+ }
+
+ public Phase union(Phase phase) {
+ assert(getName().equals(phase.getName()));
+
+ Dependencies union = dependencies.union(phase.dependencies);
+ return new Phase(getName(), union.before(), union.after());
+ }
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/After.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/After.java
new file mode 100644
index 00000000000..6b3c1b43585
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/After.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.chain.dependencies;
+
+import java.lang.annotation.*;
+
+/**
+ * Components or phases providing names contained in this list must be
+ * placed earlier in the chain than the component that is annotated.
+ * <p>
+ * See {@link com.yahoo.component.chain.dependencies.ordering.ChainBuilder}
+ * for dependency handling information.
+ *
+ * @author tonytv
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface After {
+ public abstract String[] value() default {};
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/Before.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/Before.java
new file mode 100644
index 00000000000..ebf0c2832f7
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/Before.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.chain.dependencies;
+
+import java.lang.annotation.*;
+
+/**
+ * Components or phases providing names contained in this list must be
+ * placed later in the chain than the component that is annotated.
+ * <p>
+ * See {@link com.yahoo.component.chain.dependencies.ordering.ChainBuilder}
+ * for dependency handling information.
+ *
+ * @author tonytv
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface Before {
+ public abstract String[] value() default {};
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/Dependencies.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/Dependencies.java
new file mode 100644
index 00000000000..e39d7a5c56e
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/Dependencies.java
@@ -0,0 +1,73 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies;
+
+import java.util.*;
+
+import com.google.common.collect.ImmutableSet;
+import net.jcip.annotations.Immutable;
+
+/**
+ * Constraints for ordering ChainedComponents in chains.
+ *
+ * @author tonytv
+ */
+@Immutable
+public class Dependencies {
+
+ private final Set<String> provides;
+ private final Set<String> before;
+ private final Set<String> after;
+
+ /**
+ * Create from collections of strings, typically from config.
+ */
+ public Dependencies(Collection<String> provides, Collection<String> before, Collection<String> after) {
+ this.provides = immutableSet(provides);
+ this.before = immutableSet(before);
+ this.after = immutableSet(after);
+ }
+
+ public static Dependencies emptyDependencies() {
+ return new Dependencies(null, null, null);
+ }
+
+ public Dependencies union(Dependencies dependencies) {
+ return new Dependencies(
+ union(provides, dependencies.provides),
+ union(before, dependencies.before),
+ union(after, dependencies.after));
+ }
+
+ private Set<String> immutableSet(Collection<String> set) {
+ if (set == null) return ImmutableSet.of();
+ return ImmutableSet.copyOf(set);
+ }
+
+ private Set<String> union(Set<String> s1, Set<String> s2) {
+ Set<String> result = new LinkedHashSet<>(s1);
+ result.addAll(s2);
+ return result;
+ }
+
+ @Override
+ public String toString() {
+ return "Dependencies{" +
+ "provides=" + provides +
+ ", before=" + before +
+ ", after=" + after +
+ '}';
+ }
+
+ public Set<String> provides() {
+ return provides;
+ }
+
+ public Set<String> before() {
+ return before;
+ }
+
+ public Set<String> after() {
+ return after;
+ }
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/Provides.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/Provides.java
new file mode 100644
index 00000000000..deb3096862e
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/Provides.java
@@ -0,0 +1,18 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies;
+
+import java.lang.annotation.*;
+
+/**
+ * Mark this component as providing some named functionality.
+ * Other components can then mark themselves as "before" and "after" the string provided here,
+ * to impose constraints on ordering.
+ *
+ * @author tonytv
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.TYPE)
+@Inherited
+public @interface Provides {
+ public abstract String[] value() default {};
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilder.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilder.java
new file mode 100644
index 00000000000..4d36dc53ca9
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilder.java
@@ -0,0 +1,169 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.chain.Chain;
+import com.yahoo.component.chain.ChainedComponent;
+import com.yahoo.component.chain.Phase;
+
+
+/**
+ * Given a set of phases and a set of components,
+ * a ordered list of components satisfying the dependencies is given if possible.
+ * <p>
+ * The phase list implicitly defines the ordering:
+ * {@literal if i < j : p_i before p_j where i,j are valid indexes of the phrase list p.}
+ * <p>
+ * If multiple components provide the same name, ALL the components providing
+ * the same name must be placed earlier/later than an entity depending on
+ * that name.
+ * <p>
+ * A warning will be logged if multiple components of different types provides the
+ * same name. A component can not provide the same name as a phase.
+ *
+ * @author tonytv
+ */
+public class ChainBuilder<T extends ChainedComponent> {
+ private final ComponentId id;
+ private int numComponents = 0;
+ private int priority = 1;
+
+ private Map<String, NameProvider> nameProviders =
+ new LinkedHashMap<>();
+
+ private Node allPhase;
+
+ public ChainBuilder(ComponentId id) {
+ this.id = id;
+ allPhase = addPhase(new Phase("*", set("*"), Collections.<String>emptySet()));
+ }
+
+ private Set<String> set(String... s) {
+ return new HashSet<>(Arrays.asList(s));
+ }
+
+ public PhaseNameProvider addPhase(Phase phase) {
+ NameProvider nameProvider = nameProviders.get(phase.getName());
+ if (nameProvider instanceof ComponentNameProvider) {
+ throw new ConflictingNodeTypeException("Cannot add phase '" + phase.getName() + "' as it is already provided by " + nameProvider);
+ }
+ PhaseNameProvider phaseNameProvider;
+ if(nameProvider == null) {
+ phaseNameProvider = new PhaseNameProvider(phase.getName(), priority++);
+ } else {
+ phaseNameProvider = (PhaseNameProvider) nameProvider;
+ }
+ nameProviders.put(phase.getName(), phaseNameProvider);
+ for(String before : phase.before()) {
+ phaseNameProvider.before(getPhaseNameProvider(before));
+ }
+ for(String after : phase.after()) {
+ getPhaseNameProvider(after).before(phaseNameProvider);
+ }
+
+ return phaseNameProvider;
+ }
+
+ public void addComponent(ChainedComponent component) {
+ ComponentNode<ChainedComponent> componentNode = new ComponentNode<>(component, priority++);
+
+ ensureProvidesNotEmpty(component);
+ for (String name : component.getDependencies().provides()) {
+ NameProvider nameProvider = getNameProvider(name);
+
+ nameProvider.addNode(componentNode);
+ }
+
+ for (String before : component.getDependencies().before()) {
+ componentNode.before(getNameProvider(before));
+ }
+
+ for (String after : component.getDependencies().after()) {
+ getNameProvider(after).before(componentNode);
+ }
+
+ ++numComponents;
+ }
+
+ //destroys this dependency handler in the process
+ @SuppressWarnings("unchecked")
+ public Chain<T> orderNodes() {
+ List<T> chain = new ArrayList<>();
+ OrderedReadyNodes readyNodes = getReadyNodes();
+
+ while (!readyNodes.isEmpty() || popAllPhase(readyNodes) ) {
+ Node candidate = readyNodes.pop();
+
+ candidate.removed(readyNodes);
+
+ if ( candidate instanceof ComponentNode)
+ chain.add(((ComponentNode<T>)candidate).getComponent());
+ }
+
+ if ( chain.size() != numComponents)
+ throw new CycleDependenciesException(nameProviders);
+
+ //prevent accidental reuse
+ nameProviders = null;
+
+ return new Chain<>(id, chain);
+ }
+
+ private void ensureProvidesNotEmpty(ChainedComponent component) {
+ if (component.getDependencies().provides().isEmpty()) {
+ throw new RuntimeException("The component " + component.getId() + " did not provide anything.");
+ }
+ }
+
+ private Node getPhaseNameProvider(String name) {
+ NameProvider nameProvider = nameProviders.get(name);
+ if (nameProvider != null)
+ return nameProvider;
+ else {
+ nameProvider = new PhaseNameProvider(name, priority++);
+ nameProviders.put(name, nameProvider);
+ return nameProvider;
+ }
+ }
+
+ private boolean popAllPhase(OrderedReadyNodes readyNodes) {
+ if (allPhase == null) {
+ return false;
+ } else {
+ Node phase = allPhase;
+ allPhase = null;
+ phase.removed(readyNodes);
+ return !readyNodes.isEmpty();
+ }
+ }
+
+ private NameProvider getNameProvider(String name) {
+ NameProvider nameProvider = nameProviders.get(name);
+ if (nameProvider != null)
+ return nameProvider;
+ else {
+ nameProvider = new ComponentNameProvider(name);
+ nameProviders.put(name, nameProvider);
+ return nameProvider;
+ }
+ }
+
+ private OrderedReadyNodes getReadyNodes() {
+ OrderedReadyNodes readyNodes = new OrderedReadyNodes();
+ for (Node node : nameProviders.values() ) {
+ if (node.ready())
+ readyNodes.add(node);
+ }
+ return readyNodes;
+ }
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNameProvider.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNameProvider.java
new file mode 100644
index 00000000000..77e492395ce
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNameProvider.java
@@ -0,0 +1,63 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import com.yahoo.component.chain.ChainedComponent;
+
+/**
+ * A set of components providing a given name.
+ *
+ * @author tonytv
+ */
+class ComponentNameProvider extends NameProvider {
+
+ @SuppressWarnings("rawtypes")
+ private Set<ComponentNode> nodes = new LinkedHashSet<>();
+ private Logger logger = Logger.getLogger(getClass().getName());
+
+ ComponentNameProvider(String name) {
+ super(name, 0);
+ }
+
+ protected void addNode(@SuppressWarnings("rawtypes") ComponentNode componentNode) {
+ if (nodes.add(componentNode))
+ componentNode.notifyAfter();
+ }
+
+ @Override
+ protected void handleRemoved(OrderedReadyNodes readyNodes) {
+ for (Node node: nodes) {
+ /*
+ All providers must be run before dependencies are run.
+ Adding these dependencies just in time improves dot output
+ for the purpose of finding cycles manually.
+ */
+ for (Node afterThis : nodesAfterThis) {
+ node.before(afterThis);
+ }
+ node.beforeRemoved(readyNodes);
+ }
+ }
+
+ @Override
+ int classPriority() {
+ return 1;
+ }
+
+ public @Override String toString() {
+ StringBuilder b=new StringBuilder("components [");
+ for (@SuppressWarnings("rawtypes")
+ Iterator<ComponentNode> i=nodes.iterator(); i.hasNext(); ) {
+ b.append(i.next().getComponent().getId());
+ if (i.hasNext())
+ b.append(", ");
+ }
+ b.append("]");
+ return b.toString();
+ }
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNode.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNode.java
new file mode 100644
index 00000000000..f6b62aec741
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ComponentNode.java
@@ -0,0 +1,35 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+import com.yahoo.component.chain.ChainedComponent;
+
+/**
+ * A node representing a given component.
+ *
+ * @see Node
+ * @author tonytv
+ */
+class ComponentNode<T extends ChainedComponent> extends Node {
+ private T component;
+
+ public ComponentNode(T component, int priority) {
+ super(priority);
+ this.component = component;
+ }
+
+ T getComponent() {
+ return component;
+ }
+
+ @Override
+ protected String dotName() {
+ //TODO: config dependent name
+ return component.getClass().getSimpleName();
+ }
+
+ @Override
+ int classPriority() {
+ return 2;
+ }
+}
+
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ConflictingNodeTypeException.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ConflictingNodeTypeException.java
new file mode 100644
index 00000000000..75b4025eef0
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/ConflictingNodeTypeException.java
@@ -0,0 +1,16 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+/**
+ * Thrown if a searcher provides the same name as a phase.
+ *
+ * @author tonytv
+ */
+@SuppressWarnings("serial")
+public class ConflictingNodeTypeException extends RuntimeException {
+
+ public ConflictingNodeTypeException(String message) {
+ super(message);
+ }
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/CycleDependenciesException.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/CycleDependenciesException.java
new file mode 100644
index 00000000000..09f8d7eb914
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/CycleDependenciesException.java
@@ -0,0 +1,45 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Signals that the dependency graph contains cycles. A DOT language
+ * representation of the cycle is available to help solve the problem (<a
+ * href="http://graphviz.org/">GraphViz</a>).
+ *
+ * @author tonytv
+ */
+@SuppressWarnings("serial")
+public class CycleDependenciesException extends RuntimeException {
+ public Map<String, NameProvider> cycleNodes;
+
+ CycleDependenciesException(Map<String, NameProvider> cycleNodes) {
+ super("The following set of dependencies lead to a cycle:\n"
+ + createDotString(cycleNodes));
+ this.cycleNodes = cycleNodes;
+ }
+
+ private static String createDotString(Map<String, NameProvider> cycleNodes) {
+ StringBuilder res = new StringBuilder();
+ res.append("digraph dependencyGraph {\n");
+
+ Set<Node> used = new HashSet<>();
+ for (Node node: cycleNodes.values()) {
+ if (!node.ready())
+ node.dotDependenciesString(res, used);
+
+ }
+ res.append("}");
+ return res.toString();
+ }
+
+
+ public String dotString() {
+ return createDotString(cycleNodes);
+ }
+
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/NameProvider.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/NameProvider.java
new file mode 100644
index 00000000000..d914fa489e9
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/NameProvider.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.chain.dependencies.ordering;
+
+/**
+ * A node containing nodes providing a given name.
+ *
+ * @author tonytv
+ */
+abstract class NameProvider extends Node {
+ final String name;
+
+ public NameProvider(String name, int priority) {
+ super(priority);
+ this.name = name;
+ }
+
+ protected abstract void addNode(ComponentNode<?> node);
+
+ protected String name() {
+ return name;
+ }
+
+ @Override
+ protected String dotName() {
+ return name;
+ }
+}
+
+
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/Node.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/Node.java
new file mode 100644
index 00000000000..7d2a7e112e4
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/Node.java
@@ -0,0 +1,85 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+import java.util.HashSet;
+import java.util.Set;
+
+/**
+ * A node in a dependency graph.
+ *
+ * Dependencies must declared as follows:
+ * a.before(b) , where a,b are nodes.
+ *
+ * The following dependencies are currently allowed:
+ * searcher.before(name)
+ * name.before(searcher)
+ * searcher1.before(searcher2)
+ *
+ * Where name designates a NameProvider( either a phase or a set of searchers).
+ *
+ * @author tonytv
+*/
+abstract class Node {
+ //How this node should be prioritized if its compared with a node of the same class, see class priority.
+ final int priority;
+
+ private int numNodesBeforeThis = 0;
+ Set<Node> nodesAfterThis = new HashSet<>();
+
+ public Node(int priority) {
+ this.priority = priority;
+ }
+
+ protected void before(Node node) {
+ if (nodesAfterThis.add(node)) {
+ node.notifyAfter();
+ }
+ }
+
+ void notifyAfter() {
+ ++numNodesBeforeThis;
+ }
+
+ void removed(OrderedReadyNodes readyNodes) {
+ handleRemoved(readyNodes);
+ for (Node node: nodesAfterThis) {
+ node.beforeRemoved(readyNodes);
+ }
+ }
+
+ void beforeRemoved(OrderedReadyNodes readyNodes) {
+ --numNodesBeforeThis;
+
+ if (ready()) {
+ readyNodes.add(this);
+ }
+ }
+
+ boolean ready() {
+ return numNodesBeforeThis == 0;
+ }
+
+ protected void handleRemoved(OrderedReadyNodes readyNodes) {}
+
+ void dotDependenciesString(StringBuilder s, Set<Node> used) {
+ if (used.contains(this))
+ return;
+ used.add(this);
+
+ for (Node afterNode : nodesAfterThis) {
+ String indent = " ";
+ s.append(indent);
+ s.append(dotName()).append(" -> ").append(afterNode.dotName())
+ .append('\n');
+ afterNode.dotDependenciesString(s, used);
+ }
+ }
+
+ abstract protected String dotName();
+
+ /*
+ * Ensures that PhaseNameProviders < ComponentNameProviders < ComponentNodes
+ * The regular priority is only considered if the class priorities are equal.
+ */
+ abstract int classPriority();
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodes.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodes.java
new file mode 100644
index 00000000000..f0b12e26a1b
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodes.java
@@ -0,0 +1,39 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+
+import java.util.Comparator;
+import java.util.PriorityQueue;
+
+/**
+ * Ensures that Searchers are ordered deterministically.
+ *
+ * @author tonytv
+ */
+class OrderedReadyNodes {
+ private class PriorityComparator implements Comparator<Node> {
+ @Override
+ public int compare(Node lhs, Node rhs) {
+ int result = new Integer(lhs.classPriority()).compareTo(rhs.classPriority());
+
+ return result != 0 ?
+ result :
+ new Integer(lhs.priority).compareTo(rhs.priority);
+ }
+ }
+
+ final private PriorityQueue<Node> nodes =
+ new PriorityQueue<>(10, new PriorityComparator());
+
+ public void add(Node node) {
+ nodes.add(node);
+ }
+
+ public Node pop() {
+ return nodes.poll();
+ }
+
+ public boolean isEmpty() {
+ return nodes.isEmpty();
+ }
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/PhaseNameProvider.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/PhaseNameProvider.java
new file mode 100644
index 00000000000..f72a18d5ce6
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/ordering/PhaseNameProvider.java
@@ -0,0 +1,28 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+/**
+ * A phase providing a given name.
+ *
+ * @author tonytv
+ */
+class PhaseNameProvider extends NameProvider {
+ public PhaseNameProvider(String name, int priority) {
+ super(name,priority);
+ }
+
+ protected void addNode(ComponentNode<?> newNode) {
+ throw new ConflictingNodeTypeException("Both a phase and a searcher provides the name '" + name + "'");
+ }
+
+ @Override
+ public String toString() {
+ return getClass().getSimpleName() + "[name = " + name + "]";
+ }
+
+
+ @Override
+ int classPriority() {
+ return 0;
+ }
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/dependencies/package-info.java b/chain/src/main/java/com/yahoo/component/chain/dependencies/package-info.java
new file mode 100644
index 00000000000..ce64ef8ffab
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/dependencies/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.chain.dependencies;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ChainSpecification.java b/chain/src/main/java/com/yahoo/component/chain/model/ChainSpecification.java
new file mode 100644
index 00000000000..8a5b907abdd
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/model/ChainSpecification.java
@@ -0,0 +1,221 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.model;
+
+import com.google.common.collect.ImmutableSet;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.chain.Phase;
+import net.jcip.annotations.Immutable;
+
+import java.util.*;
+
+/**
+ * Specifies how the components should be selected to create a chain.
+ *
+ * @author tonytv
+ */
+@Immutable
+public class ChainSpecification {
+ public static class Inheritance {
+ public final Set<ComponentSpecification> chainSpecifications;
+ public final Set<ComponentSpecification> excludedComponents;
+
+ Inheritance flattened() {
+ return new Inheritance(Collections.<ComponentSpecification>emptySet(), excludedComponents);
+ }
+
+ public Inheritance(Set<ComponentSpecification> inheritedChains, Set<ComponentSpecification> excludedComponents) {
+ this.chainSpecifications = immutableCopy(inheritedChains);
+ this.excludedComponents = immutableCopy(excludedComponents);
+ }
+
+ public Inheritance addInherits(Collection<ComponentSpecification> inheritedChains) {
+ Set<ComponentSpecification> newChainSpecifications =
+ new LinkedHashSet<>(chainSpecifications);
+ newChainSpecifications.addAll(inheritedChains);
+ return new Inheritance(newChainSpecifications, excludedComponents);
+ }
+ }
+
+ public final ComponentId componentId;
+ public final Inheritance inheritance;
+ final Map<String, Phase> phases;
+ public final Set<ComponentSpecification> componentReferences;
+
+ public ChainSpecification(ComponentId componentId, Inheritance inheritance,
+ Collection<Phase> phases,
+ Set<ComponentSpecification> componentReferences) {
+ assertNotNull(componentId, inheritance, phases, componentReferences);
+
+ if (componentsByName(componentReferences).size() != componentReferences.size())
+ throw new RuntimeException("Two components with the same name are specified in '" + componentId +
+ "', but name must be unique inside a given chain.");
+
+ this.componentId = componentId;
+ this.inheritance = inheritance;
+ this.phases = copyPhasesImmutable(phases);
+ this.componentReferences = ImmutableSet.copyOf(
+ filterByComponentSpecification(componentReferences, inheritance.excludedComponents));
+ }
+
+ public ChainSpecification addComponents(Collection<ComponentSpecification> componentSpecifications) {
+ Set<ComponentSpecification> newComponentReferences = new LinkedHashSet<>(componentReferences);
+ newComponentReferences.addAll(componentSpecifications);
+
+ return new ChainSpecification(componentId, inheritance, phases(), newComponentReferences);
+ }
+
+ public ChainSpecification addInherits(Collection<ComponentSpecification> inheritedChains) {
+ return new ChainSpecification(componentId, inheritance.addInherits(inheritedChains), phases(), componentReferences);
+ }
+
+ public ChainSpecification setComponentId(ComponentId newComponentId) {
+ return new ChainSpecification(newComponentId, inheritance, phases(), componentReferences);
+ }
+
+ public ChainSpecification flatten(Resolver<ChainSpecification> allChainSpecifications) {
+ Deque<ComponentId> path = new ArrayDeque<>();
+ return flatten(allChainSpecifications, path);
+ }
+
+ /**
+ * @param allChainSpecifications resolves ChainSpecifications from ComponentSpecifications
+ * as given in the inheritance fields.
+ * @param path tracks which chains are used in each recursive invocation of flatten, used for detecting cycles.
+ * @return ChainSpecification directly containing all the component references and phases of the inherited chains.
+ */
+ private ChainSpecification flatten(Resolver<ChainSpecification> allChainSpecifications,
+ Deque<ComponentId> path) {
+ path.push(componentId);
+
+ //if this turns out to be a bottleneck(which I seriously doubt), please add memoization
+ Map<String, ComponentSpecification> resultingComponents = componentsByName(componentReferences);
+ Map<String, Phase> resultingPhases = new LinkedHashMap<>(phases);
+
+
+ for (ComponentSpecification inheritedChainSpecification : inheritance.chainSpecifications) {
+ ChainSpecification inheritedChain =
+ resolveChain(path, allChainSpecifications, inheritedChainSpecification).
+ flatten(allChainSpecifications, path);
+
+ mergeInto(resultingComponents,
+ filterByComponentSpecification(
+ filterByName(inheritedChain.componentReferences, names(componentReferences)),
+ inheritance.excludedComponents));
+ mergeInto(resultingPhases, inheritedChain.phases);
+ }
+
+ path.pop();
+ return new ChainSpecification(componentId, inheritance.flattened(), resultingPhases.values(),
+ new LinkedHashSet<>(resultingComponents.values()));
+ }
+
+ public Collection<Phase> phases() {
+ return phases.values();
+ }
+
+ private static <T> Set<T> immutableCopy(Set<T> set) {
+ if (set == null) return ImmutableSet.of();
+ return ImmutableSet.copyOf(set);
+ }
+
+ private static Map<String, Phase> copyPhasesImmutable(Collection<Phase> phases) {
+ Map<String, Phase> result = new LinkedHashMap<>();
+ for (Phase phase : phases) {
+ Phase oldValue = result.put(phase.getName(), phase);
+ if (oldValue != null)
+ throw new RuntimeException("Two phases with the same name " + phase.getName() + " present in the same scope.");
+ }
+ return Collections.unmodifiableMap(result);
+ }
+
+ private static void assertNotNull(Object... objects) {
+ for (Object o : objects) {
+ assert(o != null);
+ }
+ }
+
+ static Map<String, ComponentSpecification> componentsByName(Set<ComponentSpecification> componentSpecifications) {
+ Map<String, ComponentSpecification> componentsByName = new LinkedHashMap<>();
+
+ for (ComponentSpecification component : componentSpecifications)
+ componentsByName.put(component.getName(), component);
+
+ return componentsByName;
+ }
+
+ private static void mergeInto(Map<String, ComponentSpecification> resultingComponents,
+ Set<ComponentSpecification> components) {
+ for (ComponentSpecification component : components) {
+ String name = component.getName();
+ if (resultingComponents.containsKey(name)) {
+ resultingComponents.put(name, component.intersect(resultingComponents.get(name)));
+ } else {
+ resultingComponents.put(name, component);
+ }
+ }
+ }
+
+
+ private static void mergeInto(Map<String, Phase> resultingPhases, Map<String, Phase> phases) {
+ for (Phase phase : phases.values()) {
+ String name = phase.getName();
+ if (resultingPhases.containsKey(name)) {
+ phase = phase.union(resultingPhases.get(name));
+ }
+ resultingPhases.put(name, phase);
+ }
+ }
+
+ private static Set<String> names(Set<ComponentSpecification> components) {
+ Set<String> names = new LinkedHashSet<>();
+ for (ComponentSpecification component : components) {
+ names.add(component.getName());
+ }
+ return names;
+ }
+
+ private static Set<ComponentSpecification> filterByComponentSpecification(Set<ComponentSpecification> components, Set<ComponentSpecification> excludes) {
+ Set<ComponentSpecification> result = new LinkedHashSet<>();
+ for (ComponentSpecification component : components) {
+ if (!matches(component, excludes))
+ result.add(component);
+ }
+
+ return result;
+ }
+
+ private static Set<ComponentSpecification> filterByName(Set<ComponentSpecification> components, Set<String> names) {
+ Set<ComponentSpecification> result = new LinkedHashSet<>();
+ for (ComponentSpecification component : components) {
+ if (!names.contains(component.getName()))
+ result.add(component);
+ }
+ return result;
+ }
+
+ private static boolean matches(ComponentSpecification component, Set<ComponentSpecification> excludes) {
+ ComponentId id = component.toId().withoutNamespace();
+ for (ComponentSpecification exclude : excludes) {
+ if (exclude.matches(id)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private ChainSpecification resolveChain(Deque<ComponentId> path,
+ Resolver<ChainSpecification> allChainSpecifications,
+ ComponentSpecification chainSpecification) {
+
+ ChainSpecification chain = allChainSpecifications.resolve(chainSpecification);
+ if (chain == null) {
+ throw new RuntimeException("Missing chain '" + chainSpecification + "'.");
+ } else if (path.contains(chain.componentId)) {
+ throw new RuntimeException("The chain " + chain.componentId + " inherits(possibly indirectly) from itself.");
+ } else {
+ return chain;
+ }
+ }
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ChainedComponentModel.java b/chain/src/main/java/com/yahoo/component/chain/model/ChainedComponentModel.java
new file mode 100644
index 00000000000..be94bd4973d
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/model/ChainedComponentModel.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.model;
+
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.osgi.provider.model.ComponentModel;
+import net.jcip.annotations.Immutable;
+
+/**
+ * Describes how a chained component should be created.
+ *
+ * @author <a href="mailto:arnebef@yahoo-inc.com">Arne Bergene Fossaa</a>
+ * @author tonytv
+ */
+@Immutable
+public class ChainedComponentModel extends ComponentModel {
+ public final Dependencies dependencies;
+
+ public ChainedComponentModel(BundleInstantiationSpecification bundleInstantiationSpec, Dependencies dependencies,
+ String configId) {
+ super(bundleInstantiationSpec, configId);
+ assert(dependencies != null);
+
+ this.dependencies = dependencies;
+ }
+
+ public ChainedComponentModel(BundleInstantiationSpecification bundleInstantiationSpec, Dependencies dependencies) {
+ this(bundleInstantiationSpec, dependencies, null);
+ }
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ChainsModel.java b/chain/src/main/java/com/yahoo/component/chain/model/ChainsModel.java
new file mode 100644
index 00000000000..6b92c8899e5
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/model/ChainsModel.java
@@ -0,0 +1,83 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.model;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Map;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.component.provider.ComponentRegistry;
+
+/**
+ * A model of how the chains and components should be created.
+ *
+ * @author tonytv
+ */
+public class ChainsModel {
+
+ private final ComponentRegistry<ComponentAdaptor<ChainSpecification>> chainSpecifications = new ComponentRegistry<>();
+ private final ComponentRegistry<ComponentAdaptor<ChainedComponentModel>> componentModels = new ComponentRegistry<>();
+
+ public void register(ChainSpecification chainSpecification) {
+ chainSpecifications.register(chainSpecification.componentId,
+ ComponentAdaptor.create(chainSpecification.componentId, chainSpecification));
+ }
+
+ public void register(ComponentId globalComponentId, ChainedComponentModel componentModel) {
+ assert (componentModel.getComponentId().withoutNamespace().equals(
+ globalComponentId.withoutNamespace()));
+
+ componentModels.register(globalComponentId, ComponentAdaptor.create(globalComponentId, componentModel));
+ }
+
+ public Collection<ChainedComponentModel> allComponents() {
+ Collection<ChainedComponentModel> components = new ArrayList<>();
+ for (ComponentAdaptor<ChainedComponentModel> component : componentModels.allComponents()) {
+ components.add(component.model);
+ }
+ return components;
+ }
+
+ public Collection<ChainSpecification> allChainsFlattened() {
+ Resolver<ChainSpecification> resolver = new Resolver<ChainSpecification>() {
+ @Override
+ public ChainSpecification resolve(ComponentSpecification componentSpecification) {
+ ComponentAdaptor<ChainSpecification> spec = chainSpecifications.getComponent(componentSpecification);
+ return (spec==null) ? null : spec.model;
+ }
+ };
+
+ Collection<ChainSpecification> chains = new ArrayList<>();
+ for (ComponentAdaptor<ChainSpecification> chain : chainSpecifications.allComponents()) {
+ chains.add(chain.model.flatten(resolver));
+ }
+ return chains;
+ }
+
+ public void validate() {
+ allChainsFlattened();
+ for (ComponentAdaptor<ChainSpecification> chain : chainSpecifications.allComponents()) {
+ validate(chain.model);
+ }
+ }
+
+ private void validate(ChainSpecification model) {
+ for (ComponentSpecification componentSpec : model.componentReferences) {
+ if (componentModels.getComponent(componentSpec) == null) {
+ throw new RuntimeException("No component matching the component specification " + componentSpec);
+ }
+ }
+ }
+
+ // For testing
+ Map<ComponentId, ComponentAdaptor<ChainSpecification>> chainSpecifications() {
+ return chainSpecifications.allComponentsById();
+ }
+
+ // For testing
+ Map<ComponentId, ComponentAdaptor<ChainedComponentModel>> componentModels() {
+ return componentModels.allComponentsById();
+ }
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ChainsModelBuilder.java b/chain/src/main/java/com/yahoo/component/chain/model/ChainsModelBuilder.java
new file mode 100644
index 00000000000..b656829ffbd
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/model/ChainsModelBuilder.java
@@ -0,0 +1,84 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.model;
+
+import java.util.*;
+
+import com.yahoo.container.bundle.BundleInstantiationSpecification;
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+
+import com.yahoo.component.chain.dependencies.Dependencies;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.container.core.ChainsConfig;
+
+/**
+ * Builds a chains model from config.
+ *
+ * @author tonytv
+ */
+public class ChainsModelBuilder {
+
+ public static ChainsModel buildFromConfig(ChainsConfig chainsConfig) {
+ ChainsModel model = createChainsModel(chainsConfig);
+
+ for (ChainsConfig.Components component : chainsConfig.components()) {
+ ChainedComponentModel componentModel = createChainedComponentModel(component);
+ model.register(componentModel.getComponentId(), componentModel);
+ }
+ return model;
+ }
+
+ private static ChainedComponentModel createChainedComponentModel(ChainsConfig.Components component) {
+ return new ChainedComponentModel(
+ new BundleInstantiationSpecification(new ComponentSpecification(component.id()), null, null),
+ createDependencies(
+ component.dependencies().provides(),
+ component.dependencies().before(),
+ component.dependencies().after()),
+ null);
+ }
+
+ private static ChainsModel createChainsModel(ChainsConfig chainsConfig) {
+ ChainsModel model = new ChainsModel();
+ for (ChainsConfig.Chains chainConfig : chainsConfig.chains()) {
+ model.register(
+ createChainSpecification(chainConfig));
+ }
+ return model;
+ }
+
+ private static ChainSpecification createChainSpecification(ChainsConfig.Chains config) {
+ return new ChainSpecification(new ComponentId(config.id()),
+ createInheritance(config.inherits(), config.excludes()),
+ createPhases(config.phases()),
+ createComponentSpecifications(config.components()));
+ }
+
+ private static Collection<Phase> createPhases(List<ChainsConfig.Chains.Phases> phases) {
+ Collection<Phase> result = new ArrayList<>();
+ for (ChainsConfig.Chains.Phases phase : phases) {
+ result.add(
+ new Phase(phase.id(), createDependencies(null, phase.before(), phase.after())));
+ }
+ return result;
+ }
+
+ private static Dependencies createDependencies(List<String> provides,
+ List<String> before, List<String> after) {
+ return new Dependencies(provides, before, after);
+ }
+
+ private static Set<ComponentSpecification> createComponentSpecifications(List<String> stringSpecs) {
+ Set<ComponentSpecification> specifications = new LinkedHashSet<>();
+ for (String stringSpec : stringSpecs) {
+ specifications.add(new ComponentSpecification(stringSpec));
+ }
+ return specifications;
+ }
+
+ private static ChainSpecification.Inheritance createInheritance(List<String> inherit, List<String> exclude) {
+ return new ChainSpecification.Inheritance(
+ createComponentSpecifications(inherit),
+ createComponentSpecifications(exclude));
+ }
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/model/ComponentAdaptor.java b/chain/src/main/java/com/yahoo/component/chain/model/ComponentAdaptor.java
new file mode 100644
index 00000000000..1aa96f1fcf3
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/model/ComponentAdaptor.java
@@ -0,0 +1,31 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.model;
+
+import com.yahoo.component.AbstractComponent;
+import com.yahoo.component.ComponentId;
+
+/**
+ * For using non-component model classes with ComponentRegistry.
+ *
+ * @author tonytv
+ */
+public final class ComponentAdaptor<T> extends AbstractComponent {
+
+ public final T model;
+
+ @SuppressWarnings("deprecation")
+ public ComponentAdaptor(ComponentId globalComponentId, T model) {
+ super(globalComponentId);
+ this.model = model;
+ }
+
+ public static <T> ComponentAdaptor<T> create(ComponentId globalComponentId, T model) {
+ return new ComponentAdaptor<>(globalComponentId, model);
+ }
+
+ // For testing
+ T model() {
+ return model;
+ }
+
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/model/Resolver.java b/chain/src/main/java/com/yahoo/component/chain/model/Resolver.java
new file mode 100644
index 00000000000..0f848cafea3
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/model/Resolver.java
@@ -0,0 +1,13 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.model;
+
+import com.yahoo.component.ComponentSpecification;
+
+/**
+ * Maps component specifications to matching instances.
+ *
+ * @author tonytv
+ */
+public interface Resolver<T> {
+ T resolve(ComponentSpecification componentSpecification);
+}
diff --git a/chain/src/main/java/com/yahoo/component/chain/model/package-info.java b/chain/src/main/java/com/yahoo/component/chain/model/package-info.java
new file mode 100644
index 00000000000..d1c5d5f0d76
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/model/package-info.java
@@ -0,0 +1,5 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+@ExportPackage
+package com.yahoo.component.chain.model;
+
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/chain/src/main/java/com/yahoo/component/chain/package-info.java b/chain/src/main/java/com/yahoo/component/chain/package-info.java
new file mode 100644
index 00000000000..a8b1981ef98
--- /dev/null
+++ b/chain/src/main/java/com/yahoo/component/chain/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.chain;
+
+import com.yahoo.api.annotations.PublicApi;
+import com.yahoo.osgi.annotation.ExportPackage;
diff --git a/chain/src/main/resources/configdefinitions/chains.def b/chain/src/main/resources/configdefinitions/chains.def
new file mode 100644
index 00000000000..c0ea1ef7d85
--- /dev/null
+++ b/chain/src/main/resources/configdefinitions/chains.def
@@ -0,0 +1,43 @@
+# Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+# Chains configuration
+version=13
+namespace=container.core
+
+components[].id string
+
+# Configured functionality provided by this - comes in addition to those set in the code
+components[].dependencies.provides[] string
+
+# Configured "before" dependencies provided by this - comes in addition to those set in the code
+components[].dependencies.before[] string
+
+# Configured "after" dependencies provided by this - comes in addition to those set in the code
+components[].dependencies.after[] string
+
+# The id of this chain. The id has the form name(:version)?
+# where the version has the form 1(.2(.3(.identifier)?)?)?.
+# The default chain must be called "default".
+chains[].id string
+
+#The type of this chain
+chains[].type enum {DOCPROC, SEARCH} default=SEARCH
+
+# The id of a component to include in this chain.
+# The id has the form fullclassname(:version)?
+# where the version has the form 1(.2(.3(.identifier)?)?)?.
+chains[].components[] string
+
+# The optional list of chain ids this inherits.
+# The ids has the form name(:version)?
+# where the version has the form 1(.2(.3(.identifier)?)?)?.
+# If the version is not specified the newest version is used.
+chains[].inherits[] string
+
+# The optional list of component ids to exclude from this chain even if they exists in inherited chains
+# If versions are specified in these ids, they are ignored.
+chains[].excludes[] string
+
+# The phases for a chain
+chains[].phases[].id string
+chains[].phases[].before[] string
+chains[].phases[].after[] string
diff --git a/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilderTest.java b/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilderTest.java
new file mode 100644
index 00000000000..0695dcd7cff
--- /dev/null
+++ b/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/ChainBuilderTest.java
@@ -0,0 +1,248 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.chain.Chain;
+import com.yahoo.component.chain.ChainedComponent;
+import com.yahoo.component.chain.Phase;
+import com.yahoo.component.chain.dependencies.After;
+import com.yahoo.component.chain.dependencies.Before;
+import com.yahoo.component.chain.dependencies.Provides;
+import org.junit.Test;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author tonytv
+ * @since 5.1.10
+ */
+@SuppressWarnings({"rawtypes", "unchecked"})
+public class ChainBuilderTest {
+
+ private void addAtoG(ChainBuilder chainBuilder)
+ throws IllegalAccessException, InstantiationException {
+
+ List<Class<? extends ChainedComponent>> componentTypes = new ArrayList<>();
+
+ componentTypes.add(A.class);
+ componentTypes.add(B.class);
+ componentTypes.add(C.class);
+ componentTypes.add(D.class);
+ componentTypes.add(E.class);
+ componentTypes.add(F.class);
+ componentTypes.add(G.class);
+
+ permute(componentTypes);
+
+ for (Class<? extends ChainedComponent> searcherClass : componentTypes) {
+ chainBuilder.addComponent(searcherClass.newInstance());
+ }
+ }
+
+
+ private void permute(List<Class<? extends ChainedComponent>> searcherTypes) {
+ for (int i=0; i<searcherTypes.size(); ++i) {
+ int j = (int) (Math.random() * searcherTypes.size());
+ Class<? extends ChainedComponent> tmp = searcherTypes.get(i);
+ searcherTypes.set(i,searcherTypes.get(j));
+ searcherTypes.set(j, tmp);
+ }
+ }
+
+ @Test
+ public void testRegular() throws InstantiationException, IllegalAccessException {
+ ChainBuilder chainBuilder = createDependencyHandler();
+
+ addAtoG(chainBuilder);
+
+ Chain<ChainedComponent> res = chainBuilder.orderNodes();
+
+ Iterator<ChainedComponent> i = res.components().iterator();
+ for (char j=0; j< 'G' - 'A'; ++j) {
+ assertEquals(String.valueOf((char)('A' + j)), name(i.next()));
+ }
+ }
+
+ @Test
+ public void testCycle()
+ throws InstantiationException, IllegalAccessException {
+
+ ChainBuilder chainBuilder = createDependencyHandler();
+
+ addAtoG(chainBuilder);
+ chainBuilder.addComponent(new H());
+
+ boolean cycle = false;
+ try {
+ chainBuilder.orderNodes();
+ } catch (CycleDependenciesException e) {
+ cycle = true;
+ }
+ assertTrue(cycle);
+ }
+
+
+ @Test
+ public void testPhaseAndSearcher() {
+ ChainBuilder depHandler = newChainBuilder();
+ depHandler.addPhase(new Phase("phase1", set("phase2"), Collections.<String>emptySet()));
+ depHandler.addPhase(new Phase("phase2", set("phase3"), set("phase1")));
+ depHandler.addPhase(new Phase("phase3", Collections.<String>emptySet(), set("phase2", "phase1")));
+ ChainedComponent first = new First();
+ ChainedComponent second = new Second();
+
+ depHandler.addComponent(first);
+ depHandler.addComponent(second);
+ assertEquals(depHandler.orderNodes().components(), Arrays.asList(first, second));
+
+ }
+
+ @Test
+ public void testInputOrderPreservedWhenProvidesOverlap() {
+ ChainBuilder chainBuilder = newChainBuilder();
+
+ A a1 = new A();
+ C c = new C();
+ A a2 = new A();
+
+ chainBuilder.addComponent(a1);
+ chainBuilder.addComponent(c);
+ chainBuilder.addComponent(a2);
+
+ assertEquals(Arrays.asList(a1, c, a2), chainBuilder.orderNodes().components());
+ }
+
+ private ChainBuilder newChainBuilder() {
+ return new ChainBuilder(new ComponentId("test"));
+ }
+
+ private Set<String> set(String... strings) {
+ return new HashSet<>(Arrays.asList(strings));
+ }
+
+ @Before("phase1")
+ static class First extends NoopComponent {
+
+ }
+
+ @After("phase3")
+ static class Second extends NoopComponent {
+
+ }
+
+ @Test
+ public void testAfterAll1()
+ throws InstantiationException, IllegalAccessException {
+ ChainBuilder chainBuilder = createDependencyHandler();
+ ChainedComponent afterAll1 = new AfterAll();
+ chainBuilder.addComponent(afterAll1);
+ addAtoG(chainBuilder);
+
+ List<ChainedComponent> resolution= chainBuilder.orderNodes().components();
+ assertEquals(afterAll1,resolution.get(resolution.size()-1));
+ }
+
+ @Test
+ public void testAfterAll2()
+ throws InstantiationException, IllegalAccessException {
+ ChainBuilder chainBuilder = createDependencyHandler();
+ addAtoG(chainBuilder);
+ ChainedComponent afterAll1 = new AfterAll();
+ chainBuilder.addComponent(afterAll1);
+
+ List<ChainedComponent> resolution = chainBuilder.orderNodes().components();
+ assertEquals(afterAll1,resolution.get(resolution.size()-1));
+ }
+
+ @Test
+ public void testAfterImplicitProvides()
+ throws InstantiationException, IllegalAccessException {
+ ChainBuilder chainBuilder = createDependencyHandler();
+ ChainedComponent afterProvidesNothing=new AfterProvidesNothing();
+ ChainedComponent providesNothing=new ProvidesNothing();
+ chainBuilder.addComponent(afterProvidesNothing);
+ chainBuilder.addComponent(providesNothing);
+ List<ChainedComponent> resolution = chainBuilder.orderNodes().components();
+ assertEquals(providesNothing,resolution.get(0));
+ assertEquals(afterProvidesNothing,resolution.get(1));
+ }
+
+ private ChainBuilder createDependencyHandler() {
+ ChainBuilder chainBuilder = newChainBuilder();
+ chainBuilder.addPhase(new Phase("phase1", Collections.<String>emptySet(), Collections.<String>emptySet()));
+ chainBuilder.addPhase(new Phase("phase2", Collections.<String>emptySet(), Collections.<String>emptySet()));
+ chainBuilder.addPhase(new Phase("phase3", Collections.<String>emptySet(), Collections.<String>emptySet()));
+ return chainBuilder;
+ }
+
+ private String name(ChainedComponent searcher) {
+ return searcher.getClass().getSimpleName();
+ }
+
+ @Provides("A")
+ static class A extends NoopComponent {
+ }
+
+ @Provides("B")
+ @After("A")
+ @Before({"D", "phase1"})
+ static class B extends NoopComponent {
+ }
+
+ @Provides("C")
+ @After("phase1")
+ static class C extends NoopComponent {
+ }
+
+ @Provides("D")
+ @After({"C","A"})
+ static class D extends NoopComponent {
+ }
+
+ @Provides("E")
+ @After({"B","D"})
+ @Before("phase2")
+ static class E extends NoopComponent {
+ }
+
+ @Provides("F")
+ @After("phase2")
+ static class F extends NoopComponent {
+ }
+
+ @Provides("G")
+ @After("F")
+ static class G extends NoopComponent {
+ }
+
+ @Provides("H")
+ @Before("A")
+ @After("F")
+ static class H extends NoopComponent {
+ }
+
+ @Provides("AfterAll")
+ @After("*")
+ static class AfterAll extends NoopComponent {
+ }
+
+ static class ProvidesNothing extends NoopComponent {
+ }
+
+ @After("ProvidesNothing")
+ static class AfterProvidesNothing extends NoopComponent {
+ }
+
+ public static class NoopComponent extends ChainedComponent {
+ }
+
+}
diff --git a/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodesTest.java b/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodesTest.java
new file mode 100644
index 00000000000..9cb974a128e
--- /dev/null
+++ b/chain/src/test/java/com/yahoo/component/chain/dependencies/ordering/OrderedReadyNodesTest.java
@@ -0,0 +1,104 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.dependencies.ordering;
+
+import static org.junit.Assert.assertEquals;
+
+import java.util.Arrays;
+
+import com.yahoo.component.chain.ChainedComponent;
+import com.yahoo.component.chain.dependencies.Dependencies;
+import org.junit.Before;
+import org.junit.Test;
+
+import com.yahoo.component.ComponentId;
+
+
+
+/**
+ * Test for OrderedReadyNodes.
+ * @author tonytv
+ */
+@SuppressWarnings("rawtypes")
+public class OrderedReadyNodesTest {
+ class ComponentA extends ChainedComponent {
+ public ComponentA(ComponentId id) {
+ super(id);
+ }
+
+ @Override
+ public Dependencies getDependencies() {
+ return new Dependencies(Arrays.asList(getId().getName()), null, null);
+ }
+ }
+
+ class ComponentB extends ComponentA {
+ public ComponentB(ComponentId id) {
+ super(id);
+ }
+ }
+
+ private OrderedReadyNodes readyNodes;
+
+ @Before
+ public void setup() {
+ readyNodes = new OrderedReadyNodes();
+ }
+
+ @Test
+ public void require_NameProviders_before_SearcherNodes() {
+ NameProvider nameProvider = createDummyNameProvider(100);
+ ComponentNode componentNode = new ComponentNode<>(createFakeComponentA("a"), 1);
+
+ addNodes(nameProvider, componentNode);
+
+ assertEquals(nameProvider, pop());
+ assertEquals(componentNode, pop());
+ }
+
+ private NameProvider createDummyNameProvider(int priority) {
+ return new NameProvider("anonymous", priority) {
+ @Override
+ protected void addNode(ComponentNode node) {
+ throw new UnsupportedOperationException();
+ }
+
+ @Override
+ int classPriority() {
+ return 0;
+ }
+ };
+ }
+
+ @Test
+ public void require_SearcherNodes_ordered_by_insertion_order() {
+ int priority = 0;
+ ComponentNode a = new ComponentNode<>(createFakeComponentB("1"), priority++);
+ ComponentNode b = new ComponentNode<>(createFakeComponentA("2"), priority++);
+ ComponentNode c = new ComponentNode<>(createFakeComponentA("03"), priority++);
+
+ addNodes(a, b, c);
+
+ assertEquals(a, pop());
+ assertEquals(b, pop());
+ assertEquals(c, pop());
+ }
+
+ ChainedComponent createFakeComponentA(String id) {
+ return new ComponentA(ComponentId.fromString(id));
+ }
+
+ ChainedComponent createFakeComponentB(String id) {
+ return new ComponentB(ComponentId.fromString(id));
+ }
+
+
+ private void addNodes(Node... nodes) {
+ for (Node node : nodes) {
+ readyNodes.add(node);
+ }
+ }
+
+ private Node pop() {
+ return readyNodes.pop();
+ }
+}
diff --git a/chain/src/test/java/com/yahoo/component/chain/model/ChainsModelBuilderTest.java b/chain/src/test/java/com/yahoo/component/chain/model/ChainsModelBuilderTest.java
new file mode 100644
index 00000000000..a4e09dc90b7
--- /dev/null
+++ b/chain/src/test/java/com/yahoo/component/chain/model/ChainsModelBuilderTest.java
@@ -0,0 +1,72 @@
+// Copyright 2016 Yahoo Inc. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root.
+package com.yahoo.component.chain.model;
+
+import com.yahoo.component.ComponentId;
+import com.yahoo.component.ComponentSpecification;
+import com.yahoo.container.core.ChainsConfig;
+import org.junit.Test;
+
+import java.util.Map;
+import java.util.Set;
+
+import static com.yahoo.container.core.ChainsConfig.Components;
+import static com.yahoo.container.core.ChainsConfig.Chains;
+import static org.hamcrest.CoreMatchers.is;
+import static org.junit.Assert.assertThat;
+import static org.junit.Assert.assertTrue;
+
+/**
+ * @author gjoranv
+ * @since 5.1.10
+ */
+public class ChainsModelBuilderTest {
+
+ @Test
+ public void components_are_added_to_componentModels() throws Exception {
+ ChainsModel model = chainsModel();
+ assertThat(model.allComponents().size(), is(2));
+ assertTrue(model.componentModels().containsKey(new ComponentId("componentA")));
+ }
+
+ @Test
+ public void components_are_added_to_chainSpecification() throws Exception {
+ ChainsModel model = chainsModel();
+ ChainSpecification chainSpec = model.chainSpecifications().get(new ComponentId("chain1")).model();
+ assertTrue(getComponentsByName(chainSpec.componentReferences).containsKey("componentA"));
+ }
+
+ @Test
+ public void inherited_chains_are_added_to_chainSpecification() throws Exception {
+ ChainsModel model = chainsModel();
+ ChainSpecification inheritsChain1 = model.chainSpecifications().get(new ComponentId("inheritsChain1")).model();
+ assertThat(model.allChainsFlattened().size(), is(2));
+ assertTrue(getComponentsByName(inheritsChain1.inheritance.chainSpecifications).containsKey("chain1"));
+ assertTrue(getComponentsByName(inheritsChain1.inheritance.excludedComponents).containsKey("componentA"));
+ }
+
+ private ChainsModel chainsModel() {
+ ChainsConfig.Builder builder = new ChainsConfig.Builder()
+ .components(new Components.Builder()
+ .id("componentA"))
+ .components(new Components.Builder()
+ .id("componentB"))
+ .chains(new Chains.Builder()
+ .id("chain1")
+ .components("componentA")
+ .components("componentB"))
+ .chains(new Chains.Builder()
+ .id("inheritsChain1")
+ .inherits("chain1")
+ .excludes("componentA"));
+ ChainsConfig config = new ChainsConfig(builder);
+
+ ChainsModel model = ChainsModelBuilder.buildFromConfig(config);
+ model.validate();
+ return model;
+ }
+
+ private static Map<String, ComponentSpecification>
+ getComponentsByName(Set<ComponentSpecification> componentSpecifications) {
+ return ChainSpecification.componentsByName(componentSpecifications);
+ }
+}