org.trancecode.xproc.step.Step.java Source code

Java tutorial

Introduction

Here is the source code for org.trancecode.xproc.step.Step.java

Source

/*
 * Copyright (C) 2008 Herve Quiroz
 *
 * This library is free software; you can redistribute it and/or modify it under
 * the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation; either version 2.1 of the License, or (at your option)
 * any later version.
 * 
 * This library is distributed in the hope that it will be useful, but WITHOUT
 * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
 * FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more
 * details.
 * 
 * You should have received a copy of the GNU Lesser General Public License
 * along with this library; if not, write to the Free Software Foundation, Inc.,
 * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 
 *
 * $Id$
 */
package org.trancecode.xproc.step;

import com.google.common.base.Function;
import com.google.common.base.Preconditions;
import com.google.common.base.Predicate;
import com.google.common.base.Predicates;
import com.google.common.base.Supplier;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import com.google.common.collect.Maps;
import com.google.common.collect.Sets;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import net.sf.saxon.s9api.QName;
import net.sf.saxon.s9api.XdmNode;
import org.trancecode.api.Nullable;
import org.trancecode.api.ReturnsNullable;
import org.trancecode.collection.TcIterables;
import org.trancecode.collection.TcLists;
import org.trancecode.collection.TcMaps;
import org.trancecode.collection.TcSets;
import org.trancecode.lang.TcObjects;
import org.trancecode.logging.Logger;
import org.trancecode.xml.AbstractHasLocation;
import org.trancecode.xml.Location;
import org.trancecode.xml.saxon.SaxonQNames;
import org.trancecode.xproc.Environment;
import org.trancecode.xproc.XProcExceptions;
import org.trancecode.xproc.binding.DataPortBinding;
import org.trancecode.xproc.binding.DocumentPortBinding;
import org.trancecode.xproc.binding.PipePortBinding;
import org.trancecode.xproc.binding.PortBinding;
import org.trancecode.xproc.binding.PortBindingPredicates;
import org.trancecode.xproc.port.Port;
import org.trancecode.xproc.port.PortFunctions;
import org.trancecode.xproc.port.PortPredicates;
import org.trancecode.xproc.port.PortReference;
import org.trancecode.xproc.port.XProcPorts;
import org.trancecode.xproc.variable.Variable;

/**
 * @author Herve Quiroz
 */
public final class Step extends AbstractHasLocation implements StepContainer {
    private static final Logger LOG = Logger.getLogger(Step.class);
    private static final Map<QName, Variable> EMPTY_VARIABLE_LIST = ImmutableMap.of();
    private static final Map<QName, Variable> EMPTY_PARAMETER_MAP = ImmutableMap.of();
    private static final Map<String, Port> EMPTY_PORT_MAP = ImmutableMap.of();
    private static final List<Step> EMPTY_STEP_LIST = ImmutableList.of();
    private static final Iterable<Log> EMPTY_LOG_LIST = ImmutableList.of();

    private final Predicate<Port> PREDICATE_IS_XPATH_CONTEXT_PORT = port -> isXPathContextPort(port);

    private final Map<QName, Variable> parameters;
    private final Map<QName, Variable> variables;

    private final Map<String, Port> ports;

    private final XdmNode node;
    private final QName type;
    private final String name;
    private final String internalName;
    private final StepProcessor stepProcessor;
    private final List<Step> steps;
    private final boolean compoundStep;
    private final Iterable<Log> logs;

    private final Supplier<Integer> hashCode;
    private Map<Step, Iterable<Step>> dependencies;

    public static final class Log {
        private final String port;
        private final String href;

        private Log(final String port, @Nullable final String href) {
            this.port = Preconditions.checkNotNull(port);
            this.href = href;
        }

        public String getPort() {
            return this.port;
        }

        public String getHref() {
            return this.href;
        }

        @Override
        public int hashCode() {
            return TcObjects.hashCode(port, href);
        }

        @Override
        public boolean equals(final Object o) {
            if (o != null && o instanceof Log) {
                final Log other = (Log) o;
                return TcObjects.pairEquals(port, other.port, href, other.href);
            }

            return false;
        }

        @Override
        public String toString() {
            return String.format("p:log[%s = %s]", port, href);
        }
    }

    public static Step newStep(final QName type, final StepProcessor stepProcessor, final boolean compoundStep) {
        return new Step(null, type, null, null, null, stepProcessor, compoundStep, EMPTY_VARIABLE_LIST,
                EMPTY_PARAMETER_MAP, EMPTY_PORT_MAP, EMPTY_STEP_LIST, EMPTY_LOG_LIST);
    }

    public static Step newStep(final XdmNode node, final QName type, final StepProcessor stepProcessor,
            final boolean compoundStep) {
        return new Step(node, type, null, null, null, stepProcessor, compoundStep, EMPTY_VARIABLE_LIST,
                EMPTY_PARAMETER_MAP, EMPTY_PORT_MAP, EMPTY_STEP_LIST, EMPTY_LOG_LIST);
    }

    private Step(final XdmNode node, final QName type, final String name, final String internalName,
            final Location location, final StepProcessor stepProcessor, final boolean compoundStep,
            final Map<QName, Variable> variables, final Map<QName, Variable> parameters,
            final Map<String, Port> ports, final Iterable<Step> steps, final Iterable<Log> logs) {
        super(location);

        this.node = node;
        this.type = type;
        this.name = name;
        this.internalName = internalName;

        assert stepProcessor != null;
        this.stepProcessor = stepProcessor;

        this.compoundStep = compoundStep;

        this.variables = ImmutableMap.copyOf(variables);
        this.parameters = ImmutableMap.copyOf(parameters);
        this.ports = ImmutableMap.copyOf(ports);
        this.steps = ImmutableList.copyOf(steps);
        this.logs = ImmutableList.copyOf(logs);

        hashCode = TcObjects.immutableObjectHashCode(Step.class, node, type, name, location, stepProcessor,
                compoundStep, variables, parameters, ports, steps);
    }

    public boolean isPipelineStep() {
        return isCompoundStep() && !(getStepProcessor() instanceof CoreStepProcessor);
    }

    public Step setName(final String name) {
        LOG.trace("{@method} {} -> {}", this.name, name);

        if (TcObjects.equals(this.name, name)) {
            return this;
        }

        assert internalName == null : internalName;
        final String newInternalName;
        if (isPipelineStep()) {
            newInternalName = this.name;
            LOG.trace("newInternalName = {}", newInternalName);
        } else {
            newInternalName = null;
        }
        Step step = new Step(node, type, name, newInternalName, location, stepProcessor, compoundStep, variables,
                parameters, ports, steps, logs);
        for (final Port port : ports.values()) {
            step = step.withPort(port.setStepName(name));
        }

        return step;
    }

    public boolean isCompoundStep() {
        return compoundStep;
    }

    public Step declareVariable(final Variable variable) {
        if (variables.containsKey(variable.getName())) {
            throw XProcExceptions.xs0004(variable);
        }
        return new Step(node, type, name, internalName, location, stepProcessor, compoundStep,
                TcMaps.copyAndPut(variables, variable.getName(), variable), parameters, ports, steps, logs);
    }

    public Step declareVariables(final Map<QName, Variable> variables) {
        Step step = this;
        for (final Entry<QName, Variable> variable : variables.entrySet()) {
            step = step.declareVariable(variable.getValue());
        }

        return step;
    }

    public String getName() {
        return name;
    }

    @ReturnsNullable
    public String getInternalName() {
        assert internalName == null || isPipelineStep() : internalName;
        return internalName;
    }

    public Step declarePort(final Port port) {
        LOG.trace("{@method} step = {} ; port = {}", name, port);
        return declarePorts(ImmutableList.of(port));
    }

    public Step declarePorts(final Iterable<Port> ports) {
        LOG.trace("{@method} step = {} ; ports = {}", name, ports);

        final Map<String, Port> newPorts = Maps.newHashMap(this.ports);
        newPorts.putAll(Maps.uniqueIndex(ports, PortFunctions.getPortName()));

        return new Step(node, type, name, internalName, location, stepProcessor, compoundStep, variables,
                parameters, newPorts, steps, logs);
    }

    public Port getPort(final String name) {
        assert ports.containsKey(name) : "step = " + getName() + " ; port = " + name + " ; ports = "
                + ports.keySet();
        return ports.get(name);
    }

    public boolean hasPortDeclared(final String name) {
        return ports.containsKey(name);
    }

    public Map<String, Port> getPorts() {
        return ports;
    }

    private boolean isXPathContextPort(final Port port) {
        if (port.isInput() && !port.isParameter()) {
            if (port.getPortName().equals(XProcPorts.XPATH_CONTEXT)) {
                return true;
            }

            if (isPrimary(port)) {
                return !ports.containsKey(XProcPorts.XPATH_CONTEXT);
            }
        }

        return false;
    }

    public Environment run(final Environment environment) {
        LOG.trace("{@method} step = {} ; type = {}", name, type);
        return stepProcessor.run(this, environment);
    }

    @ReturnsNullable
    public Port getPrimaryInputPort() {
        final List<Port> inputPorts = ImmutableList.copyOf(getInputPorts(false));
        LOG.trace("{@method} inputPorts = {}", inputPorts);
        if (inputPorts.size() == 1) {
            final Port inputPort = Iterables.getOnlyElement(inputPorts);
            if (!inputPort.isNotPrimary()) {
                return inputPort;
            }
        }

        for (final Port inputPort : inputPorts) {
            if (inputPort.isPrimary()) {
                return inputPort;
            }
        }

        return null;
    }

    @ReturnsNullable
    public Port getPrimaryParameterPort() {
        final List<Port> parameterPorts = ImmutableList.copyOf(getParameterPorts());
        LOG.trace("parameterPorts = {}", parameterPorts);
        if (parameterPorts.size() == 1) {
            final Port parameterPort = Iterables.getOnlyElement(parameterPorts);
            if (!parameterPort.isNotPrimary()) {
                return parameterPort;
            }
        }

        for (final Port parameterPort : parameterPorts) {
            if (parameterPort.isPrimary()) {
                return parameterPort;
            }
        }

        return null;
    }

    @ReturnsNullable
    public Port getPrimaryOutputPort() {
        final List<Port> outputPorts = ImmutableList.copyOf(getOutputPorts());
        LOG.trace("outputPorts = {}", outputPorts);
        if (outputPorts.size() == 1) {
            final Port outputPort = Iterables.getOnlyElement(outputPorts);
            if (!outputPort.isNotPrimary()) {
                return outputPort;
            }
        }

        for (final Port outputPort : outputPorts) {
            if (outputPort.isPrimary()) {
                return outputPort;
            }
        }

        return null;
    }

    private boolean isPrimary(final Port port) {
        if (port.isParameter()) {
            return isPrimary(port, getParameterPorts());
        }

        if (port.isInput()) {
            return isPrimary(port, getInputPorts());
        }

        assert port.isOutput();
        return isPrimary(port, getOutputPorts());
    }

    private static boolean isPrimary(final Port port, final Iterable<Port> ports) {
        assert port != null;

        if (port.isNotPrimary()) {
            return false;
        }

        if (port.isPrimary()) {
            return true;
        }

        if (Iterables.size(ports) == 1) {
            return true;
        }

        return false;
    }

    public Iterable<Port> getInputPorts() {
        return getInputPorts(true);
    }

    public Iterable<Port> getInputPorts(final boolean includeParameterPorts) {
        if (includeParameterPorts) {
            return Iterables.filter(ports.values(), PortPredicates.isInputPort());
        } else {
            return Iterables.filter(ports.values(),
                    Predicates.and(PortPredicates.isInputPort(), Predicates.not(PortPredicates.isParameterPort())));
        }
    }

    public Iterable<Port> getOutputPorts() {
        return Iterables.filter(ports.values(), PortPredicates.isOutputPort());
    }

    public Iterable<Port> getParameterPorts() {
        return Iterables.filter(ports.values(), PortPredicates.isParameterPort());
    }

    public Step withOption(final QName name, final String select, final XdmNode node) {
        return withOption(name, select, node, null);
    }

    public Step withOption(final QName name, final String select, final XdmNode node,
            final PortBinding portBinding) {
        final Variable option = variables.get(name);
        Preconditions.checkArgument(option != null, "no such option: %s", name);
        Preconditions.checkArgument(option.isOption(), "not an options: %s", name);

        return new Step(node, type, this.name, internalName, location, stepProcessor, compoundStep,
                TcMaps.copyAndPut(variables, name,
                        option.setSelect(select).setNode(node).setPortBinding(portBinding)),
                parameters, ports, steps, logs);
    }

    public Step withParam(final QName name, final String select, final String value, final Location location) {
        return withParam(name, select, value, location, null);
    }

    public Step withParam(final QName name, final String select, final String value, final Location location,
            final XdmNode node) {
        return withParam(name, select, value, location, node, null);
    }

    public Step withParam(final QName name, final String select, final String value, final Location location,
            final XdmNode node, final PortBinding portBinding) {
        Preconditions.checkArgument(!parameters.containsKey(name), "parameter already set: %s", name);
        return new Step(node, type, this.name, internalName, location, stepProcessor, compoundStep, variables,
                TcMaps.copyAndPut(parameters, name, Variable.newParameter(name, location).setSelect(select)
                        .setValue(value).setNode(node).setPortBinding(portBinding)),
                ports, steps, logs);
    }

    public Step withOptionValue(final QName name, final String value) {
        return withOptionValue(name, value, null);
    }

    public Step withOptionValue(final QName name, final String value, final XdmNode node) {
        final Variable option = variables.get(name);
        Preconditions.checkArgument(option != null, "no such option: %s", name);
        Preconditions.checkArgument(option.isOption(), "not an options: %s", name);

        return new Step(node, type, this.name, internalName, location, stepProcessor, compoundStep,
                TcMaps.copyAndPut(variables, name, option.setValue(value).setNode(node)), parameters, ports, steps,
                logs);
    }

    public boolean hasOptionDeclared(final QName name) {
        final Variable variable = variables.get(name);
        return variable != null && variable.isOption();
    }

    @Override
    public String toString() {
        if (name != null) {
            if (type != null) {
                return name + "(" + SaxonQNames.toPrefixString(type) + ")";
            }

            return name;
        }

        return SaxonQNames.toPrefixString(type);
    }

    public Step setPortBindings(final String portName, final PortBinding... portBindings) {
        return setPortBindings(portName, ImmutableList.copyOf(portBindings));
    }

    public Step setPortBindings(final String portName, final Iterable<PortBinding> portBindings) {
        return withPort(getPort(portName).setPortBindings(portBindings).setSelect(null));
    }

    public Step withPort(final Port port) {
        assert ports.containsKey(port.getPortName());

        return new Step(node, type, name, internalName, location, stepProcessor, compoundStep, variables,
                parameters, TcMaps.copyAndPut(ports, port.getPortName(), port), steps, logs);
    }

    @ReturnsNullable
    public Port getXPathContextPort() {
        final Port xpathContextPort = Iterables
                .getOnlyElement(Iterables.filter(getInputPorts(), PREDICATE_IS_XPATH_CONTEXT_PORT), null);
        LOG.trace("XPath context port = {}", xpathContextPort);
        return xpathContextPort;
    }

    public Map<QName, Variable> getParameters() {
        return parameters;
    }

    public Map<QName, Variable> getVariables() {
        return variables;
    }

    public QName getType() {
        return type;
    }

    public Step setNode(final XdmNode node) {
        if (TcObjects.equals(this.node, node)) {
            return this;
        }
        return new Step(node, type, name, internalName, location, stepProcessor, compoundStep, variables,
                parameters, ports, steps, logs);
    }

    public XdmNode getNode() {
        return node;
    }

    public Step addChildStep(final Step step) {
        LOG.trace("{@method} step = {} ; steps = {} ; childStep = {}", name, steps, step);
        Preconditions.checkNotNull(step);
        Preconditions.checkState(isCompoundStep());

        return new Step(node, type, name, internalName, location, stepProcessor, compoundStep, variables,
                parameters, ports, TcIterables.append(steps, step), logs);

    }

    public Step setSubpipeline(final Iterable<Step> steps) {
        assert steps != null;
        if (TcObjects.equals(this.steps, steps)) {
            return this;
        }

        LOG.trace("{@method} step = {} ; steps = {}", name, steps);
        return new Step(node, type, name, internalName, location, stepProcessor, compoundStep, variables,
                parameters, ports, steps, logs);
    }

    public List<Step> getSubpipeline() {
        return steps;
    }

    public Step setLocation(final Location location) {
        if (TcObjects.equals(this.location, location)) {
            return this;
        }

        return new Step(node, type, name, internalName, location, stepProcessor, compoundStep, variables,
                parameters, ports, steps, logs);
    }

    public Variable getVariable(final QName name) {
        return variables.get(name);
    }

    public PortReference getPortReference(final String portName) {
        return PortReference.newReference(name, portName);
    }

    public StepProcessor getStepProcessor() {
        return stepProcessor;
    }

    @ReturnsNullable
    private ExternalResources getExternalResources() {
        return stepProcessor.getClass().getAnnotation(ExternalResources.class);
    }

    protected boolean readsExternalResources() {
        final ExternalResources externalResources = getExternalResources();
        if (externalResources != null) {
            if (externalResources.read()) {
                return true;
            }
        } else {
            return true;
        }

        for (final Port inputPort : getInputPorts()) {
            for (final PortBinding portBinding : inputPort.getPortBindings()) {
                if (portBinding instanceof DocumentPortBinding || portBinding instanceof DataPortBinding) {
                    return true;
                }
            }
        }

        return false;
    }

    protected boolean writesExternalResources() {
        final ExternalResources externalResources = getExternalResources();
        if (externalResources != null) {
            return externalResources.write();
        }

        return true;
    }

    protected Map<Step, Iterable<Step>> getSubpipelineStepDependencies() {
        Preconditions.checkState(isCompoundStep(), "not a compound step: %s", getName());
        if (dependencies == null) {
            dependencies = getSubpipelineStepDependencies(getSubpipeline());
        }

        return dependencies;
    }

    protected static Map<Step, Iterable<Step>> getSubpipelineStepDependencies(final Iterable<Step> steps) {
        LOG.trace("{@method} steps = {}", steps);

        Step lastWriteStep = null;
        Step defaultReadblePortStep = null;
        final Map<String, Step> subpipelineStepByName = Maps.newHashMap();
        for (final Step step : steps) {
            subpipelineStepByName.put(step.getName(), step);
        }

        final Map<Step, Iterable<Step>> dependencies = Maps.newHashMap();
        for (final Step step : steps) {
            LOG.trace("  step {}", step);

            final List<Step> stepDependencies = Lists.newArrayListWithExpectedSize(16);
            dependencies.put(step, stepDependencies);

            // dependencies related to external resources
            if (lastWriteStep != null && step.readsExternalResources()) {
                LOG.trace("  step {} reads external resources and thus from output of step {}", step,
                        lastWriteStep);
                stepDependencies.add(lastWriteStep);
            }

            // dependencies related to port binding (pipe)
            for (final Port inputPort : step.getInputPorts()) {
                if (defaultReadblePortStep != null && inputPort.getPortBindings().isEmpty()
                        && (step.isPrimary(inputPort) || step.isXPathContextPort(inputPort))) {
                    LOG.trace("  step {} reads implicitly from output of step {}", step, defaultReadblePortStep);
                    stepDependencies.add(defaultReadblePortStep);
                } else {
                    for (final PipePortBinding portBinding : Iterables.filter(inputPort.getPortBindings(),
                            PipePortBinding.class)) {
                        final PortReference dependencyPortReference = portBinding.getPortReference();
                        LOG.trace("  step {} reads from output port {}", step, dependencyPortReference);
                        final Step dependency = subpipelineStepByName.get(dependencyPortReference.getStepName());
                        if (dependency != null) {
                            stepDependencies.add(dependency);
                        }
                    }
                }
            }

            // dependencies related to variable binding (pipe)
            for (final Variable variable : step.getVariables().values()) {
                if (variable.getPortBinding() != null && variable.getPortBinding() instanceof PipePortBinding) {
                    final PipePortBinding portBinding = (PipePortBinding) variable.getPortBinding();
                    final PortReference dependencyPortReference = portBinding.getPortReference();
                    LOG.trace("  variable {} in step {} reads from output port {}", variable.getName(), step,
                            dependencyPortReference);
                    final Step dependency = subpipelineStepByName.get(dependencyPortReference.getStepName());
                    if (dependency != null) {
                        stepDependencies.add(dependency);
                    }
                }
            }

            LOG.trace("  => stepDependencies = {}", stepDependencies);

            // update current information about the step
            if (step.writesExternalResources()) {
                lastWriteStep = step;
            }
            if (step.getPrimaryOutputPort() != null) {
                defaultReadblePortStep = step;
            }
        }

        return dependencies;
    }

    @Override
    public int hashCode() {
        return hashCode.get();
    }

    @Override
    public boolean equals(final Object o) {
        if (o == this) {
            return true;
        }

        if (o != null && o instanceof Step) {
            final Step other = (Step) o;
            return TcObjects.pairEquals(node, other.node, type, other.type, name, other.name, location,
                    other.location, stepProcessor, other.stepProcessor, compoundStep, other.compoundStep, variables,
                    other.variables, parameters, other.parameters, ports, other.ports, steps, other.steps, logs,
                    other.logs);
        }

        return false;
    }

    @Override
    public Iterable<Step> getAllSteps() {
        return Iterables.concat(ImmutableList.of(this),
                Iterables.concat(Iterables.transform(getSubpipeline(), step -> step.getAllSteps())));
    }

    @Override
    public Step getStepByName(final String name) {
        return Iterables.find(getAllSteps(), StepPredicates.hasName(name));
    }

    public Iterable<Log> getLogs() {
        return logs;
    }

    public Step addLog(final String port, final String href) {
        LOG.trace("{@method} step = {} ; port = {} ; href = {}", name, port, href);
        final Log log = new Log(port, href);
        assert !Iterables.contains(logs, log) : name + " / " + logs + " / " + log;
        return new Step(node, type, name, internalName, location, stepProcessor, compoundStep, variables,
                parameters, ports, steps, TcLists.immutableList(logs, log));
    }

    public boolean hasLogDeclaredForPort(final String port) {
        return Iterables.any(logs, log -> log.port.equals(port));
    }

    private void checkCycleDependencies(final Step childStep, final Collection<Step> dependingSteps) {
        LOG.trace("{@method} childStep = {} ; dependingSteps = {}", childStep, dependingSteps);
        final Collection<Step> newDependingSteps = TcSets.immutableSet(dependingSteps, childStep);
        for (final Step dependencyStep : getSubpipelineStepDependencies().get(childStep)) {
            if (newDependingSteps.contains(dependencyStep)) {
                throw XProcExceptions.xs0001(childStep);
            }

            checkCycleDependencies(dependencyStep, newDependingSteps);
        }
    }

    /**
     * {@code err:XS0001}.
     */
    private void checkCyclicDependencies() {
        LOG.trace("{@method} step = {} ; subpipeline = {}", this, getSubpipeline());
        final Collection<Step> empty = ImmutableSet.of();
        for (final Step childStep : getSubpipeline()) {
            checkCycleDependencies(childStep, empty);
        }
    }

    /**
     * {@code err:XS0002}.
     */
    private void checkStepNames() {
        final Set<String> stepNames = Sets.newHashSet();
        for (final Step childStep : getSubpipeline()) {
            if (stepNames.contains(childStep.getName())) {
                throw XProcExceptions.xs0002(childStep);
            }

            stepNames.add(childStep.getName());
        }
    }

    /**
     * {@code err:XS0003}.
     */
    private void checkInputPorts() {
        for (final Port inputPort : getInputPorts()) {
            if (!isPrimary(inputPort) && inputPort.getPortBindings().isEmpty()) {
                if (inputPort.getPortName().equals("xpath-context")) {
                    continue;
                }

                if (getType().equals(XProcSteps.FOR_EACH) && inputPort.getPortName().equals("current")) {
                    continue;
                }

                throw XProcExceptions.xs0003(this, inputPort.getPortName());
            }
        }
    }

    private Iterable<PortBinding> getInputPortBindings() {
        return Iterables.concat(Iterables.transform(getInputPorts(true), PortFunctions.getPortBindings()));
    }

    private Iterable<PortBinding> getOutputPortBindings() {
        return Iterables.concat(Iterables.transform(getOutputPorts(), PortFunctions.getPortBindings()));
    }

    /**
     * {@code err:XS0005}.
     */
    private void checkOutputPorts() {
        for (int i = 0; i < getSubpipeline().size(); i++) {
            final Step childStep = getSubpipeline().get(i);
            final Port outputPort = childStep.getPrimaryOutputPort();
            if (outputPort != null && outputPort.getPortBindings().isEmpty()) {
                final boolean explicitlyConnected = Iterables.any(getDescendantSteps(),
                        step -> Iterables.any(step.getInputPortBindings(),
                                PortBindingPredicates.isConnectedTo(outputPort.getPortReference())));
                if (explicitlyConnected) {
                    continue;
                }

                // The parent step has an output port
                // explicitly bound to this output port
                if (Iterables.any(getOutputPortBindings(),
                        PortBindingPredicates.isConnectedTo(outputPort.getPortReference()))) {
                    continue;
                }

                // Step is the latest from subpipeline and
                // The parent step has a primary output port
                // implicitly bound to this output port
                if (i == getSubpipeline().size() - 1 && childStep.isPrimary(outputPort)
                        && getPrimaryOutputPort() != null && getPrimaryOutputPort().getPortBindings().isEmpty()) {
                    continue;
                }

                // Next step has unbound primary input port
                if (i < getSubpipeline().size() - 1 && getSubpipeline().get(i + 1).getPrimaryInputPort() != null
                        && getSubpipeline().get(i + 1).getPrimaryInputPort().getPortBindings().isEmpty()) {
                    continue;
                }

                // Next step is a choose step
                if (i < getSubpipeline().size() - 1 && getSubpipeline().get(i + 1).getType() == XProcSteps.CHOOSE) {
                    continue;
                }

                // TODO: xs0005 is not manage very well !
                throw XProcExceptions.xs0005(childStep, outputPort.getPortName());
            }
        }
    }

    private Iterable<Step> getDescendantSteps() {
        return Iterables.concat(Iterables.transform(getSubpipeline(),
                step -> Iterables.concat(ImmutableList.of(step), step.getDescendantSteps())));
    }

    public void checkDeclaredStep() {
        checkCyclicDependencies();
        checkStepNames();
        checkOutputPorts();
    }

    public void checkInstanceStep() {
        checkInputPorts();
    }
}