You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@taverna.apache.org by st...@apache.org on 2015/02/23 11:05:17 UTC

[32/79] [partial] incubator-taverna-language git commit: Revert "temporarily empty repository"

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/core/Workflow.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/core/Workflow.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/core/Workflow.java
new file mode 100644
index 0000000..0b101c5
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/core/Workflow.java
@@ -0,0 +1,283 @@
+package org.apache.taverna.scufl2.api.core;
+
+/*
+ * 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 java.net.URI;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+import org.apache.taverna.scufl2.api.annotation.Revisioned;
+import org.apache.taverna.scufl2.api.common.AbstractRevisioned;
+import org.apache.taverna.scufl2.api.common.Child;
+import org.apache.taverna.scufl2.api.common.NamedSet;
+import org.apache.taverna.scufl2.api.common.Ported;
+import org.apache.taverna.scufl2.api.common.Visitor;
+import org.apache.taverna.scufl2.api.common.WorkflowBean;
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+import org.apache.taverna.scufl2.api.port.InputWorkflowPort;
+import org.apache.taverna.scufl2.api.port.OutputWorkflowPort;
+
+
+/**
+ * A <code>Workflow</code> is a set of {@link Processor}s and {@link DataLink}s
+ * between the <code>Processor</code>s. <code>Workflow</code>s may also have
+ * input and output ports.
+ * 
+ * @author Alan R Williams
+ * @author Stian Soiland-Reyes
+ */
+public class Workflow extends AbstractRevisioned implements
+		Child<WorkflowBundle>, Ported, Revisioned {
+	public static final URI WORKFLOW_ROOT = URI
+			.create("http://ns.taverna.org.uk/2010/workflow/");
+
+	private final TreeSet<DataLink> dataLinks = new TreeSet<>();
+	private final TreeSet<ControlLink> controlLinks = new TreeSet<>();
+	private final NamedSet<InputWorkflowPort> inputPorts = new NamedSet<>();
+	private final NamedSet<OutputWorkflowPort> outputPorts = new NamedSet<>();
+	private final NamedSet<Processor> processors = new NamedSet<>();
+	private WorkflowBundle parent;
+
+	/**
+	 * Constructs a <code>Workflow</code> with a name based on a random UUID.
+	 */
+	public Workflow() {	
+	}
+
+	/**
+     * Constructs a <code>Workflow</code> with the specified name.
+     * 
+     * @param name
+     *            The name of the <code>Workflow</code>. <strong>Must
+     *            not</strong> be <code>null</code> or an empty String.
+     */
+    public Workflow(String name) {
+        super(name);
+    }
+	
+	@Override
+	public boolean accept(Visitor visitor) {
+		if (visitor.visitEnter(this)) {
+			List<WorkflowBean> children = new ArrayList<>();
+			children.addAll(getInputPorts());
+			children.addAll(getOutputPorts());
+			children.addAll(getProcessors());
+			children.addAll(getDataLinks());
+			children.addAll(getControlLinks());
+			children.add(getCurrentRevision());
+			for (WorkflowBean bean : children)
+				if (!bean.accept(visitor))
+					break;
+		}
+		return visitor.visitLeave(this);
+	}
+
+	/**
+	 * Returns the <code>ControlLink</code>s.
+	 * 
+	 * If there are no <code>ControlLink</code>s an empty set is returned.
+	 * 
+	 * @return the <code>ControlLink</code>s
+	 */
+	public Set<ControlLink> getControlLinks() {
+		return controlLinks;
+	}
+	
+	/**
+	 * Returns the <code>DataLink</code>s.
+	 * 
+	 * If there are no <code>DataLink</code>s an empty set is returned.
+	 * 
+	 * @return the <code>DataLink</code>s.
+	 */
+	public Set<DataLink> getDataLinks() {
+		return dataLinks;
+	}
+
+	/**
+	 * Returns the <code>InputWorkflowPort</code>s.
+	 * 
+	 * If there are no <code>InputWorkflowPort</code>s an empty set is returned.
+	 * 
+	 * @return the <code>InputWorkflowPort</code>s.
+	 */
+	@Override
+	public NamedSet<InputWorkflowPort> getInputPorts() {
+		return inputPorts;
+	}
+
+	/**
+	 * Returns the <code>OutputWorkflowPort</code>s.
+	 * 
+	 * If there are no <code>OutputWorkflowPort</code>s an empty set is
+	 * returned.
+	 * 
+	 * @return the <code>OutputWorkflowPort</code>s.
+	 */
+	@Override
+	public NamedSet<OutputWorkflowPort> getOutputPorts() {
+		return outputPorts;
+	}
+
+	@Override
+	public WorkflowBundle getParent() {
+		return parent;
+	}
+
+	/**
+	 * Returns the <code>Processor</code>s.
+	 * 
+	 * If there are no <code>Processor</code>s an empty set is returned.
+	 * 
+	 * @return the <code>Processor</code>s.
+	 */
+	public NamedSet<Processor> getProcessors() {
+		return processors;
+	}
+
+	/**
+	 * Set the <code>ControlLink</code>s to be the contents of the specified
+	 * set.
+	 * <p>
+	 * <code>ControlLink</code>s can be added by using
+	 * {@link #getControlLinks()}.add(controlLink).
+	 * 
+	 * @param controlLinks
+	 *            the <code>ControlLink</code>s. <strong>Must not</strong> be
+	 *            null
+	 */
+	public void setControlLinks(Set<ControlLink> controlLinks) {
+		this.controlLinks.clear();
+		this.controlLinks.addAll(controlLinks);
+	}
+	
+	/**
+	 * Set the <code>DataLink</code>s to be the contents of the specified set.
+	 * <p>
+	 * <code>DataLink</code>s can be added by using {@link #getDataLinks()}
+	 * .add(dataLink).
+	 * 
+	 * @param dataLinks
+	 *            the <code>DataLink</code>s. <strong>Must not</strong> be null
+	 */
+	public void setDataLinks(Set<DataLink> dataLinks) {
+		this.dataLinks.clear();
+		this.dataLinks.addAll(dataLinks);
+	}
+
+	/**
+	 * Set the <code>InputWorkflowPort</code>s to be the contents of the
+	 * specified set.
+	 * <p>
+	 * <code>InputWorkflowPort</code>s can be added by using
+	 * {@link #getInputWorkflowPorts()}.add(inputPort).
+	 * 
+	 * @param inputPorts
+	 *            the <code>InputWorkflowPort</code>s. <strong>Must not</strong>
+	 *            be null
+	 */
+	public void setInputPorts(Set<InputWorkflowPort> inputPorts) {
+		this.inputPorts.clear();
+		for (InputWorkflowPort inputPort : inputPorts)
+			inputPort.setParent(this);
+	}
+
+	/**
+	 * Set the <code>OutputWorkflowPort</code>s to be the contents of the
+	 * specified set.
+	 * <p>
+	 * <code>OutputWorkflowPort</code>s can be added by using
+	 * {@link #getOutputWorkflowPorts()}.add(outputPort).
+	 * 
+	 * @param outputPorts
+	 *            the <code>OutputWorkflowPort</code>s. <strong>Must
+	 *            not</strong> be null
+	 */
+	public void setOutputPorts(Set<OutputWorkflowPort> outputPorts) {
+		this.outputPorts.clear();
+		for (OutputWorkflowPort outputPort : outputPorts)
+			outputPort.setParent(this);
+	}
+
+	@Override
+	public void setParent(WorkflowBundle parent) {
+		if (this.parent != null && this.parent != parent)
+			this.parent.getWorkflows().remove(this);
+		this.parent = parent;
+		if (parent != null)
+			parent.getWorkflows().add(this);
+	}
+
+	/**
+	 * Set the <code>Processor</code>s to be the contents of the specified set.
+	 * <p>
+	 * <code>Processor</code>s can be added by using {@link #getProcessors()}
+	 * .add(processor).
+	 * 
+	 * @param processors
+	 *            the <code>Processor</code>s. <strong>Must not</strong> be null
+	 */
+	public void setProcessors(Set<Processor> processors) {
+		this.processors.clear();
+		for (Processor processor : processors)
+			processor.setParent(this);
+	}
+
+	/**
+	 * Updates the workflow identifier.
+	 * <p>
+	 * The {@link #getCurrentRevision()} will be replaced using using
+	 * {@link #newRevision()}.
+	 * 
+	 */
+	public void updateWorkflowIdentifier() {
+		newRevision();
+	}
+
+	@SuppressWarnings("unused")
+	private String toString(Collection<?> collection, int maxLen) {
+		StringBuilder builder = new StringBuilder("[");
+		String sep = "";
+		int i = 0;
+		for (Object o : collection) {
+			builder.append(sep).append(o);
+			sep = ", ";
+			if (++i >= maxLen)
+				break;
+		}
+		return builder.append("]").toString();
+	}
+
+	@Override
+	protected URI getIdentifierRoot() {
+		return WORKFLOW_ROOT;
+	}
+	
+	@Override
+	protected void cloneInto(WorkflowBean clone, Cloning cloning) {
+		super.cloneInto(clone, cloning);
+		Workflow cloneWorkflow = (Workflow)clone;
+		cloneWorkflow.setCurrentRevision(cloning.cloneIfNotInCache(getCurrentRevision()));		
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/core/package-info.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/core/package-info.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/core/package-info.java
new file mode 100644
index 0000000..5061365
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/core/package-info.java
@@ -0,0 +1,22 @@
+package org.apache.taverna.scufl2.api.core;
+
+/*
+ * 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.
+ */
+
+

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/IterableComparator.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/IterableComparator.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/IterableComparator.java
new file mode 100644
index 0000000..d7b3972
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/IterableComparator.java
@@ -0,0 +1,116 @@
+package org.apache.taverna.scufl2.api.impl;
+
+/*
+ * 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 java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+/**
+ * A Comparator of Iterables of comparables.
+ * <p>
+ * This comparator compares two {@link Iterable}s by comparing their items.
+ * Iterables are compared on the first items, then on the second items, etc. If
+ * Iterable A is larger than Iterable B, but all the items of B compared 0
+ * (equal) with the corresponding items in A, then the comparator will sort the
+ * smaller Iterable before the bigger Iterable.
+ * <p>
+ * Obviously this comparator would not give predictable results for
+ * {@link Iterable}s which don't have a predictable iteration order, like
+ * {@link HashSet}, but should work well with {@link List}s,
+ * {@link LinkedHashSet}, etc.
+ * <p>
+ * The two Iterables don't need to contain items of the same classes, as long as
+ * their element's {@link Comparable#compareTo(Object)} allow comparison. (For
+ * instance a Iterable&lt;String&gt; would not compare against
+ * Iterable&lt;Integer&gt;). As according to
+ * {@link Comparator#compare(Object, Object)} a {@link ClassCastException} would
+ * be thrown in such cases.
+ * <p>
+ * Items in the Iterable are compared using {@link NullSafeComparator}, so this
+ * comparator will handle Iterables containing <code>null</code> items
+ * predictably. This comparator also allows <code>null</code> in the same way,
+ * so comparison of a {@link Iterable} and <code>null</code> will sort
+ * <code>null</code> before the Iterable.
+ * <p>
+ * If an item in the Iterable is a {@link Iterable} which is not itself
+ * {@link Comparable}, and it is being compared with a different Iterable, it
+ * will be recursively compared with the same instance of
+ * {@link IterableComparator}. However, if such {@link Iterable}s are compared
+ * with other {@link Comparable}s a {@link ClassCastException} would be thrown.
+ * <p>
+ * If an item in a Iterable is not {@link Comparable} <code>null</code> and
+ * could not be recursively compared, a {@link ClassCastException} will be
+ * thrown, as according to {@link Comparator#compare(Object, Object)}
+ * 
+ * @author Stian Soiland-Reyes
+ */
+public class IterableComparator implements Comparator<Iterable<?>> {
+	@Override
+	public int compare(Iterable<?> a, Iterable<?> b) {
+		Integer nullCompare = compareNulls(a, b);
+		if (nullCompare != null)
+			return nullCompare;
+		
+		Iterator<?> aIt = a.iterator();
+		Iterator<?> bIt = b.iterator();
+		
+		while (aIt.hasNext() && bIt.hasNext()) {
+			int itemCompare = compareItems(aIt.next(), bIt.next());
+			if (itemCompare != 0)
+				return itemCompare;
+		}
+		// Puh, compared all corresponding items
+		
+		if (aIt.hasNext())
+			return 1; // a is bigger
+		if (bIt.hasNext())
+			return -1; // a is smaller
+		// Both finished? Then we are equal!
+		return 0;		
+	}
+
+	@SuppressWarnings("unchecked")
+	protected int compareItems(Object a, Object b) {
+		Integer nullCompare = compareNulls(a, b);
+		if (nullCompare != null)
+			return nullCompare;
+		if (a instanceof Comparable && b instanceof Comparable)
+			return ((Comparable<Object>) a).compareTo((Comparable<Object>) b);
+		if (a instanceof Iterable && b instanceof Iterable)
+			// Recurse
+			return compare((Iterable<?>) a, (Iterable<?>) b);
+		throw new ClassCastException(
+				"Compared items must be null, or both be Comparable or Iterables");
+	}
+
+	protected Integer compareNulls(Object a, Object b) {
+		if (a == null && b == null)
+			return 0;
+		if (a == null && b != null)
+			return -1;
+		if (a != null && b == null)
+			return 1;
+		return null;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/LazyMap.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/LazyMap.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/LazyMap.java
new file mode 100644
index 0000000..4ec5ce7
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/LazyMap.java
@@ -0,0 +1,79 @@
+package org.apache.taverna.scufl2.api.impl;
+
+/*
+ * 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 java.util.Comparator;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.TreeMap;
+
+/**
+ * A lazy TreeMap, inspired by org.apache.commons.collections.map.LazyMap
+ * <p>
+ * On {@link #get(Object)}, if a key is not found in the map,
+ * {@link #getDefault(Object)} will be called to create the value for the given
+ * key. This value is subsequently inserted into the map before being returned.
+ * <p>
+ * Call {@link #containsKey(Object)} to check if a key is in the map.
+ * <p>
+ * Implementations of this class must implement {@link #getDefault(Object)} to
+ * specify the default value to create.
+ * 
+ * @author Stian Soiland-Reyes
+ * 
+ * @param <KeyType>
+ *            Type of key
+ * @param <ValueType>
+ *            Type of value
+ */
+public abstract class LazyMap<KeyType, ValueType> extends
+		TreeMap<KeyType, ValueType> implements Map<KeyType, ValueType> {
+	private static final long serialVersionUID = 3284689384208221667L;
+
+	public LazyMap() {
+		super();
+	}
+
+	public LazyMap(Comparator<? super KeyType> comparator) {
+		super(comparator);
+	}
+
+	public LazyMap(Map<? extends KeyType, ? extends ValueType> m) {
+		super(m);
+	}
+
+	public LazyMap(SortedMap<KeyType, ? extends ValueType> m) {
+		super(m);
+	}
+
+	@SuppressWarnings("unchecked")
+	@Override
+	public ValueType get(Object key) {
+		ValueType value = super.get(key);
+		if (value == null) {
+			value = getDefault((KeyType) key);
+			put((KeyType) key, value);
+		}
+		return value;
+	}
+
+	public abstract ValueType getDefault(KeyType key);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/NullSafeComparator.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/NullSafeComparator.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/NullSafeComparator.java
new file mode 100644
index 0000000..b8d39c3
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/impl/NullSafeComparator.java
@@ -0,0 +1,105 @@
+package org.apache.taverna.scufl2.api.impl;
+
+/*
+ * 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 java.util.Comparator;
+
+/**
+ * A Comparator that is null-safe
+ * <p>
+ * This comparator performs {@link Comparable#compareTo(Object)} if both objects
+ * are non-null, and sorts <code>null</code> to be -1 (before the non-null), and
+ * compares two <code>null</code>s as equal.
+ * </p>
+ * <p>
+ * The static method {@link #compareObjects(Object, Object)} can be used if such
+ * a comparison is needed without having to instantiate this class, for instance
+ * because it is used within a {@link Comparable#compareTo(Object)} or because
+ * the two arguments are not of an agreeable subclass &lt<T&gt; of
+ * {@link Comparable} (but still can be compared).
+ * 
+ * @author Stian Soiland-Reyes
+ * @param <T>
+ *            The common type of the objects to be compared.
+ */
+public class NullSafeComparator<T extends Comparable<T>> implements
+		Comparator<T> {
+	/**
+	 * Compare two objects for <code>null</code>ity only.
+	 * <p>
+	 * If both parameters are <code>null</code>, return <code>0</code>. If only a
+	 * is <code>null</code>, return <code>-1</code>, if only b is
+	 * <code>null</code>, return <code>1</code>. If none are <code>null</code>,
+	 * return <code>null</code>.
+	 * </p>
+	 * The <code>null</code> return might sound counter-intuitive, but it
+	 * basically means that the objects could not be compared on nullity alone,
+	 * and must be further compared.
+	 * 
+	 * @param a
+	 *            First object to compare
+	 * @param b
+	 *            Second object to compare
+	 * @return 0 if both are null, -1 if a is null, 1 if b is null, otherwise
+	 *         <code>null</code>.
+	 */
+	public static Integer nullCompare(Object a, Object b) {
+		if (a == null && b == null)
+			return 0;
+		if (a == null && b != null)
+			return -1;
+		if (a != null && b == null)
+			return 1;
+		return null;
+	}
+
+	/**
+	 * Compare any two objects, null-safe.
+	 * <p>
+	 * If any of the parameters are <code>null</code>, results are returned as
+	 * for {@link #nullCompare(Object, Object)}. Otherwise,
+	 * {@link Comparable#compareTo(Object)} is called on the first parameter
+	 * against the second.
+	 * 
+	 * @param a
+	 *            First object to compare. Must be instance of
+	 *            {@link Comparable}.
+	 * @param b
+	 *            Second object to compare. Must be
+	 * @return 0 if both are null, -1 if a is null, 1 if b is null, otherwise
+	 *         the result of a.compareTo(b).
+	 * @throws ClassCastException
+	 *             if the specified object's type prevents it from being
+	 *             compared to this object.
+	 */
+	@SuppressWarnings("unchecked")
+	public static int compareObjects(Object a, Object b) {
+		Integer diff = nullCompare(a, b);
+		if (diff != null)
+			return diff;
+		return ((Comparable<Object>) a).compareTo(b);
+	}
+
+	@Override
+	public int compare(T a, T b) {
+		return compareObjects(a, b);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/ReaderException.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/ReaderException.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/ReaderException.java
new file mode 100644
index 0000000..553c0ff
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/ReaderException.java
@@ -0,0 +1,74 @@
+package org.apache.taverna.scufl2.api.io;
+
+/*
+ * 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 org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+/**
+ * Thrown when there is a problem reading a {@link WorkflowBundle}
+ * 
+ * @see WorkflowBundleIO#readBundle(java.io.File, String)
+ * @see WorkflowBundleIO#readBundle(java.io.InputStream, String)
+ * @see WorkflowBundleIO#readBundle(java.net.URL, String)
+ * @see WorkflowBundleReader#readBundle(java.io.File, String)
+ * @see WorkflowBundleReader#readBundle(java.io.InputStream, String)
+ */
+@SuppressWarnings("serial")
+public class ReaderException extends Exception {
+	/**
+	 * Constructs an exception with no message or cause.
+	 */
+	public ReaderException() {
+	}
+
+	/**
+	 * Constructs an exception with the specified message and no cause.
+	 * 
+	 * @param message
+	 *            details about the exception. Can be <code>null</code>
+	 */
+	public ReaderException(String message) {
+		super(message);
+	}
+
+	/**
+	 * Constructs an exception with the specified message and cause.
+	 * 
+	 * @param message
+	 *            details about the exception. Can be <code>null</code>
+	 * @param cause
+	 *            the cause of the exception. Can be <code>null</code>
+	 */
+	public ReaderException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * Constructs an exception with the specified cause and and the same message
+	 * as the cause (if the cause is not null).
+	 * 
+	 * @param cause
+	 *            the cause of the exception. Can be <code>null</code>
+	 */
+	public ReaderException(Throwable cause) {
+		super(cause);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleIO.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleIO.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleIO.java
new file mode 100644
index 0000000..d8cb24f
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleIO.java
@@ -0,0 +1,538 @@
+package org.apache.taverna.scufl2.api.io;
+
+/*
+ * 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 java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.URL;
+import java.net.URLConnection;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.List;
+import java.util.ServiceLoader;
+import java.util.Set;
+import java.util.TreeSet;
+import java.util.logging.Logger;
+
+import org.apache.taverna.scufl2.api.annotation.Revisioned;
+import org.apache.taverna.scufl2.api.common.Scufl2Tools;
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+import org.apache.taverna.scufl2.api.core.Workflow;
+import org.apache.taverna.scufl2.api.profiles.Profile;
+
+
+/**
+ * Utility class for reading and writing <code>WorkflowBundle</code>s.
+ * <p>
+ * This class depends on implemented {@link WorkflowBundleReader} and
+ * {@link WorkflowBundleWriter} instances, which are discovered from the
+ * classpath using {@link ServiceLoader} or set using {@link #setReaders(List)}
+ * and {@link #setWriters(List)}. An OSGi service descriptors is provided for
+ * instantiating this class as bean <code>workflowBundleIO</code>, while
+ * non-OSGi uses can just instantiate this class where needed.
+ * <p>
+ * The methods {@link #readBundle(File, String)},
+ * {@link #readBundle(InputStream, String)}, {@link #readBundle(URL, String)}
+ * and {@link #writeBundle(WorkflowBundle, File, String)}/
+ * {@link #writeBundle(WorkflowBundle, OutputStream, String)} take an argument
+ * to indicate the media type of the format. The reader methods from file and
+ * URL allow the parameter to be <code>null</code> in order to guess the format,
+ * but the writer method requires the format to be specified explicitly.
+ * <p>
+ * Known supported formats (as of 2013-04-23):
+ * <dl>
+ * <dt>text/vnd.taverna.scufl2.structure</dt>
+ * <dd>A textual tree-view, useful for debugging, but probably incomplete for
+ * actual workflow execution. Reader and writer provided by scufl2-api (this
+ * module).</dd>
+ * <dt>application/vnd.taverna.scufl2.workflow-bundle</dt>
+ * <dd>The <a href=
+ * 'http://dev.mygrid.org.uk/wiki/display/developer/Taverna+Workflow+Bundle'>SCU
+ * F L 2 workflow bundle</a> format, a ZIP container of RDF/XML files. Reader
+ * and writer provided by the scufl2-rdfxml module.</dd>
+ * <dt>application/vnd.taverna.t2flow+xml</dt>
+ * <dd>The Taverna 2 workflow format t2flow. An XML format based on XMLBeans
+ * serialization of T2 java objects. Reader provided by the scufl2-t2flow
+ * module.
+ * <dt>application/vnd.taverna.scufl+xml</dt>
+ * <dd>The Taverna 1 workflow format SCUFL. An XML format made for the FreeFluo
+ * workflow engine. Experimental reader provided by the scufl2-scufl module.
+ * <dt>text/vnd.wf4ever.wfdesc+turtle</dt>
+ * <dd>An abstract workflow structure format by the <a
+ * href="http://www.wf4ever-project.org/">Wf4Ever project. RDF Turtle according
+ * to the <a href="http://purl.org/wf4ever/model">wfdesc ontology</a>. Writer
+ * provided by the third-party <a
+ * href="https://github.com/wf4ever/scufl2-wfdesc">scufl2-wfdesc</a> module.
+ * </dl>
+ */
+public class WorkflowBundleIO {
+	private static Logger log = Logger.getLogger(WorkflowBundleIO.class
+			.getCanonicalName());
+
+	private static final Scufl2Tools scufl2Tools = new Scufl2Tools();
+	// delay initialising the ServiceLoaders
+	protected ServiceLoader<WorkflowBundleWriter> writersLoader;
+	protected ServiceLoader<WorkflowBundleReader> readersLoader;
+
+	private List<WorkflowBundleWriter> writers;
+
+	private List<WorkflowBundleReader> readers;
+
+	protected List<WorkflowBundleReader> discoverReaders() {
+		synchronized (this) {
+			if (readersLoader == null)
+				/*
+				 * FIXME: This uses Thread.currentThread.getContextClassLoader()
+				 * - depending on who gets synchronized-block first this can
+				 * vary - but as it's still local per instance of
+				 * WorkflowBundleIO it should be OK for now.
+				 */
+				readersLoader = ServiceLoader.load(WorkflowBundleReader.class);
+		}
+
+		List<WorkflowBundleReader> allReaders = new ArrayList<>();
+		for (WorkflowBundleReader reader : readersLoader)
+			allReaders.add(reader);
+		return allReaders;
+	}
+
+	protected List<WorkflowBundleWriter> discoverWriters() {
+		synchronized (this) {
+			if (writersLoader == null)
+				/*
+				 * FIXME: This uses Thread.currentThread.getContextClassLoader()
+				 * - depending on who gets synchronized-block first this can
+				 * vary - but as it's still local per instance of
+				 * WorkflowBundleIO it should be OK for now.
+				 */
+				writersLoader = ServiceLoader.load(WorkflowBundleWriter.class);
+		}
+		List<WorkflowBundleWriter> allWriters = new ArrayList<>();
+		for (WorkflowBundleWriter writer : writersLoader)
+			allWriters.add(writer);
+		return allWriters;
+	}
+
+	/**
+	 * Returns a <code>WorkflowBundleReader</code> for the specified media type.
+	 * 
+	 * If there is more than one <code>WorkflowBundleReader</code> for the
+	 * specified media type the first reader discovered is returned. Subsequent
+	 * calls to this method may return a different reader.
+	 * 
+	 * If there is no <code>WorkflowBundleReader</code> for the specified media
+	 * type <code>null</code> is returned.
+	 * 
+	 * @param mediaType
+	 *            the media type of the <code>WorkflowBundleReader</code>
+	 * @return a <code>WorkflowBundleReader</code> for the specified media type
+	 */
+	public WorkflowBundleReader getReaderForMediaType(String mediaType) {
+		for (WorkflowBundleReader reader : getReaders())
+			if (reader.getMediaTypes().contains(mediaType))
+				return reader;
+		return null;
+	}
+
+	/**
+	 * Returns all the available <code>WorkflowBundleReader</code>s.
+	 * 
+	 * @return all the available <code>WorkflowBundleReader</code>s
+	 */
+	public List<WorkflowBundleReader> getReaders() {
+		if (readers == null)
+			return discoverReaders();
+		return readers;
+	}
+
+	/**
+	 * Get the supported media types for reading.
+	 * <p>
+	 * Returned media types can be used with {@link #readBundle(File, String)},
+	 * {@link #readBundle(InputStream, String)} and/or
+	 * {@link #readBundle(URL, String)}.
+	 * 
+	 * @return A (usually sorted) set of media types
+	 */
+	public Set<String> getSupportedReaderMediaTypes() {
+		Set<String> mediaTypes = new TreeSet<>();
+		for (WorkflowBundleReader reader : getReaders())
+			mediaTypes.addAll(reader.getMediaTypes());
+		return mediaTypes;
+	}
+
+	/**
+	 * Get the supported media types for writing.
+	 * <p>
+	 * Returned media types can be used with
+	 * {@link #writeBundle(WorkflowBundle, File, String)} and/or
+	 * {@link #writeBundle(WorkflowBundle, OutputStream, String)}.
+	 * 
+	 * @return A (usually sorted) set of media types
+	 */
+	public Set<String> getSupportedWriterMediaTypes() {
+		Set<String> mediaTypes = new TreeSet<>();
+		for (WorkflowBundleWriter writer : getWriters())
+			mediaTypes.addAll(writer.getMediaTypes());
+		return mediaTypes;
+	}
+
+	/**
+	 * Returns a <code>WorkflowBundleWriter</code> for the specified media type.
+	 * 
+	 * If there is more than one <code>WorkflowBundleWriter</code> for the
+	 * specified media type the first writer discovered is returned. Subsequent
+	 * calls to this method may return a different writer.
+	 * 
+	 * If there is no <code>WorkflowBundleWriter</code> for the specified media
+	 * type <code>null</code> is returned.
+	 * 
+	 * @param mediaType
+	 *            the media type of the <code>WorkflowBundleWriter</code>
+	 * @return a <code>WorkflowBundleWriter</code> for the specified media type
+	 */
+	public WorkflowBundleWriter getWriterForMediaType(String mediaType) {
+		for (WorkflowBundleWriter writer : getWriters())
+			if (writer.getMediaTypes().contains(mediaType))
+				return writer;
+		return null;
+	}
+
+	/**
+	 * Returns all the available <code>WorkflowBundleWriter</code>s.
+	 * 
+	 * @return all the available <code>WorkflowBundleWriter</code>s
+	 */
+	public List<WorkflowBundleWriter> getWriters() {
+		if (writers == null)
+			return discoverWriters();
+		return writers;
+	}
+
+	/**
+	 * Attempt to guess the media type for a stream or file that starts with
+	 * these bytes.
+	 * <p>
+	 * All registered {@link #getReaders()} are consulted.
+	 * <p>
+	 * Return <code>null</code> if ambiguous (more than one possibility) or
+	 * unknown.
+	 * 
+	 * @param firstBytes
+	 *            The initial bytes, at least 512 bytes long unless the resource
+	 *            is smaller.
+	 * @return The recognised media type, or <code>null</code> if the bytes were
+	 *         ambiguous or unknown.
+	 */
+	public String guessMediaTypeForSignature(byte[] firstBytes) {
+		Set<String> mediaTypes = new HashSet<>();
+		for (WorkflowBundleReader reader : getReaders()) {
+			String guess = reader.guessMediaTypeForSignature(firstBytes);
+			if (guess != null)
+				mediaTypes.add(guess);
+		}
+		if (mediaTypes.isEmpty())
+			return null;
+		if (mediaTypes.size() > 1) {
+			log.warning("Multiple media types found: " + mediaTypes);
+			return null;
+		}
+		return mediaTypes.iterator().next();
+	}
+
+	/**
+	 * Reads a file containing a workflow bundle in the specified media type and
+	 * returns a <code>WorkflowBundle</code>.
+	 * 
+	 * @param bundleFile
+	 *            the file containing the workflow bundle
+	 * @param mediaType
+	 *            the media type of the workflow bundle. A
+	 *            <code>WorkflowBundleReader</code> must exist for this media
+	 *            type. If <code>null</code>, the media type will be guessed as
+	 *            with {@link #guessMediaTypeForSignature(byte[])}.
+	 * @return the <code>WorkflowBundle</code> read from the file
+	 * @throws ReaderException
+	 *             if there is an error parsing the workflow bundle
+	 * @throws IOException
+	 *             if there is an error reading the file
+	 * @throws IllegalArgumentException
+	 *             if a <code>WorkflowBundleReader</code> cannot be found for
+	 *             the media type
+	 */
+	public WorkflowBundle readBundle(File bundleFile, String mediaType)
+			throws ReaderException, IOException {
+		if (mediaType == null) {
+			byte[] firstBytes = new byte[1024];
+			try (FileInputStream fileIn = new FileInputStream(bundleFile)) {
+				fileIn.read(firstBytes);
+			}
+			mediaType = guessMediaTypeForSignature(firstBytes);
+		}
+		WorkflowBundleReader reader = getReaderForMediaType(mediaType);
+		if (reader == null) {
+			if (mediaType == null)
+				throw new IllegalArgumentException(
+						"Could not guess media type for " + bundleFile);
+			throw new IllegalArgumentException(
+					"Could not find reader for media type " + mediaType);
+		}
+		return reader.readBundle(bundleFile, mediaType);
+	}
+
+	/**
+	 * Reads a stream containing a workflow bundle in the specified media type
+	 * and returns a <code>WorkflowBundle</code>.
+	 * 
+	 * @param inputStream
+	 *            the stream containing the workflow bundle
+	 * @param mediaType
+	 *            the media type of the workflow bundle. A
+	 *            <code>WorkflowBundleReader</code> must exist for this media
+	 *            type. If <code>null</code>, the media type will be guessed as
+	 *            with {@link #guessMediaTypeForSignature(byte[])}.
+	 * @return the <code>WorkflowBundle</code> read from the stream
+	 * @throws ReaderException
+	 *             if there is an error parsing the workflow bundle
+	 * @throws IOException
+	 *             if there is an error reading from the stream
+	 * @throws IllegalArgumentException
+	 *             if a <code>WorkflowBundleReader</code> cannot be found for
+	 *             the media type
+	 */
+	public WorkflowBundle readBundle(InputStream inputStream, String mediaType)
+			throws ReaderException, IOException {
+		if (mediaType == null) {
+			byte[] firstBytes = new byte[1024];
+			inputStream = new BufferedInputStream(inputStream);
+			try {
+				inputStream.mark(firstBytes.length * 2);
+				inputStream.read(firstBytes);
+				mediaType = guessMediaTypeForSignature(firstBytes);
+			} finally {
+				inputStream.reset();
+				// Important = so readBundle can start from the beginning
+			}
+		}
+		WorkflowBundleReader reader = getReaderForMediaType(mediaType);
+		if (reader == null) {
+			if (mediaType == null)
+				throw new IllegalArgumentException(
+						"Could not guess media type for input stream");
+			throw new IllegalArgumentException(
+					"Could not find reader for media type " + mediaType);
+		}
+		return reader.readBundle(inputStream, mediaType);
+	}
+
+	/**
+	 * Reads a URL containing a workflow bundle in the specified media type and
+	 * returns a <code>WorkflowBundle</code>.
+	 * 
+	 * @param url
+	 *            the URL containing the workflow bundle
+	 * @param mediaType
+	 *            the media type of the workflow bundle. A
+	 *            <code>WorkflowBundleReader</code> must exist for this media
+	 *            type If <code>null</code>, the media type will be guessed as
+	 *            with {@link #guessMediaTypeForSignature(byte[])}.
+	 * @return the <code>WorkflowBundle</code> read from the URL
+	 * @throws ReaderException
+	 *             if there is an error parsing the workflow bundle
+	 * @throws IOException
+	 *             if there is an error reading from the stream
+	 * @throws IllegalArgumentException
+	 *             if a <code>WorkflowBundleReader</code> cannot be found for
+	 *             the media type
+	 */
+	public WorkflowBundle readBundle(URL url, String mediaType)
+			throws ReaderException, IOException {
+		URLConnection connection = url.openConnection();
+		if (mediaType != null && !mediaType.isEmpty())
+			addAcceptHeaders(mediaType, connection);
+		else
+			for (String supportedType : getSupportedReaderMediaTypes()) {
+				addAcceptHeaders(supportedType, connection);
+				connection.addRequestProperty("Accept", "*/*;q=0.1");
+			}
+
+		try (InputStream inputStream = connection.getInputStream()) {
+			String contentType = connection.getContentType();
+			List<String> ignoreTypes = Arrays.asList("text/plain",
+					"application/octet-stream", "application/zip",
+					"application/x-zip-compressed", "text/xml",
+					"application/xml", "content/unknown");
+			if (contentType == null || contentType.isEmpty())
+				contentType = mediaType; // might still be null -> guess
+			else
+				for (String ignore : ignoreTypes)
+					if (contentType.toLowerCase().startsWith(ignore))
+						contentType = mediaType; // might still be null -> guess
+			// TODO: Pass URL to reader (as baseURI)
+			return readBundle(url.openStream(), contentType);
+		}
+	}
+
+	private void addAcceptHeaders(String mediaType, URLConnection connection) {
+		connection.addRequestProperty("Accept", mediaType);
+
+		if (mediaType.endsWith("+zip")
+				|| mediaType.equals("vnd.taverna.scufl2.workflow-bundle")) {
+			connection.addRequestProperty("Accept", "application/zip;q=0.5");
+			connection.addRequestProperty("Accept",
+					"application/x-zip-compressed;q=0.5");
+		} else if (mediaType.endsWith("+xml")) {
+			connection.setRequestProperty("Accept", "application/xml;q=0.6");
+			connection.setRequestProperty("Accept", "text/xml;q=0.5");
+		}
+	}
+
+	/**
+	 * Sets the <code>WorkflowBundleReader</code>s.
+	 * <p>
+	 * This method will normally be called by Spring when wiring beans.
+	 * 
+	 * @param readers
+	 *            the <code>WorkflowBundleReader</code>s
+	 */
+	public void setReaders(List<WorkflowBundleReader> readers) {
+		this.readers = readers;
+	}
+
+	/**
+	 * Sets the <code>WorkflowBundleWriter</code>s.
+	 * <p>
+	 * This method will normally be called by Spring when wiring beans.
+	 * 
+	 * @param readers
+	 *            the <code>WorkflowBundleWriter</code>s
+	 */
+	public void setWriters(List<WorkflowBundleWriter> writers) {
+		this.writers = writers;
+	}
+
+	/**
+	 * Write a <code>WorkflowBundle</code> to a file with specified media type.
+	 * <p>
+	 * {@link Scufl2Tools#setParents(WorkflowBundle)} will be called on the
+	 * bundle to ensure everything contained by the bundle has it as an
+	 * ancestor.
+	 * 
+	 * @param wfBundle
+	 *            the workflow bundle to write
+	 * @param destination
+	 *            the file to write the workflow bundle to
+	 * @param mediaType
+	 *            the media type to write workflow bundle in. A
+	 *            <code>WorkflowBundleWriter</code> must exist for this media
+	 *            type
+	 * @throws WriterException
+	 *             if there is an error writing the workflow bundle
+	 * @throws IOException
+	 *             if there is an error writing the file
+	 * @throws IllegalArgumentException
+	 *             if a <code>WorkflowBundleWriter</code> cannot be found for
+	 *             the media type
+	 */
+	public void writeBundle(WorkflowBundle wfBundle, File destination,
+			String mediaType) throws WriterException, IOException {
+		WorkflowBundleWriter writer = getWriterForMediaType(mediaType);
+		if (writer == null)
+			throw new IllegalArgumentException(
+					"Could not find writer for media type " + mediaType);
+		scufl2Tools.setParents(wfBundle);
+		writer.writeBundle(wfBundle, destination, mediaType);
+	}
+
+	/**
+	 * Write a <code>WorkflowBundle</code> to a stream with specified media
+	 * type.
+	 * <p>
+	 * {@link Scufl2Tools#setParents(WorkflowBundle)} will be called on the
+	 * bundle to ensure everything contained by the bundle has it as an
+	 * ancestor.
+	 * 
+	 * @param wfBundle
+	 *            the workflow bundle to write
+	 * @param output
+	 *            the stream to write the workflow bundle to
+	 * @param mediaType
+	 *            the media type to write workflow bundle in. A
+	 *            <code>WorkflowBundleWriter</code> must exist for this media
+	 *            type
+	 * @throws WriterException
+	 *             if there is an error writing the workflow bundle
+	 * @throws IOException
+	 *             if there is an error writing to the stream
+	 * @throws IllegalArgumentException
+	 *             if a <code>WorkflowBundleWriter</code> cannot be found for
+	 *             the media type
+	 */
+	public void writeBundle(WorkflowBundle wfBundle, OutputStream output,
+			String mediaType) throws WriterException, IOException {
+		WorkflowBundleWriter writer = getWriterForMediaType(mediaType);
+		if (writer == null)
+			throw new IllegalArgumentException(
+					"Could not find writer for media type " + mediaType);
+		scufl2Tools.setParents(wfBundle);
+		writer.writeBundle(wfBundle, output, mediaType);
+	}
+
+	/**
+	 * Create a new WorkflowBundle with a default workflow and profile.
+	 * <p>
+	 * Unlike the {@link WorkflowBundle} constructor, this method will also make
+	 * a {@link WorkflowBundle#getMainWorkflow()} and
+	 * {@link WorkflowBundle#getMainProfile()}, simplifying construction of
+	 * workflow bundles from scratch.
+	 * <p>
+	 * Each of the bundle, workflow and profile will also have a revision set
+	 * using {@link Revisioned#newRevision()} and their names set to short
+	 * default values.
+	 * 
+	 * @return A template {@link WorkflowBundle} which has a main workflow and
+	 *         main profile
+	 */
+	public WorkflowBundle createBundle() {
+		WorkflowBundle wb = new WorkflowBundle();
+		wb.setName("bundle1");
+
+		Workflow workflow = new Workflow();
+		workflow.setName("workflow1");
+		workflow.setParent(wb);
+		workflow.newRevision();
+
+		Profile profile = new Profile();
+		profile.setName("profile1");
+		profile.setParent(wb);
+		profile.newRevision();
+
+		wb.setMainWorkflow(workflow);
+		wb.setMainProfile(profile);
+		wb.newRevision();
+		return wb;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleReader.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleReader.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleReader.java
new file mode 100644
index 0000000..889ca43
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleReader.java
@@ -0,0 +1,110 @@
+package org.apache.taverna.scufl2.api.io;
+
+/*
+ * 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 java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Set;
+
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+
+/**
+ * A reader for {@link WorkflowBundle}s.
+ * 
+ * Implementations specify workflow bundle formats (media types/mime types) that
+ * they can read.
+ */
+public interface WorkflowBundleReader {
+	/**
+	 * Return the media types that this reader can read.
+	 * <p>
+	 * Returned media types are must be valid <code>mediaType</code> arguments
+	 * to {@link #readBundle(File, String)} and/or
+	 * {@link #readBundle(InputStream, String)} for this reader.
+	 * <p>
+	 * It is recommended, but not required, that the reader can also recognise
+	 * those media types from {@link #guessMediaTypeForSignature(byte[])}.
+	 * <p>
+	 * If the returned set is empty, the reader should should be able to
+	 * Recognise at least one media type from
+	 * {@link #guessMediaTypeForSignature(byte[])}.
+	 * 
+	 * @return the media types that this reader can read, or an empty set if
+	 *         this reader can't read any bundle formats.
+	 */
+	Set<String> getMediaTypes();
+
+	/**
+	 * Read a file containing a workflow bundle in the specified media type and
+	 * return a <code>WorkflowBundle</code>.
+	 * 
+	 * @param bundleFile
+	 *            the file containing the workflow bundle
+	 * @param mediaType
+	 *            the media type of the workflow bundle
+	 * @return the <code>WorkflowBundle</code> read from the file
+	 * @throws ReaderException
+	 *             if there is an error parsing the workflow bundle
+	 * @throws IOException
+	 *             if there is an error reading the file
+	 */
+	WorkflowBundle readBundle(File bundleFile, String mediaType)
+			throws ReaderException, IOException;
+
+	/**
+	 * Read a stream containing a workflow bundle in the specified media type
+	 * and return a <code>WorkflowBundle</code>.
+	 * 
+	 * @param inputStream
+	 *            the stream containing the workflow bundle
+	 * @param mediaType
+	 *            the media type of the workflow bundle
+	 * @return the <code>WorkflowBundle</code> read from the stream
+	 * @throws ReaderException
+	 *             if there is an error parsing the workflow bundle
+	 * @throws IOException
+	 *             if there is an error reading from the stream
+	 */
+	WorkflowBundle readBundle(InputStream inputStream, String mediaType)
+			throws ReaderException, IOException;
+
+	/**
+	 * Attempt to guess the media type for a stream or file that starts with
+	 * these bytes.
+	 * <p>
+	 * Return <code>null</code> if ambiguous (more than one possibility) or
+	 * unknown.
+	 * <p>
+	 * Typically a WorkflowBundleReader should be able to recognise the same
+	 * types as those listed in {@link #getMediaTypes()}, but this is no
+	 * requirement. A WorkflowBundleReader could also recognise types not listed
+	 * in its {@link #getMediaTypes()}.
+	 * 
+	 * @param firstBytes
+	 *            The initial bytes, at least 512 bytes long unless the resource
+	 *            is smaller.
+	 * @return The recognised media type, or <code>null</code> if the bytes were
+	 *         ambiguous or unknown.
+	 */
+	String guessMediaTypeForSignature(byte[] firstBytes);
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleWriter.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleWriter.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleWriter.java
new file mode 100644
index 0000000..a74127f
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WorkflowBundleWriter.java
@@ -0,0 +1,79 @@
+package org.apache.taverna.scufl2.api.io;
+
+/*
+ * 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 java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.util.Set;
+
+import org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+
+/**
+ * A writer for {@link WorkflowBundle}s.
+ * 
+ * Implementations specify workflow bundle formats (media types) that they can
+ * write.
+ */
+public interface WorkflowBundleWriter {
+	/**
+	 * Returns the media types that this writer can handle.
+	 * 
+	 * @return the media types that this writer can handle
+	 */
+	Set<String> getMediaTypes();
+
+	/**
+	 * Writes a <code>WorkflowBundle</code> to a file with specified media type.
+	 * 
+	 * @param wfBundle
+	 *            the workflow bundle to write
+	 * @param destination
+	 *            the file to write the workflow bundle to
+	 * @param mediaType
+	 *            the media type to write workflow bundle in
+	 * @throws WriterException
+	 *             if there is an error writing the workflow bundle
+	 * @throws IOException
+	 *             if there is an error writing the file
+	 */
+	void writeBundle(WorkflowBundle wfBundle, File destination, String mediaType)
+			throws WriterException, IOException;
+
+	/**
+	 * Writes a <code>WorkflowBundle</code> to a stream with specified media
+	 * type.
+	 * 
+	 * @param wfBundle
+	 *            the workflow bundle to write
+	 * @param output
+	 *            the stream to write the workflow bundle to
+	 * @param mediaType
+	 *            the media type to write workflow bundle in
+	 * @throws WriterException
+	 *             if there is an error writing the workflow bundle
+	 * @throws IOException
+	 *             if there is an error writing to the stream
+	 */
+	void writeBundle(WorkflowBundle wfBundle, OutputStream output,
+			String mediaType) throws WriterException, IOException;
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WriterException.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WriterException.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WriterException.java
new file mode 100644
index 0000000..f316a43
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/WriterException.java
@@ -0,0 +1,76 @@
+package org.apache.taverna.scufl2.api.io;
+
+/*
+ * 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 org.apache.taverna.scufl2.api.container.WorkflowBundle;
+
+/**
+ * Thrown when there is a problem writing a {@link WorkflowBundle}
+ * 
+ * @see WorkflowBundleIO#writeBundle(WorkflowBundle, java.io.File, String)
+ * @see WorkflowBundleIO#writeBundle(WorkflowBundle, java.io.OutputStream,
+ *      String)
+ * @see WorkflowBundleWriter#writeBundle(WorkflowBundle, java.io.File, String)
+ * @see WorkflowBundleWriter#writeBundle(WorkflowBundle, java.io.OutputStream,
+ *      String)
+ */
+@SuppressWarnings("serial")
+public class WriterException extends Exception {
+
+	/**
+	 * Constructs an exception with no message or cause.
+	 */
+	public WriterException() {
+	}
+
+	/**
+	 * Constructs an exception with the specified message and no cause.
+	 * 
+	 * @param message
+	 *            details about the exception. Can be <code>null</code>
+	 */
+	public WriterException(String message) {
+		super(message);
+	}
+
+	/**
+	 * Constructs an exception with the specified message and cause.
+	 * 
+	 * @param message
+	 *            details about the exception. Can be <code>null</code>
+	 * @param cause
+	 *            the cause of the exception. Can be <code>null</code>
+	 */
+	public WriterException(String message, Throwable cause) {
+		super(message, cause);
+	}
+
+	/**
+	 * Constructs an exception with the specified cause and and the same message
+	 * as the cause (if the cause is not null).
+	 * 
+	 * @param cause
+	 *            the cause of the exception. Can be <code>null</code>
+	 */
+	public WriterException(Throwable cause) {
+		super(cause);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/structure/StructureReader.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/structure/StructureReader.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/structure/StructureReader.java
new file mode 100644
index 0000000..c19444b
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/structure/StructureReader.java
@@ -0,0 +1,452 @@
+package org.apache.taverna.scufl2.api.io.structure;
+
+/*
+ * 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.lang.System.arraycopy;
+import static java.util.Collections.singleton;
+
+import java.io.BufferedInputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.Reader;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.util.Scanner;
+import java.util.Set;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import org.apache.taverna.scufl2.api.activity.Activity;
+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.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.io.WorkflowBundleReader;
+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 com.fasterxml.jackson.core.JsonParser;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+/**
+ * A <code>WorkflowBundleReader</code> that reads a {@link WorkflowBundle} in
+ * Scufl2 Structure format.
+ */
+public class StructureReader implements WorkflowBundleReader {
+	public enum Level {
+		WorkflowBundle, Workflow, Processor, Activity, Links, Profile, Configuration, ProcessorBinding, OutputPortBindings, InputPortBindings, JSON, Controls
+	}
+
+	private static final String ACTIVITY_SLASH = "activity/";
+	public static final String TEXT_VND_TAVERNA_SCUFL2_STRUCTURE = "text/vnd.taverna.scufl2.structure";
+
+	private WorkflowBundle wb;
+	private Level level;
+	private String mainWorkflow;
+	private Workflow workflow;
+	private Processor processor;
+	private Activity activity;
+	Pattern linkPattern = Pattern
+			.compile("'(.*[^\\\\])'\\s->\\s'(.*[^\\\\\\\\])'");
+	Pattern blockPattern = Pattern
+			.compile("\\s*block\\s+'(.*[^\\\\])'\\s+until\\s+'(.*[^\\\\\\\\])'\\s+finish");
+	private String mainProfile;
+	private Profile profile;
+	private Configuration configuration;
+	private ProcessorBinding processorBinding;
+    protected Scanner scanner;
+
+	@Override
+	public Set<String> getMediaTypes() {
+		return singleton(TEXT_VND_TAVERNA_SCUFL2_STRUCTURE);
+	}
+
+	// synchronized because we share wb/scanner fields across the instance
+	protected synchronized WorkflowBundle parse(InputStream is)
+			throws ReaderException {
+		scanner = new Scanner(is);
+		try {
+			wb = new WorkflowBundle();
+			while (scanner.hasNextLine())
+				parseLine(scanner.nextLine());
+			return wb;
+		} finally {
+			scanner.close();
+		}
+	}
+
+	protected void parseLine(final String nextLine) throws ReaderException {
+		try (Scanner scanner = new Scanner(nextLine.trim())) {
+			// In case it is the last line
+			if (!scanner.hasNext())
+				return;
+			// allow any whitespace
+			String next = scanner.next();
+
+			if (next.isEmpty())
+				return;
+			switch (next) {
+			case "WorkflowBundle":
+				parseWorkflowBundle(scanner);
+				return;
+			case "MainWorkflow":
+				mainWorkflow = parseName(scanner);
+				return;
+			case "Workflow":
+				parseWorkflow(scanner);
+				return;
+			case "In":
+			case "Out":
+				parsePort(scanner, next);
+				return;
+			case "Links":
+				level = Level.Links;
+				processor = null;
+				return;
+			case "Controls":
+				level = Level.Controls;
+				return;
+			case "MainProfile":
+				mainProfile = parseName(scanner);
+				return;
+			case "Profile":
+				parseProfile(scanner);
+				return;
+			case "Type":
+				parseType(nextLine);
+				return;
+			case "ProcessorBinding":
+				parseProcessorBinding(scanner);
+				return;
+			case "InputPortBindings":
+				level = Level.InputPortBindings;
+				return;
+			case "OutputPortBindings":
+				level = Level.OutputPortBindings;
+				return;
+			case "Configuration":
+				parseConfiguration(scanner);
+				return;
+			case "Configures":
+				parseConfigures(scanner);
+				return;
+			case "Activity":
+				switch (level) {
+				case Profile:
+				case Activity:
+					level = Level.Activity;
+					activity = new Activity();
+					activity.setName(parseName(scanner));
+					profile.getActivities().add(activity);
+					return;
+				case ProcessorBinding:
+					Activity boundActivity = profile.getActivities().getByName(
+							parseName(scanner));
+					processorBinding.setBoundActivity(boundActivity);
+					return;
+				default:
+					break;
+				}
+				break;
+			case "Processor":
+				switch (level) {
+				case Workflow:
+				case Processor:
+					level = Level.Processor;
+					processor = new Processor();
+					processor.setName(parseName(scanner));
+					processor.setParent(workflow);
+					workflow.getProcessors().add(processor);
+					return;
+				case ProcessorBinding:
+					String[] wfProcName = parseName(scanner).split(":");
+					Workflow wf = wb.getWorkflows().getByName(wfProcName[0]);
+					Processor boundProcessor = wf.getProcessors().getByName(
+							wfProcName[1]);
+					processorBinding.setBoundProcessor(boundProcessor);
+					return;
+				default:
+					break;
+				}
+				break;
+			}
+
+			if (next.equals("block")) {
+				Matcher blockMatcher = blockPattern.matcher(nextLine);
+				blockMatcher.find();
+				String block = blockMatcher.group(1);
+				String untilFinish = blockMatcher.group(2);
+
+				Processor blockProc = workflow.getProcessors().getByName(block);
+				Processor untilFinishedProc = workflow.getProcessors()
+						.getByName(untilFinish);
+				new BlockingControlLink(blockProc, untilFinishedProc);
+			}
+			if (next.startsWith("'") && level.equals(Level.Links)) {
+				Matcher linkMatcher = linkPattern.matcher(nextLine);
+				linkMatcher.find();
+				String firstLink = linkMatcher.group(1);
+				String secondLink = linkMatcher.group(2);
+
+				SenderPort senderPort;
+				if (firstLink.contains(":")) {
+					String[] procPort = firstLink.split(":");
+					Processor proc = workflow.getProcessors().getByName(
+							procPort[0]);
+					senderPort = proc.getOutputPorts().getByName(procPort[1]);
+				} else
+					senderPort = workflow.getInputPorts().getByName(firstLink);
+
+				ReceiverPort receiverPort;
+				if (secondLink.contains(":")) {
+					String[] procPort = secondLink.split(":");
+					Processor proc = workflow.getProcessors().getByName(
+							procPort[0]);
+					receiverPort = proc.getInputPorts().getByName(procPort[1]);
+				} else
+					receiverPort = workflow.getOutputPorts().getByName(
+							secondLink);
+
+				new DataLink(workflow, senderPort, receiverPort);
+				return;
+			}
+
+			if (next.startsWith("'")
+					&& (level == Level.InputPortBindings || level == Level.OutputPortBindings)) {
+				Matcher linkMatcher = linkPattern.matcher(nextLine);
+				linkMatcher.find();
+				String firstLink = linkMatcher.group(1);
+				String secondLink = linkMatcher.group(2);
+				if (level == Level.InputPortBindings) {
+					InputProcessorPort processorPort = processorBinding
+							.getBoundProcessor().getInputPorts()
+							.getByName(firstLink);
+					InputActivityPort activityPort = processorBinding
+							.getBoundActivity().getInputPorts()
+							.getByName(secondLink);
+					new ProcessorInputPortBinding(processorBinding,
+							processorPort, activityPort);
+				} else {
+					OutputActivityPort activityPort = processorBinding
+							.getBoundActivity().getOutputPorts()
+							.getByName(firstLink);
+					OutputProcessorPort processorPort = processorBinding
+							.getBoundProcessor().getOutputPorts()
+							.getByName(secondLink);
+					new ProcessorOutputPortBinding(processorBinding,
+							activityPort, processorPort);
+				}
+				return;
+			}
+			if (level == Level.JSON) {
+				/*
+				 * A silly reader that feeds (no more than) a single line at a
+				 * time from our parent scanner, starting with the current line
+				 */
+				Reader reader = new Reader() {
+					char[] line = nextLine.toCharArray();
+					int pos = 0;
+
+					@Override
+					public int read(char[] cbuf, int off, int len)
+							throws IOException {
+						if (pos >= line.length) {
+							// Need to read next line to fill buffer
+							if (!StructureReader.this.scanner.hasNextLine())
+								return -1;
+							String newLine = StructureReader.this.scanner
+									.nextLine();
+							pos = 0;
+							line = newLine.toCharArray();
+							// System.out.println("Read new line: " + newLine);
+						}
+						int length = Math.min(len, line.length - pos);
+						if (length <= 0)
+							return 0;
+						arraycopy(line, pos, cbuf, off, length);
+						pos += length;
+						return length;
+					}
+
+					@Override
+					public void close() throws IOException {
+						line = null;
+					}
+				};            
+
+				ObjectMapper mapper = new ObjectMapper();
+				try {
+					JsonParser parser = mapper.getFactory()
+							.createParser(reader);
+					JsonNode jsonNode = parser.readValueAs(JsonNode.class);
+					// System.out.println("Parsed " + jsonNode);
+					configuration.setJson(jsonNode);
+				} catch (IOException e) {
+					throw new ReaderException("Can't parse json", e);
+				}
+				level = Level.Configuration;
+				return;
+			}
+		}
+    }
+
+	private void parseWorkflowBundle(Scanner scanner) {
+		level = Level.WorkflowBundle;
+		String name = parseName(scanner);
+		wb.setName(name);
+	}
+
+	private void parseWorkflow(Scanner scanner) {
+		level = Level.Workflow;
+		workflow = new Workflow();
+		String workflowName = parseName(scanner);
+		workflow.setName(workflowName);
+		wb.getWorkflows().add(workflow);
+		if (workflowName.equals(mainWorkflow))
+			wb.setMainWorkflow(workflow);
+	}
+
+	private void parsePort(Scanner scanner, String next) throws ReaderException {
+		boolean in = next.equals("In");
+		String portName = parseName(scanner);
+		switch (level) {
+		case Workflow:
+			if (in)
+				new InputWorkflowPort(workflow, portName);
+			else
+				new OutputWorkflowPort(workflow, portName);
+			break;
+		case Processor:
+			if (in)
+				new InputProcessorPort(processor, portName);
+			else
+				new OutputProcessorPort(processor, portName);
+			break;
+		case Activity:
+			if (in)
+				new InputActivityPort(activity, portName);
+			else
+				new OutputActivityPort(activity, portName);
+			break;
+		default:
+			throw new ReaderException("Unexpected " + next + " at level "
+					+ level);
+		}
+	}
+
+	private void parseProfile(Scanner scanner) {
+		level = Level.Profile;
+		profile = new Profile();
+		String profileName = parseName(scanner);
+		profile.setName(profileName);
+		wb.getProfiles().add(profile);
+		if (profileName.equals(mainProfile))
+			wb.setMainProfile(profile);
+	}
+
+	private void parseType(String nextLine) {
+		URI uri = URI.create(nextLine.split("[<>]")[1]);
+		switch (level) {
+		case Activity:
+			activity.setType(uri);
+			break;
+		case Configuration:
+			configuration.setType(uri);
+			break;
+		default:
+			break;
+		}
+	}
+
+	private void parseProcessorBinding(Scanner scanner) {
+		level = Level.ProcessorBinding;
+		processorBinding = new ProcessorBinding();
+		String bindingName = parseName(scanner);
+		processorBinding.setName(bindingName);
+		profile.getProcessorBindings().add(processorBinding);
+	}
+
+	private void parseConfiguration(Scanner scanner) {
+		level = Level.Configuration;
+		configuration = new Configuration();
+		String configName = parseName(scanner);
+		configuration.setName(configName);
+		profile.getConfigurations().add(configuration);
+	}
+
+	private void parseConfigures(Scanner scanner) {
+		String configures = parseName(scanner);
+		if (!configures.startsWith(ACTIVITY_SLASH))
+			throw new UnsupportedOperationException("Unknown Configures "
+					+ configures);
+		activity = profile.getActivities().getByName(
+				configures.substring(ACTIVITY_SLASH.length(),
+						configures.length()));
+		configuration.setConfigures(activity);
+		level = Level.JSON;
+	}
+
+
+	private String parseName(Scanner scanner) {
+		String name = scanner.findInLine("'(.*[^\\\\])'");
+		return name.substring(1, name.length() - 1);
+	}
+
+	@Override
+	public WorkflowBundle readBundle(File bundleFile, String mediaType)
+			throws IOException, ReaderException {
+		try (BufferedInputStream is = new BufferedInputStream(
+				new FileInputStream(bundleFile))) {
+			return parse(is);
+		}
+	}
+
+	@Override
+	public WorkflowBundle readBundle(InputStream inputStream, String mediaType)
+			throws IOException, ReaderException {
+		return parse(inputStream);
+	}
+
+	@Override
+	public String guessMediaTypeForSignature(byte[] firstBytes) {
+		if (new String(firstBytes, Charset.forName("ISO-8859-1"))
+				.contains("WorkflowBundle '"))
+			return TEXT_VND_TAVERNA_SCUFL2_STRUCTURE;
+		return null;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/structure/StructureWriter.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/structure/StructureWriter.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/structure/StructureWriter.java
new file mode 100644
index 0000000..50610b0
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/io/structure/StructureWriter.java
@@ -0,0 +1,314 @@
+package org.apache.taverna.scufl2.api.io.structure;
+
+/*
+ * 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 org.apache.taverna.scufl2.api.io.structure.StructureReader.TEXT_VND_TAVERNA_SCUFL2_STRUCTURE;
+
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.taverna.scufl2.api.activity.Activity;
+import org.apache.taverna.scufl2.api.common.Named;
+import org.apache.taverna.scufl2.api.common.NamedSet;
+import org.apache.taverna.scufl2.api.common.Ported;
+import org.apache.taverna.scufl2.api.common.URITools;
+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.WorkflowBundleWriter;
+import org.apache.taverna.scufl2.api.port.Port;
+import org.apache.taverna.scufl2.api.port.ProcessorPort;
+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;
+
+
+/**
+ * A <code>WorkflowBundleWriter</code> that writes a {@link WorkflowBundle} in Scufl2 Structure
+ * format.
+ */
+public class StructureWriter implements WorkflowBundleWriter {
+	private StringBuilder sb;
+	private URITools uriTools = new URITools();
+
+	private void append(Named named) {
+		append(" '");
+		append(escapeName(named.getName()));
+		append("'");
+	}
+
+	private void append(String string) {
+		sb.append(string);
+	}
+
+	private void append(URI uri) {
+		sb.append(" <");
+		sb.append(uri.toASCIIString());
+		sb.append(">");
+	}
+
+	private void appendPorts(int indent, Ported wf) {
+		for (Port in : sorted(wf.getInputPorts())) {
+			newLine(indent);
+			append("In");
+			append(in);
+		}
+		for (Port out : sorted(wf.getOutputPorts())) {
+			newLine(indent);
+			append("Out");
+			append(out);
+		}
+	}
+
+	/**
+	 * Create a string representation of a workflow bundle. Note that this
+	 * method is not thread-safe; call only via the public methods which have
+	 * the right locking.
+	 */
+	@SuppressWarnings("unchecked")
+	protected String bundleString(WorkflowBundle wb) {
+		sb = new StringBuilder();
+		append("WorkflowBundle");
+		append(wb);
+
+		if (wb.getMainWorkflow() != null) {
+			newLine(1);
+			append("MainWorkflow");
+			append(wb.getMainWorkflow());
+		}
+		for (Workflow wf : sorted(wb.getWorkflows())) {
+			newLine(1);
+			append("Workflow");
+			append(wf);
+			appendPorts(2, wf);
+			for (Processor p : sorted(wf.getProcessors())) {
+				newLine(2);
+				append("Processor");
+				append(p);
+				appendPorts(3, p);
+			}
+
+			// LINKS
+			// We'll need to sort them afterwards
+			List<String> links = new ArrayList<>();
+			for (DataLink dl : wf.getDataLinks())
+				links.add(datalink(dl.getReceivesFrom()) + " -> " + datalink(dl.getSendsTo()));
+			Collections.sort(links);
+			if (!links.isEmpty()) {
+				newLine(2);
+				append("Links");
+			}
+			for (String link : links) {
+				newLine(3);
+				append(link);
+			}
+
+			if (!wf.getControlLinks().isEmpty()) {
+				newLine(2);
+				append("Controls");
+				List<ControlLink> controlLinks = new ArrayList<>(wf.getControlLinks());
+				Collections.sort(controlLinks);
+				for (ControlLink controlLink : controlLinks) {
+					if (!(controlLink instanceof BlockingControlLink))
+						// TODO
+						continue;
+					BlockingControlLink blockingControlLink = (BlockingControlLink) controlLink;
+					newLine(3);
+					append("block");
+					append(blockingControlLink.getBlock());
+					append(" until");
+					append(blockingControlLink.getUntilFinished());
+					append(" finish");
+				}
+			}
+		}
+
+		if (wb.getMainProfile() != null) {
+			newLine(1);
+			append("MainProfile");
+			append(wb.getMainProfile());
+		}
+
+		for (Profile p : sorted(wb.getProfiles())) {
+			newLine(1);
+			append("Profile");
+			append(p);
+			for (Activity a : sorted(p.getActivities())) {
+				newLine(2);
+				append("Activity");
+				append(a);
+				newLine(3);
+				append("Type");
+				append(a.getType());
+				appendPorts(3, a);
+			}
+			for (ProcessorBinding pb : p.getProcessorBindings()) {
+				newLine(2);
+				append("ProcessorBinding");
+				append(pb);
+				newLine(3);
+				append("Activity");
+				append(pb.getBoundActivity());
+				newLine(3);
+				append("Processor");
+				String name = " '" + escapeName(pb.getBoundProcessor().getParent().getName());
+				name = name + ":" + escapeName(pb.getBoundProcessor().getName()) + "'";
+				append(name);
+
+				List<String> links = new ArrayList<String>();
+				for (ProcessorInputPortBinding ip : pb.getInputPortBindings())
+					links.add("'" + escapeName(ip.getBoundProcessorPort().getName()) + "' -> '"
+							+ escapeName(ip.getBoundActivityPort().getName()) + "'");
+				Collections.sort(links);
+				if (!links.isEmpty()) {
+					newLine(3);
+					append("InputPortBindings");
+				}
+				for (String link : links) {
+					newLine(4);
+					append(link);
+				}
+
+				links.clear();
+				for (ProcessorOutputPortBinding ip : pb.getOutputPortBindings())
+					// Note: opposite direction as for ProcessorInputPortBinding
+					links.add("'" + escapeName(ip.getBoundActivityPort().getName()) + "' -> '"
+							+ escapeName(ip.getBoundProcessorPort().getName()) + "'");
+				Collections.sort(links);
+				if (!links.isEmpty()) {
+					newLine(3);
+					append("OutputPortBindings");
+				}
+				for (String link : links) {
+					newLine(4);
+					append(link);
+				}
+			}
+
+			for (Configuration config : p.getConfigurations()) {
+				newLine(2);
+				append("Configuration");
+				append(config);
+
+				newLine(3);
+				if (config.getType() != null) {
+					append("Type");
+					append(config.getType());
+				}
+				newLine(3);
+				append("Configures");
+				if (config.getConfigures() instanceof Named) {				
+					Named c = (Named) config.getConfigures();					
+					String cName = "'" + escapeName(c.getClass().getSimpleName().toLowerCase());
+					cName = cName + "/" + escapeName(c.getName()) + "'";
+					append(" " + cName);
+				} else {
+					URI configuredURI = uriTools.relativeUriForBean(config.getConfigures(), p);
+					append(" '" + configuredURI.toASCIIString() + "'");
+				}
+
+				newLine(4);
+				append(config.getJson().toString());
+			}
+
+		}
+		append("\n");
+		//System.out.println(sb);
+		return sb.toString();
+	}
+
+	private String datalink(Port port) {
+		StringBuilder s = new StringBuilder();
+		s.append("'");
+		if (port instanceof ProcessorPort) {
+			ProcessorPort processorPort = (ProcessorPort) port;
+			s.append(escapeName(processorPort.getParent().getName()));
+			s.append(":");
+		}
+		s.append(escapeName(port.getName()));
+		s.append("'");
+		return s.toString();
+	}
+
+	private String escapeName(String name) {
+		return name.replace("\\", "\\\\").replace("'", "\\'")
+				.replace(":", "\\:").replace("/", "\\/");
+	}
+
+	@Override
+	public Set<String> getMediaTypes() {
+		return Collections.singleton(TEXT_VND_TAVERNA_SCUFL2_STRUCTURE);
+	}
+
+	private void newLine(int indentLevel) {
+		sb.append("\n");
+		for (int i = 0; i < indentLevel; i++)
+			sb.append("  ");
+	}
+
+	private <T extends Named> List<T> sorted(NamedSet<T> namedSet) {
+		List<T> sorted = new ArrayList<>();
+		sorted.addAll(namedSet);
+		Collections.sort(sorted, new Comparator<T>() {
+			@Override
+			public int compare(T o1, T o2) {
+				return o1.getName().compareTo(o2.getName());
+			}
+		});
+		return sorted;
+	}
+
+	@Override
+	public synchronized void writeBundle(WorkflowBundle wb, File destination,
+			String mediaType) throws IOException {
+		destination.createNewFile();
+		try (BufferedOutputStream outputStream = new BufferedOutputStream(
+				new FileOutputStream(destination))) {
+			writeBundle(wb, outputStream, mediaType);
+		}
+	}
+
+	@Override
+	public void writeBundle(WorkflowBundle wfBundle, OutputStream output,
+			String mediaType) throws IOException {
+		OutputStreamWriter writer = new OutputStreamWriter(output, "utf-8");
+		try {
+			writer.write(bundleString(wfBundle));
+		} finally {
+			writer.close();
+		}
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/iterationstrategy/CrossProduct.java
----------------------------------------------------------------------
diff --git a/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/iterationstrategy/CrossProduct.java b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/iterationstrategy/CrossProduct.java
new file mode 100644
index 0000000..58895f0
--- /dev/null
+++ b/taverna-scufl2-api/src/main/java/org/apache/taverna/scufl2/api/iterationstrategy/CrossProduct.java
@@ -0,0 +1,106 @@
+package org.apache.taverna.scufl2.api.iterationstrategy;
+
+/*
+ * 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 java.util.ArrayList;
+import java.util.List;
+
+import org.apache.taverna.scufl2.api.common.AbstractCloneable;
+import org.apache.taverna.scufl2.api.common.Visitor;
+import org.apache.taverna.scufl2.api.common.WorkflowBean;
+
+
+/**
+ * @author Stian Soiland-Reyes
+ */
+@SuppressWarnings("serial")
+public class CrossProduct extends ArrayList<IterationStrategyNode> implements
+		IterationStrategyTopNode {
+	private IterationStrategyParent parent;
+
+	@Override
+	public boolean accept(Visitor visitor) {
+		if (visitor.visitEnter(this))
+			for (IterationStrategyNode strategy : this)
+				if (!strategy.accept(visitor))
+					break;
+		return visitor.visitLeave(this);
+	}
+
+	@Override
+	public boolean equals(Object o) {
+		return o instanceof CrossProduct && super.equals(o);
+	}
+
+	@Override
+	public IterationStrategyParent getParent() {
+		return parent;
+	}
+
+	@Override
+	public void setParent(IterationStrategyParent newParent) {
+		if (parent == newParent)
+			return;
+
+		if (parent != null) {
+			// Remove from old parent
+			if (parent instanceof IterationStrategyStack) {
+				IterationStrategyStack stack = (IterationStrategyStack) parent;
+				stack.remove(this);
+			} else if (parent instanceof DotProduct
+					|| parent instanceof CrossProduct) {
+				@SuppressWarnings("unchecked")
+				List<IterationStrategyNode> parentList = (List<IterationStrategyNode>) parent;
+				parentList.remove(this);
+			} else
+				throw new IllegalArgumentException(
+						"Old parent must be a IterationStrategy, DotProduct or CrossProduct: "
+								+ parent);
+		}
+
+		parent = newParent;
+
+		if (parent instanceof IterationStrategyStack) {
+			IterationStrategyStack stack = (IterationStrategyStack) parent;
+			if (!stack.contains(this))
+				stack.add(this);
+		} else if (parent instanceof DotProduct
+				|| parent instanceof CrossProduct) {
+			@SuppressWarnings("unchecked")
+			List<IterationStrategyNode> parentList = (List<IterationStrategyNode>) parent;
+			if (!parentList.contains(this))
+				parentList.add(this);
+		} else
+			throw new IllegalArgumentException(
+					"Parent must be a IterationStrategy, DotProduct or CrossProduct: "
+							+ parent);
+	}
+
+	@Override
+	public WorkflowBean clone() {
+		return AbstractCloneable.cloneWorkflowBean(this);
+	}
+
+	@Override
+	public String toString() {
+		return getClass().getSimpleName() + super.toString();
+	}
+}