org.apache.taverna.scufl2.translator.t2flow.T2FlowParser.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.taverna.scufl2.translator.t2flow.T2FlowParser.java

Source

package org.apache.taverna.scufl2.translator.t2flow;
/*
 *
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 *
*/

import static java.util.Locale.ENGLISH;
import static java.util.TimeZone.getTimeZone;
import static java.util.UUID.randomUUID;
import static java.util.logging.Level.SEVERE;
import static java.util.logging.Level.WARNING;
import static javax.xml.XMLConstants.W3C_XML_SCHEMA_NS_URI;
import static org.apache.taverna.scufl2.api.container.WorkflowBundle.WORKFLOW_BUNDLE_ROOT;
import static org.apache.taverna.scufl2.api.core.Workflow.WORKFLOW_ROOT;
import static org.apache.taverna.scufl2.xml.t2flow.jaxb.LinkType.DATAFLOW;
import static org.apache.taverna.scufl2.xml.t2flow.jaxb.LinkType.MERGE;
import static org.apache.taverna.scufl2.xml.t2flow.jaxb.LinkType.PROCESSOR;
import static org.apache.taverna.scufl2.xml.t2flow.jaxb.Role.TOP;

import java.io.CharArrayWriter;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Collection;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.ServiceLoader;
import java.util.Set;
import java.util.SortedMap;
import java.util.TreeMap;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.logging.Logger;

import javax.xml.bind.JAXBContext;
import javax.xml.bind.JAXBElement;
import javax.xml.bind.JAXBException;
import javax.xml.bind.Unmarshaller;
import javax.xml.transform.Source;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerConfigurationException;
import javax.xml.transform.TransformerException;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;

import org.w3c.dom.Element;
import org.xml.sax.SAXException;

import org.apache.taverna.scufl2.api.annotation.Annotation;
import org.apache.taverna.scufl2.api.annotation.Revision;
import org.apache.taverna.scufl2.api.common.Named;
import org.apache.taverna.scufl2.api.common.NamedSet;
import org.apache.taverna.scufl2.api.common.Scufl2Tools;
import org.apache.taverna.scufl2.api.common.URITools;
import org.apache.taverna.scufl2.api.common.WorkflowBean;
import org.apache.taverna.scufl2.api.configurations.Configuration;
import org.apache.taverna.scufl2.api.container.WorkflowBundle;
import org.apache.taverna.scufl2.api.core.BlockingControlLink;
import org.apache.taverna.scufl2.api.core.ControlLink;
import org.apache.taverna.scufl2.api.core.DataLink;
import org.apache.taverna.scufl2.api.core.Processor;
import org.apache.taverna.scufl2.api.core.Workflow;
import org.apache.taverna.scufl2.api.io.ReaderException;
import org.apache.taverna.scufl2.api.iterationstrategy.IterationStrategyNode;
import org.apache.taverna.scufl2.api.iterationstrategy.IterationStrategyStack;
import org.apache.taverna.scufl2.api.iterationstrategy.IterationStrategyTopNode;
import org.apache.taverna.scufl2.api.iterationstrategy.PortNode;
import org.apache.taverna.scufl2.api.port.InputActivityPort;
import org.apache.taverna.scufl2.api.port.InputProcessorPort;
import org.apache.taverna.scufl2.api.port.InputWorkflowPort;
import org.apache.taverna.scufl2.api.port.OutputActivityPort;
import org.apache.taverna.scufl2.api.port.OutputProcessorPort;
import org.apache.taverna.scufl2.api.port.OutputWorkflowPort;
import org.apache.taverna.scufl2.api.port.ReceiverPort;
import org.apache.taverna.scufl2.api.port.SenderPort;
import org.apache.taverna.scufl2.api.profiles.ProcessorBinding;
import org.apache.taverna.scufl2.api.profiles.ProcessorInputPortBinding;
import org.apache.taverna.scufl2.api.profiles.ProcessorOutputPortBinding;
import org.apache.taverna.scufl2.api.profiles.Profile;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Activity;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.AnnotatedGranularDepthPort;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.AnnotatedGranularDepthPorts;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.AnnotatedPort;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.AnnotatedPorts;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.AnnotationAssertionImpl.NetSfTavernaT2AnnotationAnnotationAssertionImpl;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.AnnotationChain;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Annotations;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Condition;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Conditions;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.ConfigBean;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.CrossProduct;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Dataflow;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Datalinks;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.DepthPort;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.DepthPorts;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.DispatchLayer;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.DispatchStack;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.DotProduct;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.GranularDepthPort;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.GranularDepthPorts;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.IterationNode;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.IterationNodeParent;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Link;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Mapping;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.ObjectFactory;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.PortProduct;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Processors;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.Raven;
import org.apache.taverna.scufl2.xml.t2flow.jaxb.TopIterationNode;

import com.fasterxml.jackson.databind.node.ObjectNode;

public class T2FlowParser {
    private static final URI INTERNAL_DISPATCH_PREFIX = URI
            .create("http://ns.taverna.org.uk/2010/scufl2/taverna/dispatchlayer/");
    private static final String TEXT_TURTLE = "text/turtle";
    private static final String SEMANTIC_ANNOTATION = "net.sf.taverna.t2.annotation.annotationbeans.SemanticAnnotation";
    private static final String IDENTIFICATION_ASSERTION = "net.sf.taverna.t2.annotation.annotationbeans.IdentificationAssertion";
    private static final String T2FLOW_EXTENDED_XSD = "xsd/t2flow-extended.xsd";
    @SuppressWarnings("unused")
    private static final String T2FLOW_XSD = "xsd/t2flow.xsd";
    private static final Logger logger = Logger.getLogger(T2FlowParser.class.getCanonicalName());
    public static final URI ravenURI = URI.create("http://ns.taverna.org.uk/2010/xml/t2flow/raven/");
    public static final URI configBeanURI = URI.create("http://ns.taverna.org.uk/2010/xml/t2flow/configbean/");
    public static final URI t2flowParserURI = URI.create("http://ns.taverna.org.uk/2012/scufl2/t2flowParser");
    // TODO: Find better example predicate
    public static final URI exampleDataURI = URI.create("http://biocatalogue.org/attribute/exampleData");
    public static final String DEFAULT_PRODUCED_BY = "unspecified";

    public static <T extends Named> T findNamed(Collection<T> namedObjects, String name) {
        for (T named : namedObjects)
            if (named.getName().equals(name))
                return named;
        return null;
    }

    protected ThreadLocal<ParserState> parserState = new ThreadLocalParserState();

    /**
     * A static class for the thread-local parser state.
     */
    private static class ThreadLocalParserState extends ThreadLocal<ParserState> {
        @Override
        protected ParserState initialValue() {
            return new ParserState();
        };
    }

    private static Scufl2Tools scufl2Tools = new Scufl2Tools();
    private static URITools uriTools = new URITools();
    private static TransformerFactory transformerFactory;

    protected Set<T2Parser> t2Parsers = null;
    protected final JAXBContext jaxbContext;
    private boolean strict = false;
    private boolean validating = false;

    public final boolean isValidating() {
        return validating;
    }

    public final void setValidating(boolean validating) {
        this.validating = validating;
    }

    protected ServiceLoader<T2Parser> discoveredT2Parsers;
    protected final ThreadLocal<Unmarshaller> unmarshaller;
    private Map<String, URI> predicates;

    public T2FlowParser() throws JAXBException {
        jaxbContext = JAXBContext.newInstance(ObjectFactory.class);
        unmarshaller = new ThreadLocalUnmarshaller(jaxbContext);
    }

    /**
     * A static class for the thread-local unmarshaller.
     */
    private static class ThreadLocalUnmarshaller extends ThreadLocal<Unmarshaller> {
        private final JAXBContext jaxbContext;

        ThreadLocalUnmarshaller(JAXBContext jaxbContext) {
            this.jaxbContext = jaxbContext;
        }

        @Override
        protected Unmarshaller initialValue() {
            try {
                return jaxbContext.createUnmarshaller();
            } catch (JAXBException e) {
                logger.log(SEVERE, "Could not create unmarshaller", e);
                return null;
            }
        };
    }

    protected ReceiverPort findReceiverPort(Workflow wf, Link sink) throws ReaderException {
        String portName = sink.getPort();
        if (portName == null)
            throw new ReaderException("Port name not specified");
        String processorName = sink.getProcessor();
        if (processorName == null) {
            if (sink.getType().equals(PROCESSOR))
                throw new ReaderException("Link type was processor, but no processor name found");
            OutputWorkflowPort candidate = wf.getOutputPorts().getByName(portName);
            if (candidate == null)
                throw new ReaderException("Link to unknown workflow port " + portName);
            return candidate;
        } else {
            if (sink.getType().equals(DATAFLOW))
                throw new ReaderException("Link type was dataflow, but processor name was found");
            Processor processor = wf.getProcessors().getByName(processorName);
            if (processor == null)
                throw new ReaderException("Link to unknown processor " + processorName);
            InputProcessorPort candidate = processor.getInputPorts().getByName(portName);
            if (candidate == null)
                throw new ReaderException("Link to unknown port " + portName + " in " + processorName);
            return candidate;
        }
    }

    protected SenderPort findSenderPort(Workflow wf, Link source) throws ReaderException {
        if (source.getType().equals(MERGE))
            throw new ReaderException("Link type Merge unexpected for sender ports");
        String portName = source.getPort();
        if (portName == null)
            throw new ReaderException("Port name not specified");
        String processorName = source.getProcessor();
        if (processorName == null) {
            if (source.getType().equals(PROCESSOR))
                throw new ReaderException("Link type was processor, but no processor name found");
            InputWorkflowPort candidate = wf.getInputPorts().getByName(portName);
            if (candidate == null)
                throw new ReaderException("Link from unknown workflow port " + portName);
            return candidate;
        } else {
            if (source.getType().equals(DATAFLOW))
                throw new ReaderException("Link type was dataflow, but processor name was found");
            Processor processor = wf.getProcessors().getByName(processorName);
            if (processor == null)
                throw new ReaderException("Link from unknown processor " + processorName);
            OutputProcessorPort candidate = processor.getOutputPorts().getByName(portName);
            if (candidate == null)
                throw new ReaderException("Link from unknown port " + portName + " in " + processorName);
            return candidate;
        }
    }

    protected T2Parser getT2Parser(URI classURI) {
        for (T2Parser t2Parser : getT2Parsers())
            if (t2Parser.canHandlePlugin(classURI))
                return t2Parser;
        return null;
    }

    public synchronized Set<T2Parser> getT2Parsers() {
        Set<T2Parser> parsers = t2Parsers;
        if (parsers != null)
            return parsers;
        parsers = new HashSet<>();
        /*
         * TODO: Do we need to cache this, or is the cache in ServiceLoader fast
         * enough?
         */
        if (discoveredT2Parsers == null)
            discoveredT2Parsers = ServiceLoader.load(T2Parser.class);
        for (T2Parser parser : discoveredT2Parsers)
            parsers.add(parser);
        return parsers;
    }

    public synchronized void setT2Parsers(Set<T2Parser> parsers) {
        this.t2Parsers = parsers;
    }

    public boolean isStrict() {
        return strict;
    }

    protected void makeProfile(org.apache.taverna.scufl2.xml.t2flow.jaxb.Workflow wf) {
        // TODO: What should the default be? Should there be one? Who knows
        Profile profile = new Profile(wf.getProducedBy() == null ? DEFAULT_PRODUCED_BY : wf.getProducedBy());
        profile.setParent(parserState.get().getCurrentWorkflowBundle());
        parserState.get().getCurrentWorkflowBundle().setMainProfile(profile);
        parserState.get().setCurrentProfile(profile);
    }

    private URI makeRavenURI(Raven raven, String className) {
        if (raven == null)
            return ravenURI.resolve("undefined/" + uriTools.validFilename(className));

        return ravenURI.resolve(
                uriTools.validFilename(raven.getGroup()) + "/" + uriTools.validFilename(raven.getArtifact()) + "/"
                        + uriTools.validFilename(raven.getVersion()) + "/" + uriTools.validFilename(className));
    }

    private URI mapTypeFromRaven(Raven raven, String activityClass) throws ReaderException {
        URI classURI = makeRavenURI(raven, activityClass);
        parserState.get().setCurrentT2Parser(null);
        T2Parser t2Parser = getT2Parser(classURI);
        if (t2Parser == null) {
            String message = "Unknown T2 activity or dispatch layer " + classURI + ", install supporting T2Parser";
            if (isStrict())
                throw new ReaderException(message);
            logger.warning(message);
            return classURI;
        }
        parserState.get().setCurrentT2Parser(t2Parser);
        return t2Parser.mapT2flowRavenIdToScufl2URI(classURI);
    }

    protected org.apache.taverna.scufl2.api.activity.Activity parseActivityAndAddToProfile(Activity origActivity)
            throws ReaderException {
        org.apache.taverna.scufl2.api.activity.Activity newActivity = parseActivity(origActivity);
        newActivity.setName(parserState.get().getCurrentProcessorBinding().getName());
        parserState.get().getCurrentProfile().getActivities().addWithUniqueName(newActivity);
        newActivity.setParent(parserState.get().getCurrentProfile());
        return newActivity;
    }

    protected org.apache.taverna.scufl2.api.activity.Activity parseActivity(Activity origActivity)
            throws ReaderException {
        Raven raven = origActivity.getRaven();
        String activityClass = origActivity.getClazz();
        URI activityId = mapTypeFromRaven(raven, activityClass);
        org.apache.taverna.scufl2.api.activity.Activity newActivity = new org.apache.taverna.scufl2.api.activity.Activity();
        newActivity.setType(activityId);
        return newActivity;
    }

    protected void parseActivityBinding(Activity origActivity, int activityPosition)
            throws ReaderException, JAXBException {
        ProcessorBinding processorBinding = new ProcessorBinding();

        processorBinding.setName(parserState.get().getCurrentProcessor().getName());
        parserState.get().getCurrentProfile().getProcessorBindings().addWithUniqueName(processorBinding);

        processorBinding.setBoundProcessor(parserState.get().getCurrentProcessor());
        parserState.get().setCurrentProcessorBinding(processorBinding);
        org.apache.taverna.scufl2.api.activity.Activity newActivity = parseActivityAndAddToProfile(origActivity);
        parserState.get().setCurrentActivity(newActivity);

        parserState.get().getCurrentProfile().getActivities().add(newActivity);
        processorBinding.setBoundActivity(newActivity);
        processorBinding.setActivityPosition(activityPosition);

        parserState.get().setCurrentConfigurable(newActivity);

        try {
            parseConfigurationAndAddToProfile(origActivity.getConfigBean());
        } catch (JAXBException e) {
            if (isStrict())
                throw e;
            logger.log(WARNING, "Can't configure activity" + newActivity, e);
        }

        parseActivityInputMap(origActivity.getInputMap());
        parseActivityOutputMap(origActivity.getOutputMap());

        parserState.get().setCurrentConfigurable(null);
        parserState.get().setCurrentActivity(null);
        parserState.get().setCurrentProcessorBinding(null);
    }

    protected Configuration parseConfigurationAndAddToProfile(ConfigBean configBean)
            throws JAXBException, ReaderException {
        Configuration configuration = parseConfiguration(configBean);
        if (configuration == null)
            return null;
        Profile profile = parserState.get().getCurrentProfile();
        configuration.setName(parserState.get().getCurrentActivity().getName());
        profile.getConfigurations().addWithUniqueName(configuration);
        configuration.setConfigures(parserState.get().getCurrentConfigurable());
        return configuration;
    }

    protected Configuration parseConfiguration(ConfigBean configBean) throws JAXBException, ReaderException {
        // Placeholder to check later if no configuration have been provided
        Configuration UNCONFIGURED = new Configuration();

        Configuration configuration = UNCONFIGURED;
        if (parserState.get().getCurrentT2Parser() == null) {
            String message = "No config parser for " + parserState.get().getCurrentConfigurable();
            if (isStrict())
                throw new ReaderException(message);
            return null;
        }

        try {
            configuration = parserState.get().getCurrentT2Parser().parseConfiguration(this, configBean,
                    parserState.get());
        } catch (ReaderException e) {
            if (isStrict())
                throw e;
        }
        if (configuration == null)
            // Perfectly valid - true for say Invoke layer
            return null;

        if (configuration == UNCONFIGURED) {
            if (isStrict())
                throw new ReaderException("No configuration returned from " + parserState.get().getCurrentT2Parser()
                        + " for " + parserState.get().getCurrentConfigurable());
            // We'll have to fall back by just keeping the existing XML

            configuration = new Configuration();
            configuration.setType(configBeanURI.resolve("Config"));
            String xml = elementToXML((Element) configBean.getAny());
            String encoding = configBean.getEncoding();
            ObjectNode json = (ObjectNode) configuration.getJson();
            json.put(encoding, xml);
        }
        return configuration;
    }

    public static String elementToXML(Element element) {
        try {
            Transformer transformer = getTransformer();
            CharArrayWriter writer = new CharArrayWriter();
            transformer.transform(new DOMSource(element), new StreamResult(writer));
            return writer.toString();
        } catch (TransformerException e) {
            throw new IllegalStateException("Can't write XML", e);
        }
    }

    public static Transformer getTransformer() throws TransformerConfigurationException {
        if (transformerFactory == null)
            transformerFactory = TransformerFactory.newInstance();
        return transformerFactory.newTransformer();
    }

    public Unmarshaller getUnmarshaller() {
        Unmarshaller u = unmarshaller.get();
        if (!isValidating() && u.getSchema() != null) {
            u.setSchema(null);
        } else if (isValidating() && u.getSchema() == null) {
            // Load and set schema to validate against
            Schema schema;
            try {
                SchemaFactory schemaFactory = SchemaFactory.newInstance(W3C_XML_SCHEMA_NS_URI);
                List<URI> schemas = getAdditionalSchemas();
                URL t2flowExtendedXSD = T2FlowParser.class.getResource(T2FLOW_EXTENDED_XSD);
                schemas.add(t2flowExtendedXSD.toURI());

                List<Source> schemaSources = new ArrayList<>();
                for (URI schemaUri : schemas)
                    schemaSources.add(new StreamSource(schemaUri.toASCIIString()));
                Source[] sources = schemaSources.toArray(new Source[schemaSources.size()]);
                schema = schemaFactory.newSchema(sources);
            } catch (SAXException e) {
                throw new RuntimeException("Can't load schemas", e);
            } catch (URISyntaxException | NullPointerException e) {
                throw new RuntimeException("Can't find schemas", e);
            }
            u.setSchema(schema);
        }
        return u;
    }

    protected List<URI> getAdditionalSchemas() {
        List<URI> uris = new ArrayList<>();
        for (T2Parser parser : getT2Parsers()) {
            List<URI> schemas = parser.getAdditionalSchemas();
            if (schemas != null)
                uris.addAll(schemas);
        }
        return uris;
    }

    protected void parseActivityInputMap(org.apache.taverna.scufl2.xml.t2flow.jaxb.Map inputMap)
            throws ReaderException {
        for (Mapping mapping : inputMap.getMap()) {
            String fromProcessorOutput = mapping.getFrom();
            String toActivityOutput = mapping.getTo();
            ProcessorInputPortBinding processorInputPortBinding = new ProcessorInputPortBinding();

            InputProcessorPort inputProcessorPort = findNamed(
                    parserState.get().getCurrentProcessor().getInputPorts(), fromProcessorOutput);
            if (inputProcessorPort == null) {
                String message = "Invalid input port binding, " + "unknown processor port: " + fromProcessorOutput
                        + "->" + toActivityOutput + " in " + parserState.get().getCurrentProcessor();
                if (isStrict())
                    throw new ReaderException(message);
                logger.warning(message);
                continue;
            }

            InputActivityPort inputActivityPort = parserState.get().getCurrentActivity().getInputPorts()
                    .getByName(toActivityOutput);
            if (inputActivityPort == null) {
                inputActivityPort = new InputActivityPort();
                inputActivityPort.setName(toActivityOutput);
                inputActivityPort.setParent(parserState.get().getCurrentActivity());
                parserState.get().getCurrentActivity().getInputPorts().add(inputActivityPort);
            }

            if (inputActivityPort.getDepth() == null)
                inputActivityPort.setDepth(inputProcessorPort.getDepth());

            processorInputPortBinding.setBoundActivityPort(inputActivityPort);
            processorInputPortBinding.setBoundProcessorPort(inputProcessorPort);
            parserState.get().getCurrentProcessorBinding().getInputPortBindings().add(processorInputPortBinding);
        }
    }

    protected void parseActivityOutputMap(org.apache.taverna.scufl2.xml.t2flow.jaxb.Map outputMap)
            throws ReaderException {
        for (Mapping mapping : outputMap.getMap()) {
            String fromActivityOutput = mapping.getFrom();
            String toProcessorOutput = mapping.getTo();
            ProcessorOutputPortBinding processorOutputPortBinding = new ProcessorOutputPortBinding();

            OutputProcessorPort outputProcessorPort = findNamed(
                    parserState.get().getCurrentProcessor().getOutputPorts(), toProcessorOutput);
            if (outputProcessorPort == null) {
                String message = "Invalid output port binding, " + "unknown processor port: " + fromActivityOutput
                        + "->" + toProcessorOutput + " in " + parserState.get().getCurrentProcessor();
                if (isStrict())
                    throw new ReaderException(message);
                logger.warning(message);
                continue;
            }

            OutputActivityPort outputActivityPort = parserState.get().getCurrentActivity().getOutputPorts()
                    .getByName(fromActivityOutput);
            if (outputActivityPort == null) {
                outputActivityPort = new OutputActivityPort();
                outputActivityPort.setName(fromActivityOutput);
                outputActivityPort.setParent(parserState.get().getCurrentActivity());
                parserState.get().getCurrentActivity().getOutputPorts().add(outputActivityPort);
            }

            if (outputActivityPort.getDepth() == null)
                outputActivityPort.setDepth(outputProcessorPort.getDepth());
            if (outputActivityPort.getGranularDepth() == null)
                outputActivityPort.setGranularDepth(outputProcessorPort.getGranularDepth());

            processorOutputPortBinding.setBoundActivityPort(outputActivityPort);
            processorOutputPortBinding.setBoundProcessorPort(outputProcessorPort);
            parserState.get().getCurrentProcessorBinding().getOutputPortBindings().add(processorOutputPortBinding);
        }
    }

    protected Workflow parseDataflow(Dataflow df, Workflow wf) throws ReaderException, JAXBException {
        parserState.get().setCurrentWorkflow(wf);
        wf.setInputPorts(parseInputPorts(df.getInputPorts()));
        wf.setOutputPorts(parseOutputPorts(df.getOutputPorts()));
        wf.setProcessors(parseProcessors(df.getProcessors()));
        wf.setDataLinks(parseDatalinks(df.getDatalinks()));
        wf.setControlLinks(parseControlLinks(df.getConditions()));
        Revision revision = parseIdentificationAnnotations(df.getAnnotations());
        if (revision != null)
            wf.setCurrentRevision(revision);
        parseAnnotations(wf, df.getAnnotations());

        parserState.get().setCurrentWorkflow(null);
        return wf;
    }

    public void parseAnnotations(WorkflowBean annotatedBean, Annotations annotations) throws ReaderException {
        // logger.fine("Checking annotations for " + annotatedSubject);

        Map<String, NetSfTavernaT2AnnotationAnnotationAssertionImpl> annotationElems = new HashMap<>();
        if (annotations == null || annotations.getAnnotationChainOrAnnotationChain22() == null)
            return;
        for (JAXBElement<AnnotationChain> el : annotations.getAnnotationChainOrAnnotationChain22()) {
            NetSfTavernaT2AnnotationAnnotationAssertionImpl ann = el.getValue()
                    .getNetSfTavernaT2AnnotationAnnotationChainImpl().getAnnotationAssertions()
                    .getNetSfTavernaT2AnnotationAnnotationAssertionImpl();
            String annClass = ann.getAnnotationBean().getClazz();
            if (annotationElems.containsKey(annClass)
                    && annotationElems.get(annClass).getDate().compareToIgnoreCase(ann.getDate()) > 0)
                // ann.getDate() is less than current 'latest' annotation, skip
                continue;
            annotationElems.put(annClass, ann);
        }

        for (String clazz : annotationElems.keySet()) {
            NetSfTavernaT2AnnotationAnnotationAssertionImpl ann = annotationElems.get(clazz);
            Calendar cal = parseDate(ann.getDate());
            String value = null;
            String semanticMediaType = TEXT_TURTLE;
            for (Object obj : ann.getAnnotationBean().getAny()) {
                if (!(obj instanceof Element))
                    continue;
                Element elem = (Element) obj;
                if (!(elem.getNamespaceURI() == null))
                    continue;
                if (elem.getLocalName().equals("text")) {
                    value = elem.getTextContent().trim();
                    break;
                }
                if (clazz.equals(SEMANTIC_ANNOTATION)) {
                    if (elem.getLocalName().equals("content"))
                        value = elem.getTextContent().trim();
                    if (elem.getLocalName().equals("mimeType"))
                        semanticMediaType = elem.getTextContent().trim();
                }
            }
            if (value != null) {
                // Add the annotation
                Annotation annotation = new Annotation();
                WorkflowBundle workflowBundle = parserState.get().getCurrentWorkflowBundle();
                annotation.setParent(workflowBundle);

                String path = "annotation/" + annotation.getName() + ".ttl";
                URI bodyURI = URI.create(path);

                annotation.setTarget(annotatedBean);
                annotation.setAnnotatedAt(cal);
                // annotation.setAnnotator();
                annotation.setSerializedBy(t2flowParserURI);
                annotation.setSerializedAt(new GregorianCalendar());
                URI annotatedSubject = uriTools.relativeUriForBean(annotatedBean, annotation);
                String body;
                if (clazz.equals(SEMANTIC_ANNOTATION)) {
                    String baseStr = "@base <" + annotatedSubject.toASCIIString() + "> .\n";
                    body = baseStr + value;
                } else {
                    // Generate Turtle from 'classic' annotation
                    URI predicate = getPredicatesForClass().get(clazz);
                    if (predicate == null) {
                        if (isStrict())
                            throw new ReaderException("Unsupported annotation class " + clazz);
                        return;
                    }

                    StringBuilder turtle = new StringBuilder();
                    turtle.append("<");
                    turtle.append(annotatedSubject.toASCIIString());
                    turtle.append("> ");

                    turtle.append("<");
                    turtle.append(predicate.toASCIIString());
                    turtle.append("> ");

                    // A potentially multi-line string
                    turtle.append("\"\"\"");
                    // Escape existing \ to \\
                    String escaped = value.replace("\\", "\\\\");
                    // Escape existing " to \" (beware Java's escaping of \ and " below)
                    escaped = escaped.replace("\"", "\\\"");
                    turtle.append(escaped);
                    turtle.append("\"\"\"");
                    turtle.append(" .");
                    body = turtle.toString();
                }
                try {
                    workflowBundle.getResources().addResource(body, path, semanticMediaType);
                } catch (IOException e) {
                    throw new ReaderException("Could not store annotation body to " + path, e);
                }
                annotation.setBody(bodyURI);
            }
        }
    }

    private Map<String, URI> getPredicatesForClass() {
        if (this.predicates != null)
            return this.predicates;
        synchronized (this) {
            if (this.predicates != null)
                return this.predicates;
            Map<String, URI> predicates = new HashMap<>();
            predicates.put("net.sf.taverna.t2.annotation.annotationbeans.DescriptiveTitle",
                    URI.create("http://purl.org/dc/terms/title"));

            predicates.put("net.sf.taverna.t2.annotation.annotationbeans.Author",
                    URI.create("http://purl.org/dc/elements/1.1/creator"));

            predicates.put("net.sf.taverna.t2.annotation.annotationbeans.FreeTextDescription",
                    URI.create("http://purl.org/dc/terms/description"));

            predicates.put("net.sf.taverna.t2.annotation.annotationbeans.MimeType",
                    URI.create("http://purl.org/dc/elements/1.1/format"));

            predicates.put("net.sf.taverna.t2.annotation.annotationbeans.ExampleValue", exampleDataURI);
            this.predicates = predicates;
            return this.predicates;
        }
    }

    protected Revision parseIdentificationAnnotations(Annotations annotations) {
        SortedMap<Calendar, UUID> revisions = new TreeMap<>();
        if (annotations == null || annotations.getAnnotationChainOrAnnotationChain22() == null)
            return null;
        for (JAXBElement<AnnotationChain> el : annotations.getAnnotationChainOrAnnotationChain22()) {
            NetSfTavernaT2AnnotationAnnotationAssertionImpl ann = el.getValue()
                    .getNetSfTavernaT2AnnotationAnnotationChainImpl().getAnnotationAssertions()
                    .getNetSfTavernaT2AnnotationAnnotationAssertionImpl();
            String annClass = ann.getAnnotationBean().getClazz();
            if (!annClass.equals(IDENTIFICATION_ASSERTION))
                continue;
            for (Object obj : ann.getAnnotationBean().getAny()) {
                if (!(obj instanceof Element))
                    continue;
                Element elem = (Element) obj;
                if (elem.getNamespaceURI() == null && elem.getLocalName().equals("identification")) {
                    String uuid = elem.getTextContent().trim();
                    String date = ann.getDate();
                    Calendar cal = parseDate(date);
                    revisions.put(cal, UUID.fromString(uuid));
                }
            }
        }

        Revision rev = null;
        for (Entry<Calendar, UUID> entry : revisions.entrySet()) {
            Calendar cal = entry.getKey();
            UUID uuid = entry.getValue();
            URI uri = WORKFLOW_ROOT.resolve(uuid.toString() + "/");
            rev = new Revision(uri, rev);
            rev.setGeneratedAtTime(cal);
        }
        return rev;
    }

    private Calendar parseDate(String dateStr) {
        // Based briefly on patterns used by 
        // com.thoughtworks.xstream.converters.basic.DateConverter

        List<String> patterns = new ArrayList<>();
        patterns.add("yyyy-MM-dd HH:mm:ss.S z");
        patterns.add("yyyy-MM-dd HH:mm:ss z");
        patterns.add("yyyy-MM-dd HH:mm:ssz");
        patterns.add("yyyy-MM-dd HH:mm:ss.S 'UTC'");
        patterns.add("yyyy-MM-dd HH:mm:ss 'UTC'");
        Date date;
        for (String pattern : patterns)
            try {
                SimpleDateFormat dateFormat = new SimpleDateFormat(pattern, ENGLISH);
                date = dateFormat.parse(dateStr);
                GregorianCalendar cal = new GregorianCalendar(getTimeZone("UTC"), ENGLISH);
                cal.setTime(date);
                return cal;
            } catch (ParseException e) {
                continue;
            }
        throw new IllegalArgumentException("Can't parse date: " + dateStr);
    }

    private Set<ControlLink> parseControlLinks(Conditions conditions) throws ReaderException {
        Set<ControlLink> links = new HashSet<>();
        for (Condition condition : conditions.getCondition()) {
            NamedSet<Processor> processors = parserState.get().getCurrentWorkflow().getProcessors();
            String target = condition.getTarget();
            Processor block = processors.getByName(target);
            if (block == null && isStrict())
                throw new ReaderException("Unrecognized blocking processor in control link: " + target);
            String control = condition.getControl();
            Processor untilFinished = processors.getByName(control);
            if (untilFinished == null && isStrict())
                throw new ReaderException("Unrecognized untilFinished processor in control link: " + control);

            BlockingControlLink newLink = new BlockingControlLink(block, untilFinished);

            // FIXME: missing from t2flow and schema
            //parseAnnotations(newLink, condition.getAnnotations());

            links.add(newLink);
        }
        return links;
    }

    protected Set<DataLink> parseDatalinks(Datalinks origLinks) throws ReaderException {
        HashSet<DataLink> newLinks = new HashSet<>();
        Map<ReceiverPort, AtomicInteger> mergeCounts = new HashMap<>();
        for (org.apache.taverna.scufl2.xml.t2flow.jaxb.DataLink origLink : origLinks.getDatalink()) {
            try {
                SenderPort senderPort = findSenderPort(parserState.get().getCurrentWorkflow(),
                        origLink.getSource());
                ReceiverPort receiverPort = findReceiverPort(parserState.get().getCurrentWorkflow(),
                        origLink.getSink());

                DataLink newLink = new DataLink(parserState.get().getCurrentWorkflow(), senderPort, receiverPort);

                AtomicInteger mergeCount = mergeCounts.get(receiverPort);
                if (origLink.getSink().getType().equals(MERGE)) {
                    if (mergeCount != null && mergeCount.intValue() < 1)
                        throw new ReaderException("Merged and non-merged links to port " + receiverPort);
                    if (mergeCount == null) {
                        mergeCount = new AtomicInteger(0);
                        mergeCounts.put(receiverPort, mergeCount);
                    }
                    newLink.setMergePosition(mergeCount.getAndIncrement());
                } else {
                    if (mergeCount != null)
                        throw new ReaderException("More than one link to non-merged port " + receiverPort);
                    mergeCounts.put(receiverPort, new AtomicInteger(-1));
                }

                // FIXME: missing from t2flow and schema
                //parseAnnotations(newLink, origLink.getAnnotations());

                newLinks.add(newLink);
            } catch (ReaderException ex) {
                logger.log(WARNING, "Could not translate link:\n" + origLink, ex);
                if (isStrict())
                    throw ex;
                continue;
            }
        }
        return newLinks;
    }

    protected void parseDispatchStack(DispatchStack dispatchStack) throws ReaderException {
        Processor processor = parserState.get().getCurrentProcessor();
        Configuration procConfig = scufl2Tools.createConfigurationFor(processor,
                parserState.get().getCurrentProfile());
        parserState.get().setCurrentConfigurable(processor);
        parserState.get().setCurrentConfiguration(procConfig);
        parserState.get().setPreviousDispatchLayerName(null);
        try {
            for (DispatchLayer dispatchLayer : dispatchStack.getDispatchLayer())
                parseDispatchStack(dispatchLayer);
        } finally {
            parserState.get().setCurrentConfigurable(null);
            parserState.get().setCurrentConfiguration(null);
            parserState.get().setPreviousDispatchLayerName(null);
        }
    }

    protected void parseDispatchStack(DispatchLayer dispatchLayer) throws ReaderException {
        URI typeUri = mapTypeFromRaven(dispatchLayer.getRaven(), dispatchLayer.getClazz());
        ObjectNode procConfig = parserState.get().getCurrentConfiguration().getJsonAsObjectNode();

        try {
            Configuration dispatchConfig = parseConfiguration(dispatchLayer.getConfigBean());
            URI relUri = INTERNAL_DISPATCH_PREFIX.relativize(typeUri);
            String name;
            if (!relUri.isAbsolute()) {
                /*
                 * It's an internal layer. We'll put it under the name which
                 * we'll cleverly fish out of the URI path, eg. "retry" or
                 * "parallelize"
                 */
                name = relUri.getPath().toLowerCase();
                if (dispatchConfig != null && dispatchConfig.getJson().size() > 0)
                    // But only if non-empty (non-default)
                    procConfig.put(name, dispatchConfig.getJson());
            } else {
                ObjectNode json;
                if (dispatchConfig != null && dispatchConfig.getJson().isObject())
                    json = dispatchConfig.getJsonAsObjectNode();
                else {
                    /*
                     * We'll still need to create an objectNode to keep _type
                     * and _below
                     */
                    json = procConfig.objectNode();
                    if (dispatchConfig != null)
                        // We'll put the non-objectnode here
                        json.put("_config", dispatchConfig.getJson());
                }

                /*
                 * Not really much to go from here, we don't want to use the
                 * typeUri as the name as third-party layers in theory could be
                 * added several times for same type
                 */
                name = randomUUID().toString();
                json.put("_type", typeUri.toString());
                // Might be null - meaning "top"
                json.put("_below", parserState.get().getPreviousDispatchLayerName());
                procConfig.put(name, json);
            }
            parserState.get().setPreviousDispatchLayerName(name);
        } catch (JAXBException ex) {
            String message = "Can't parse configuration for dispatch layer in "
                    + parserState.get().getCurrentProcessor();
            if (isStrict())
                throw new ReaderException(message, ex);
            logger.log(WARNING, message, ex);
        }
    }

    protected Set<InputWorkflowPort> parseInputPorts(AnnotatedGranularDepthPorts originalPorts)
            throws ReaderException {
        Set<InputWorkflowPort> createdPorts = new HashSet<>();
        for (AnnotatedGranularDepthPort originalPort : originalPorts.getPort()) {
            InputWorkflowPort newPort = new InputWorkflowPort(parserState.get().getCurrentWorkflow(),
                    originalPort.getName());
            newPort.setDepth(originalPort.getDepth().intValue());
            if (!originalPort.getGranularDepth().equals(originalPort.getDepth())) {
                String message = "Specific input port granular depth not " + "supported in scufl2, port "
                        + originalPort.getName() + " has depth " + originalPort.getDepth() + " and granular depth "
                        + originalPort.getGranularDepth();
                if (isStrict())
                    throw new ReaderException(message);
                logger.warning(message);
            }
            parseAnnotations(newPort, originalPort.getAnnotations());

            createdPorts.add(newPort);
        }
        return createdPorts;
    }

    protected IterationStrategyStack parseIterationStrategyStack(
            org.apache.taverna.scufl2.xml.t2flow.jaxb.IterationStrategyStack originalStack) throws ReaderException {
        IterationStrategyStack newStack = new IterationStrategyStack();

        for (TopIterationNode strategy : originalStack.getIteration().getStrategy()) {
            IterationNode topNode = strategy.getCross();
            if (topNode == null)
                topNode = strategy.getDot();
            if (topNode == null)
                continue;
            IterationNodeParent parent = (IterationNodeParent) topNode;
            if (parent.getCrossOrDotOrPort().isEmpty())
                continue;
            try {
                newStack.add((IterationStrategyTopNode) parseIterationStrategyNode(topNode));
            } catch (ReaderException e) {
                if (isStrict())
                    throw e;
                logger.warning(e.getMessage());
            }
        }

        return newStack;
    }

    protected IterationStrategyNode parseIterationStrategyNode(IterationNode topNode) throws ReaderException {
        if (topNode instanceof PortProduct) {
            PortProduct portProduct = (PortProduct) topNode;
            PortNode portNode = new PortNode();
            portNode.setDesiredDepth(portProduct.getDepth().intValue());
            String name = portProduct.getName();
            Processor processor = parserState.get().getCurrentProcessor();
            InputProcessorPort inputProcessorPort = processor.getInputPorts().getByName(name);
            portNode.setInputProcessorPort(inputProcessorPort);
            return portNode;
        }

        IterationStrategyNode node;
        if (topNode instanceof DotProduct)
            node = new org.apache.taverna.scufl2.api.iterationstrategy.DotProduct();
        else if (topNode instanceof CrossProduct)
            node = new org.apache.taverna.scufl2.api.iterationstrategy.CrossProduct();
        else
            throw new ReaderException("Invalid node " + topNode);
        @SuppressWarnings("unchecked")
        List<IterationStrategyNode> children = (List<IterationStrategyNode>) node;
        IterationNodeParent parent = (IterationNodeParent) topNode;
        for (IterationNode child : parent.getCrossOrDotOrPort())
            children.add(parseIterationStrategyNode(child));
        return node;
    }

    protected Set<OutputWorkflowPort> parseOutputPorts(AnnotatedPorts originalPorts) throws ReaderException {
        Set<OutputWorkflowPort> createdPorts = new HashSet<>();
        for (AnnotatedPort originalPort : originalPorts.getPort()) {
            OutputWorkflowPort newPort = new OutputWorkflowPort(parserState.get().getCurrentWorkflow(),
                    originalPort.getName());
            parseAnnotations(newPort, originalPort.getAnnotations());
            createdPorts.add(newPort);
        }
        return createdPorts;
    }

    @SuppressWarnings("boxing")
    protected Set<InputProcessorPort> parseProcessorInputPorts(Processor newProc, DepthPorts origPorts) {
        Set<InputProcessorPort> newPorts = new HashSet<>();
        for (DepthPort origPort : origPorts.getPort()) {
            InputProcessorPort newPort = new InputProcessorPort(newProc, origPort.getName());
            newPort.setDepth(origPort.getDepth().intValue());
            // TODO: What about InputProcessorPort granular depth?
            newPorts.add(newPort);
        }
        return newPorts;
    }

    @SuppressWarnings("boxing")
    protected Set<OutputProcessorPort> parseProcessorOutputPorts(Processor newProc, GranularDepthPorts origPorts) {
        Set<OutputProcessorPort> newPorts = new HashSet<>();
        for (GranularDepthPort origPort : origPorts.getPort()) {
            OutputProcessorPort newPort = new OutputProcessorPort(newProc, origPort.getName());
            newPort.setDepth(origPort.getDepth().intValue());
            newPort.setGranularDepth(origPort.getGranularDepth().intValue());
            newPorts.add(newPort);
        }
        return newPorts;
    }

    protected Set<Processor> parseProcessors(Processors originalProcessors) throws ReaderException, JAXBException {
        HashSet<Processor> newProcessors = new HashSet<>();
        for (org.apache.taverna.scufl2.xml.t2flow.jaxb.Processor origProc : originalProcessors.getProcessor()) {
            Processor newProc = new Processor(parserState.get().getCurrentWorkflow(), origProc.getName());
            parserState.get().setCurrentProcessor(newProc);
            newProc.setInputPorts(parseProcessorInputPorts(newProc, origProc.getInputPorts()));
            newProc.setOutputPorts(parseProcessorOutputPorts(newProc, origProc.getOutputPorts()));
            parseDispatchStack(origProc.getDispatchStack());
            newProc.setIterationStrategyStack(parseIterationStrategyStack(origProc.getIterationStrategyStack()));
            parseAnnotations(newProc, origProc.getAnnotations());

            newProcessors.add(newProc);
            int i = 0;
            for (Activity origActivity : origProc.getActivities().getActivity())
                parseActivityBinding(origActivity, i++);
        }
        parserState.get().setCurrentProcessor(null);
        return newProcessors;
    }

    @SuppressWarnings("unchecked")
    public WorkflowBundle parseT2Flow(File t2File) throws IOException, ReaderException, JAXBException {
        JAXBElement<org.apache.taverna.scufl2.xml.t2flow.jaxb.Workflow> root = (JAXBElement<org.apache.taverna.scufl2.xml.t2flow.jaxb.Workflow>) getUnmarshaller()
                .unmarshal(t2File);
        return parseT2Flow(root.getValue());
    }

    @SuppressWarnings("unchecked")
    public WorkflowBundle parseT2Flow(InputStream t2File) throws IOException, JAXBException, ReaderException {
        JAXBElement<org.apache.taverna.scufl2.xml.t2flow.jaxb.Workflow> root = (JAXBElement<org.apache.taverna.scufl2.xml.t2flow.jaxb.Workflow>) getUnmarshaller()
                .unmarshal(t2File);
        return parseT2Flow(root.getValue());
    }

    public WorkflowBundle parseT2Flow(org.apache.taverna.scufl2.xml.t2flow.jaxb.Workflow wf)
            throws ReaderException, JAXBException {
        try {
            parserState.get().setT2FlowParser(this);
            WorkflowBundle wfBundle = new WorkflowBundle();
            parserState.get().setCurrentWorkflowBundle(wfBundle);
            makeProfile(wf);

            // First a skeleton scan of workflows (for nested workflow configs)
            Map<Dataflow, Workflow> dataflowMap = new HashMap<>();
            for (Dataflow df : wf.getDataflow()) {
                Workflow workflow = skeletonDataflow(df);
                dataflowMap.put(df, workflow);
                wfBundle.getWorkflows().addWithUniqueName(workflow);
                workflow.setParent(wfBundle);
                if (df.getRole().equals(TOP)) {
                    wfBundle.setMainWorkflow(workflow);
                    wfBundle.setName(df.getName());
                    wfBundle.setGlobalBaseURI(WORKFLOW_BUNDLE_ROOT.resolve(df.getId() + "/"));
                }
            }
            // Second stage
            for (Dataflow df : wf.getDataflow()) {
                Workflow workflow = dataflowMap.get(df);
                parseDataflow(df, workflow);
            }
            if (isStrict() && wfBundle.getMainWorkflow() == null)
                throw new ReaderException("No main workflow");
            scufl2Tools.setParents(wfBundle);

            return wfBundle;
        } finally {
            parserState.remove();
        }
    }

    protected Workflow skeletonDataflow(Dataflow df) {
        Workflow wf = new Workflow();
        parserState.get().setCurrentWorkflow(wf);
        wf.setName(df.getName());
        wf.setIdentifier(WORKFLOW_ROOT.resolve(df.getId() + "/"));
        return wf;
    }

    public void setStrict(boolean strict) {
        this.strict = strict;
    }
}