// Copyright 2017 Yahoo Holdings. 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 java.util.*; /** * Specifies how the components should be selected to create a chain. Immutable. * * @author Tony Vaagenes */ public class ChainSpecification { public static class Inheritance { public final Set chainSpecifications; public final Set excludedComponents; Inheritance flattened() { return new Inheritance(Collections.emptySet(), excludedComponents); } public Inheritance(Set inheritedChains, Set excludedComponents) { this.chainSpecifications = immutableCopy(inheritedChains); this.excludedComponents = immutableCopy(excludedComponents); } public Inheritance addInherits(Collection inheritedChains) { Set newChainSpecifications = new LinkedHashSet<>(chainSpecifications); newChainSpecifications.addAll(inheritedChains); return new Inheritance(newChainSpecifications, excludedComponents); } } public final ComponentId componentId; public final Inheritance inheritance; final Map phases; public final Set componentReferences; public ChainSpecification(ComponentId componentId, Inheritance inheritance, Collection phases, Set 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 componentSpecifications) { Set newComponentReferences = new LinkedHashSet<>(componentReferences); newComponentReferences.addAll(componentSpecifications); return new ChainSpecification(componentId, inheritance, phases(), newComponentReferences); } public ChainSpecification addInherits(Collection 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 allChainSpecifications) { Deque 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 allChainSpecifications, Deque path) { path.push(componentId); //if this turns out to be a bottleneck(which I seriously doubt), please add memoization Map resultingComponents = componentsByName(componentReferences); Map 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 phases() { return phases.values(); } private static Set immutableCopy(Set set) { if (set == null) return ImmutableSet.of(); return ImmutableSet.copyOf(set); } private static Map copyPhasesImmutable(Collection phases) { Map 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 componentsByName(Set componentSpecifications) { Map componentsByName = new LinkedHashMap<>(); for (ComponentSpecification component : componentSpecifications) componentsByName.put(component.getName(), component); return componentsByName; } private static void mergeInto(Map resultingComponents, Set 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 resultingPhases, Map 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 names(Set components) { Set names = new LinkedHashSet<>(); for (ComponentSpecification component : components) { names.add(component.getName()); } return names; } private static Set filterByComponentSpecification(Set components, Set excludes) { Set result = new LinkedHashSet<>(); for (ComponentSpecification component : components) { if (!matches(component, excludes)) result.add(component); } return result; } private static Set filterByName(Set components, Set names) { Set result = new LinkedHashSet<>(); for (ComponentSpecification component : components) { if (!names.contains(component.getName())) result.add(component); } return result; } private static boolean matches(ComponentSpecification component, Set excludes) { ComponentId id = component.toId().withoutNamespace(); for (ComponentSpecification exclude : excludes) { if (exclude.matches(id)) { return true; } } return false; } private ChainSpecification resolveChain(Deque path, Resolver 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; } } }