Merge branch 'patch-1' of https://github.com/matthiaskoenig/incubator-taverna-language

Contributed by Matthias König

This closes #33.
diff --git a/pom.xml b/pom.xml
index d597b19..9dcb863 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,7 +24,7 @@
 	</parent>
 	<groupId>org.apache.taverna.language</groupId>
 	<artifactId>apache-taverna-language</artifactId>
-	<version>0.15.2-incubating-SNAPSHOT</version>
+	<version>0.16.0-incubating-SNAPSHOT</version>
 	<packaging>pom</packaging>
 
 	<name>Apache Taverna Language APIs (Scufl2, Databundle)</name>
diff --git a/taverna-baclava-language/pom.xml b/taverna-baclava-language/pom.xml
index 1157e64..567b992 100644
--- a/taverna-baclava-language/pom.xml
+++ b/taverna-baclava-language/pom.xml
@@ -23,7 +23,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
   <artifactId>taverna-baclava-language</artifactId>
   <name>Apache Taverna Baclava support</name>
diff --git a/taverna-databundle/pom.xml b/taverna-databundle/pom.xml
index cb9bb61..25f76b9 100644
--- a/taverna-databundle/pom.xml
+++ b/taverna-databundle/pom.xml
@@ -21,7 +21,7 @@
     <parent>
       	<groupId>org.apache.taverna.language</groupId>
       	<artifactId>apache-taverna-language</artifactId>
-      	<version>0.15.2-incubating-SNAPSHOT</version>
+      	<version>0.16.0-incubating-SNAPSHOT</version>
     </parent>
     <artifactId>taverna-databundle</artifactId>
     <name>Apache Taverna Databundle API</name>
diff --git a/taverna-databundle/src/main/java/org/apache/taverna/databundle/DataBundles.java b/taverna-databundle/src/main/java/org/apache/taverna/databundle/DataBundles.java
index c491d13..897bc15 100644
--- a/taverna-databundle/src/main/java/org/apache/taverna/databundle/DataBundles.java
+++ b/taverna-databundle/src/main/java/org/apache/taverna/databundle/DataBundles.java
@@ -1,6 +1,4 @@
-package org.apache.taverna.databundle;
 /*
- *
  * 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
@@ -18,8 +16,8 @@
  * specific language governing permissions and limitations
  * under the License.
  *
-*/
-
+ */
+package org.apache.taverna.databundle;
 
 import static java.nio.file.Files.createDirectories;
 import static java.nio.file.Files.delete;
@@ -33,10 +31,16 @@
 import static java.nio.file.StandardOpenOption.CREATE;
 import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.io.UncheckedIOException;
+import java.lang.reflect.Array;
+import java.net.MalformedURLException;
 import java.net.URI;
+import java.net.URL;
+import java.net.URLStreamHandler;
 import java.nio.charset.Charset;
 import java.nio.file.DirectoryIteratorException;
 import java.nio.file.DirectoryStream;
@@ -45,19 +49,25 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.EnumSet;
 import java.util.List;
 import java.util.NavigableMap;
 import java.util.TreeMap;
 import java.util.UUID;
-import java.util.logging.Logger;
 import java.util.logging.Level;
+import java.util.logging.Logger;
+import java.util.stream.Collectors;
+import java.util.stream.Stream;
+import java.util.stream.StreamSupport;
 
+import org.apache.taverna.databundle.DataBundles.ResolveOptions;
+import org.apache.taverna.robundle.Bundle;
+import org.apache.taverna.robundle.Bundles;
 import org.apache.taverna.scufl2.api.container.WorkflowBundle;
 import org.apache.taverna.scufl2.api.io.ReaderException;
 import org.apache.taverna.scufl2.api.io.WorkflowBundleIO;
 import org.apache.taverna.scufl2.api.io.WriterException;
-import org.apache.taverna.robundle.Bundle;
-import org.apache.taverna.robundle.Bundles;
 
 import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
@@ -68,7 +78,6 @@
  * The style of using this class is similar to that of {@link Files}. In fact, a
  * data bundle is implemented as a set of {@link Path}s.
  * 
- * @author Stian Soiland-Reyes
  */
 public class DataBundles extends Bundles {
 	private static final class OBJECT_MAPPER {
@@ -440,7 +449,279 @@
 			throw ex.getCause();
 		}
 	}
+	
+	public enum ResolveOptions { 
+		/**
+		 * Leaf values are represented as bundle {@link Path}s, except errors as
+		 * {@link ErrorDocument} and references as {@link URL}. Note that specifying this
+		 * option does not negate any of the other options like {@link #BYTES}.
+		 */
+		DEFAULT,
+		/**
+		 * Leaf values should be represented as a {@link String} (NOTE: This won't work well if the path is a binary)
+		 */
+		STRING,
+		/**
+		 * Leaf values should be represented as a <code>byte[]</code>
+		 */
+		BYTES,
+		/**
+		 * Leaf values should always be represented as {@link URI}s (except errors)
+		 */
+		URI,
+		/**
+		 * Leaf values should be represented as bundle {@link Path}s (even if they are errors)
+		 */
+		PATH,
+		/**
+		 * Replace errors with <code>null</code>, or the empty string if {@link #REPLACE_NULL} is also specified.
+		 */
+		REPLACE_ERRORS,
+		/**
+		 * Instead of returning <code>null</code>, return the empty
+		 * {@link String} "", or empty byte[] if {@link #BYTES} is specified, or
+		 * the missing path if {@link #PATH} is specified.
+		 */
+		REPLACE_NULL
+	}
+		
+	/**
+	 * Deeply resolve a {@link Path} to JVM objects.
+	 * <p>
+	 * This method is intended mainly for presentational uses 
+	 * with a particular input/output port from
+	 * {@link #getPorts(Path)} or {@link #getPort(Path, String)}.
+	 * <p>
+	 * Note that as all lists are resolved deeply (including lists of lists)
+	 * and when using options {@link ResolveOptions#STRING} or {@link ResolveOptions#BYTES}
+	 * the full content of the values are read into memory, this 
+	 * method can be time-consuming.
+	 * <p>
+	 * If the path is <code>null</code> or {@link #isMissing(Path)},
+	 * <code>null</code> is returned, unless the option
+	 * {@link ResolveOptions#REPLACE_NULL} is specified, which would return the
+	 * empty String "".
+	 * <p>
+	 * If the path {@link #isValue(Path)} and the option
+	 * {@link ResolveOptions#STRING} is specified, its
+	 * {@link #getStringValue(Path)} is returned (assuming an UTF-8 encoding).
+	 * NOTE: Binary formats (e.g. PNG) will NOT be represented correctly read as
+	 * UTF-8 String and should instead be read directly with
+	 * {@link Files#newInputStream(Path, java.nio.file.OpenOption...)}. Note
+	 * that this could consume a large amount of memory as no size checks are
+	 * performed.
+	 * <p>
+	 * If the option {@link ResolveOptions#URI} is specified, all non-missing 
+	 * non-error leaf values are resolved as a {@link URI}. If the path is a 
+	 * {@link #isReference(Path)} the URI will be the reference from 
+	 * {@link #getReference(Path)}, otherwise the URI will  
+	 * identify a {@link Path} within the current {@link Bundle}.
+	 * <p>
+	 * If the path {@link #isValue(Path)} and the option
+	 * {@link ResolveOptions#BYTES} is specified, the complete content is returned as
+	 * a <code>byte[]</code>. Note that this could consume a large amount of memory
+	 * as no size checks are performed.
+	 * <p>
+	 * If the path {@link #isError(Path)}, the corresponding
+	 * {@link ErrorDocument} is returned, except when the option
+	 * {@link ResolveOptions#REPLACE_ERRORS} is specified, which means errors are
+	 * returned as <code>null</code> (or <code>""</code> if {@link ResolveOptions#REPLACE_NULL} is also specified).
+	 * <p>
+	 * If the path {@link #isReference(Path)} and the option 
+	 * {@link ResolveOptions#URI} is <strong>not</strong> set, 
+	 * either a {@link File} or a {@link URL} is returned, 
+	 * depending on its protocol. If the reference protocol has no
+	 * corresponding {@link URLStreamHandler}, a {@link URI} is returned
+	 * instead. 
+	 * <p>
+	 * If the path {@link #isList(Path)}, a {@link List} is returned
+	 * corresponding to resolving the paths from {@link #getList(Path)}. using
+	 * this method with the same options.
+	 * <p>
+	 * If none of the above, the {@link Path} itself is returned. This is 
+	 * thus the default for non-reference non-error leaf values if neither 
+	 * {@link ResolveOptions#STRING}, {@link ResolveOptions#BYTES} or
+	 * {@link ResolveOptions#URI} are specified.
+	 * To force returning of {@link Path}s for all non-missing leaf values, specify
+	 * {@link ResolveOptions#PATH};
+	 * 
+	 * @param path
+	 *            Data bundle path to resolve
+	 * @param options
+	 *            Resolve options
+	 * @return <code>null</code>, a {@link String}, {@link ErrorDocument},
+	 *         {@link URL}, {@link File}, {@link Path} or {@link List}
+	 *         (containing any of these) depending on the path type and the options.
+	 * @throws IOException
+	 *             If the path (or any of the path in a contained list) can't be
+	 *             accessed
+	 */
+	@SuppressWarnings({ "unchecked", "rawtypes" })
+	public static Object resolve(Path path, ResolveOptions... options) throws IOException {
+		EnumSet<ResolveOptions> opt;
+		if (options.length == 0) {
+			opt = EnumSet.of(ResolveOptions.DEFAULT); // no-op
+		} else {
+			opt = EnumSet.of(ResolveOptions.DEFAULT, options);
+		}
+		
+		if (opt.contains(ResolveOptions.BYTES) && opt.contains(ResolveOptions.STRING)) {
+			throw new IllegalArgumentException("Incompatible options: BYTES and STRING");
+		}
+		if (opt.contains(ResolveOptions.BYTES) && opt.contains(ResolveOptions.PATH)) {
+			throw new IllegalArgumentException("Incompatible options: BYTES and PATH");
+		}
+		if (opt.contains(ResolveOptions.BYTES) && opt.contains(ResolveOptions.URI)) {
+			throw new IllegalArgumentException("Incompatible options: BYTES and URI");
+		}
+		if (opt.contains(ResolveOptions.STRING) && opt.contains(ResolveOptions.PATH)) {
+			throw new IllegalArgumentException("Incompatible options: STRING and PATH");
+		}
+		if (opt.contains(ResolveOptions.STRING) && opt.contains(ResolveOptions.URI)) {
+			throw new IllegalArgumentException("Incompatible options: STRING and URI");
+		}
+		if (opt.contains(ResolveOptions.PATH) && opt.contains(ResolveOptions.URI)) {
+			throw new IllegalArgumentException("Incompatible options: PATH and URI");
+		}
 
+		
+		if (path == null || isMissing(path)) {
+			if (! opt.contains(ResolveOptions.REPLACE_NULL)) { 
+				return null;
+			}
+			if (opt.contains(ResolveOptions.BYTES)) {
+				return new byte[0];
+			}
+			if (opt.contains(ResolveOptions.PATH)) { 
+				return path;
+			}
+			if (opt.contains(ResolveOptions.URI)) {
+				return path.toUri();
+			}
+			// STRING and DEFAULT
+			return "";			
+			
+ 
+		}
+		
+		if (isList(path)) {
+			List<Path> list = getList(path);
+			List<Object> objectList = new ArrayList<Object>(list.size());
+			for (Path pathElement : list) {
+				objectList.add(resolve(pathElement, options));
+			}
+			return objectList;
+		}		
+		if (opt.contains(ResolveOptions.PATH)) {
+			return path;
+		}		
+		if (isError(path)) {
+			if (opt.contains(ResolveOptions.REPLACE_ERRORS)) {
+				return opt.contains(ResolveOptions.REPLACE_NULL) ? "" : null;	
+			}
+			return getError(path);
+		}
+		if (opt.contains(ResolveOptions.URI)) {
+			if (isReference(path)) {
+				return getReference(path);
+			} else {
+				return path.toUri();
+			}
+		}
+		if (isReference(path)) {
+			URI reference = getReference(path);
+			String scheme = reference.getScheme();
+			if ("file".equals(scheme)) {
+				return new File(reference);
+			} else {
+				try { 
+					return reference.toURL();
+				} catch (IllegalArgumentException|MalformedURLException e) {
+					return reference;
+				}
+			}
+		}
+		if (isValue(path)) {
+			if (opt.contains(ResolveOptions.BYTES)) {
+				return Files.readAllBytes(path);
+			}
+			if (opt.contains(ResolveOptions.STRING)) {
+				return getStringValue(path);
+			}
+		}
+		// Fall-back - return Path as-is
+		return path;
+	}
+
+	/**
+	 * Deeply resolve path as a {@link Stream} that only contain leaf elements of 
+	 * the specified class.
+	 * <p>
+	 * This method is somewhat equivalent to {@link #resolve(Path, ResolveOptions...)}, but 
+	 * the returned stream is not in any particular order, and will contain the leaf
+	 * items from all deep lists. Empty lists and error documents are ignored.
+	 * <p>
+	 * Any {@link IOException}s occurring during resolution are 
+	 * wrapped as {@link UncheckedIOException}.
+	 * <p>
+	 * Supported types include:
+	 * <ul>
+	 *   <li>{@link String}.class</li>
+	 *   <li><code>byte[].class</code></li>
+	 *   <li>{@link Path}.class</li>
+	 *   <li>{@link URI}.class</li>
+	 *   <li>{@link URL}.class</li>  
+	 *   <li>{@link File}.class</li>
+	 *   <li>{@link ErrorDocument}.class</li>
+	 *   <li>{@link Object}.class</li>
+	 * </ul>
+	 * 
+	 * @param path Data bundle path to resolve
+	 * @param type Type of objects to return, e.g. <code>String.class</code>
+	 * @return A {@link Stream} of resolved objects, or an empty stream if no such objects were resolved.
+	 * @throws UncheckedIOException If the path could not be accessed. 
+	 */
+	public static <T> Stream<T> resolveAsStream(Path path, Class<T> type) throws UncheckedIOException {
+		ResolveOptions options;
+		if (type == String.class) {
+			options = ResolveOptions.STRING;
+		} else if (type == byte[].class) {
+			options = ResolveOptions.BYTES;
+		} else if (type == Path.class) {
+			options = ResolveOptions.PATH;
+		} else if (type == URI.class) {
+			options = ResolveOptions.URI;
+		} else {
+			// Dummy-option, we'll filter on the returned type anyway
+			options = ResolveOptions.DEFAULT;
+		}
+		if (isList(path)) {
+			// return Stream of unordered list of resolved list items,	
+			// recursing to find the leaf nodes			
+			try {
+				return Files.walk(path)
+						// avoid re-recursion
+						.filter(p -> !Files.isDirectory(p)) 
+						.flatMap(p -> resolveItemAsStream(p, type, options));
+			} catch (IOException e) {
+				throw new UncheckedIOException(e);
+			}
+		} else {
+			return resolveItemAsStream(path, type, options);
+		}
+	}
+	private static <T> Stream<T> resolveItemAsStream(Path path, Class<T> type, ResolveOptions options) throws UncheckedIOException {
+		try {
+			Object value = resolve(path, options);
+			if (type.isInstance(value)) {
+				return Stream.of(type.cast(value));
+			}
+			return Stream.empty();
+		} catch (IOException e) {
+			throw new UncheckedIOException(e);
+		}
+	}
+	
 	public static WorkflowBundleIO getWfBundleIO() {
 		if (wfBundleIO == null)
 			wfBundleIO = new WorkflowBundleIO();
diff --git a/taverna-databundle/src/main/java/org/apache/taverna/databundle/ErrorDocument.java b/taverna-databundle/src/main/java/org/apache/taverna/databundle/ErrorDocument.java
index ec551b8..6eea2e6 100644
--- a/taverna-databundle/src/main/java/org/apache/taverna/databundle/ErrorDocument.java
+++ b/taverna-databundle/src/main/java/org/apache/taverna/databundle/ErrorDocument.java
@@ -59,4 +59,11 @@
 			trace = "";
 		this.trace = trace;
 	}
+	
+	@Override
+	public String toString() {
+		return "Error: " + getMessage() + "\n" + trace;
+		// TODO: also include the causedBy paths?
+	}
+	
 }
diff --git a/taverna-databundle/src/test/java/org/apache/taverna/databundle/TestDataBundles.java b/taverna-databundle/src/test/java/org/apache/taverna/databundle/TestDataBundles.java
index c5d692d..0a67b95 100644
--- a/taverna-databundle/src/test/java/org/apache/taverna/databundle/TestDataBundles.java
+++ b/taverna-databundle/src/test/java/org/apache/taverna/databundle/TestDataBundles.java
@@ -21,11 +21,17 @@
 */
 
 
-import static org.junit.Assert.*;
+import static org.junit.Assert.assertArrayEquals;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
 
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.net.URI;
+import java.net.URL;
 import java.nio.charset.Charset;
 import java.nio.file.DirectoryStream;
 import java.nio.file.FileAlreadyExistsException;
@@ -37,9 +43,9 @@
 import java.util.List;
 import java.util.Map;
 import java.util.UUID;
+import java.util.stream.Stream;
 
-import org.apache.taverna.databundle.DataBundles;
-import org.apache.taverna.databundle.ErrorDocument;
+import org.apache.taverna.databundle.DataBundles.ResolveOptions;
 import org.apache.taverna.robundle.Bundle;
 import org.apache.taverna.scufl2.api.container.WorkflowBundle;
 import org.apache.taverna.scufl2.api.io.WorkflowBundleIO;
@@ -185,7 +191,7 @@
 	public void getInputs() throws Exception {
 		Path inputs = DataBundles.getInputs(dataBundle);
 		assertTrue(Files.isDirectory(inputs));
-		// Second time should not fail because it already exists
+		// Second time should not fail because it alreadresolvy exists
 		inputs = DataBundles.getInputs(dataBundle);
 		assertTrue(Files.isDirectory(inputs));
 		assertEquals(dataBundle.getRoot(), inputs.getParent());
@@ -523,6 +529,273 @@
 	}
     
     @Test
+    public void resolveString() throws Exception {
+		Path inputs = DataBundles.getInputs(dataBundle);
+		Path list = DataBundles.getPort(inputs, "in1");
+		DataBundles.createList(list);
+		// 0 string value
+		DataBundles.setStringValue(DataBundles.newListItem(list), "test0");
+		// 1 http:// reference
+		URI reference = URI.create("http://example.com/");
+		DataBundles.setReference(DataBundles.newListItem(list), reference);
+		// 2 file:/// reference
+		Path tmpFile = Files.createTempFile("test", ".txt");
+		URI fileRef = tmpFile.toUri();
+		assertEquals("file", fileRef.getScheme());
+		DataBundles.setReference(DataBundles.newListItem(list), fileRef);
+		// 3 empty (null)
+		// 4 error
+		DataBundles.setError(DataBundles.getListItem(list,  4), "Example error", "1. Tried it\n2. Didn't work");
+		
+		
+		
+		
+		Object resolved = DataBundles.resolve(list, ResolveOptions.STRING);
+		assertTrue("Didn't resolve to a list", resolved instanceof List);
+		
+		List resolvedList = (List) resolved;
+		assertEquals("Unexpected list size", 5, resolvedList.size());
+		
+		assertTrue(resolvedList.get(0) instanceof String);
+		assertEquals("test0", resolvedList.get(0));
+		
+		assertTrue(resolvedList.get(1) instanceof URL);
+		assertEquals(reference, ((URL)resolvedList.get(1)).toURI());
+		
+		assertTrue(resolvedList.get(2) instanceof File);
+		assertEquals(tmpFile.toFile(), resolvedList.get(2));
+		
+		assertNull(resolvedList.get(3));
+		assertTrue(resolvedList.get(4) instanceof ErrorDocument);
+		assertEquals("Example error", ((ErrorDocument)resolvedList.get(4)).getMessage());
+		
+    }    
+
+    @Test
+    public void resolveNestedString() throws Exception {
+		Path inputs = DataBundles.getInputs(dataBundle);
+		Path list = DataBundles.getPort(inputs, "in1");
+		DataBundles.createList(list);
+		
+		
+		Path nested0 = DataBundles.newListItem(list);
+		DataBundles.newListItem(nested0);		
+		DataBundles.setStringValue(DataBundles.newListItem(nested0), "test0,0");
+		DataBundles.setStringValue(DataBundles.newListItem(nested0), "test0,1");
+		DataBundles.setStringValue(DataBundles.newListItem(nested0), "test0,2");
+		Path nested1 = DataBundles.newListItem(list);
+		DataBundles.newListItem(nested1); // empty
+		Path nested2 = DataBundles.newListItem(list);
+		DataBundles.newListItem(nested2);
+		DataBundles.setStringValue(DataBundles.newListItem(nested2), "test2,0");
+		
+		
+		
+		List<List<String>> resolved = (List<List<String>>) DataBundles.resolve(list, ResolveOptions.STRING);
+		
+		assertEquals("Unexpected list size", 3, resolved.size());
+		assertEquals("Unexpected sublist[0] size", 3, resolved.get(0).size());
+		assertEquals("Unexpected sublist[1] size", 0, resolved.get(1).size());
+		assertEquals("Unexpected sublist[2] size", 1, resolved.get(2).size());
+
+		
+		assertEquals("test0,0", resolved.get(0).get(0));
+		assertEquals("test0,1", resolved.get(0).get(1));
+		assertEquals("test0,2", resolved.get(0).get(2));
+		assertEquals("test2,0", resolved.get(2).get(0));		
+    }        
+
+
+    @Test
+    public void resolveStream() throws Exception {
+		Path inputs = DataBundles.getInputs(dataBundle);
+		Path list = DataBundles.getPort(inputs, "in1");
+		DataBundles.createList(list);
+		
+		Path nested0 = DataBundles.newListItem(list);
+		DataBundles.newListItem(nested0);		
+		DataBundles.setStringValue(DataBundles.newListItem(nested0), "test0,0");
+		DataBundles.setStringValue(DataBundles.newListItem(nested0), "test0,1");
+		DataBundles.setStringValue(DataBundles.newListItem(nested0), "test0,2");
+		DataBundles.setError(DataBundles.newListItem(nested0), "Ignore me", "This error is hidden");
+		Path nested1 = DataBundles.newListItem(list);
+		DataBundles.newListItem(nested1); // empty
+		Path nested2 = DataBundles.newListItem(list);
+		DataBundles.newListItem(nested2);
+		DataBundles.setStringValue(DataBundles.newListItem(nested2), "test2,0");
+		DataBundles.setReference(DataBundles.newListItem(nested2), URI.create("http://example.com/"));
+		
+		
+
+		assertEquals(6, DataBundles.resolveAsStream(list, Object.class).count());		
+		assertEquals(6, DataBundles.resolveAsStream(list, Path.class).count());
+		assertEquals(5, DataBundles.resolveAsStream(list, URI.class).count());
+		assertEquals(1, DataBundles.resolveAsStream(list, URL.class).count());
+		assertEquals(0, DataBundles.resolveAsStream(list, File.class).count());
+		assertEquals(1, DataBundles.resolveAsStream(list, ErrorDocument.class).count());
+		// Let's have a look at one of the types in detail
+		assertEquals(4, DataBundles.resolveAsStream(list, String.class).count());		
+		Stream<String> resolved = DataBundles.resolveAsStream(list, String.class);
+		Object[] strings = resolved.sorted().map(t -> t.replace("test", "X")).toArray();
+		// NOTE: We can only assume the below order because we used .sorted()
+		assertEquals("X0,0", strings[0]);
+		assertEquals("X0,1", strings[1]);
+		assertEquals("X0,2", strings[2]);
+		assertEquals("X2,0", strings[3]);
+    }        
+    
+    @Test
+    public void resolveURIs() throws Exception {
+    	Path inputs = DataBundles.getInputs(dataBundle);
+		Path list = DataBundles.getPort(inputs, "in1");
+		DataBundles.createList(list);
+		// 0 string value
+		Path test0 = DataBundles.newListItem(list);
+		DataBundles.setStringValue(test0, "test0");
+		// 1 http:// reference
+		URI reference = URI.create("http://example.com/");
+		DataBundles.setReference(DataBundles.newListItem(list), reference);
+		// 2 file:/// reference
+		Path tmpFile = Files.createTempFile("test", ".txt");
+		URI fileRef = tmpFile.toUri();
+		assertEquals("file", fileRef.getScheme());
+		DataBundles.setReference(DataBundles.newListItem(list), fileRef);
+		// 3 empty (null)
+		// 4 error
+		Path error4 = DataBundles.getListItem(list,  4);
+		DataBundles.setError(error4, "Example error", "1. Tried it\n2. Didn't work");
+		
+		List resolved = (List) DataBundles.resolve(list, ResolveOptions.URI);
+		assertEquals(test0.toUri(), resolved.get(0));
+		assertEquals(reference, resolved.get(1));
+		assertEquals(fileRef, resolved.get(2));
+		assertNull(resolved.get(3));
+		// NOTE: Need to get the Path again due to different file extension
+		assertTrue(resolved.get(4) instanceof ErrorDocument);		
+		//assertTrue(DataBundles.getListItem(list,  4).toUri(), resolved.get(4));
+    }
+    
+
+    @Test
+    public void resolvePaths() throws Exception {
+    	Path inputs = DataBundles.getInputs(dataBundle);
+		Path list = DataBundles.getPort(inputs, "in1");
+		DataBundles.createList(list);
+		// 0 string value
+		Path test0 = DataBundles.newListItem(list);
+		DataBundles.setStringValue(test0, "test0");
+		// 1 http:// reference
+		URI reference = URI.create("http://example.com/");
+		Path test1 = DataBundles.setReference(DataBundles.newListItem(list), reference);
+		// 2 file:/// reference
+		Path tmpFile = Files.createTempFile("test", ".txt");
+		URI fileRef = tmpFile.toUri();
+		assertEquals("file", fileRef.getScheme());
+		Path test2 = DataBundles.setReference(DataBundles.newListItem(list), fileRef);
+		// 3 empty (null)
+		// 4 error
+		Path error4 = DataBundles.setError(DataBundles.getListItem(list,  4), "Example error", "1. Tried it\n2. Didn't work");
+		
+		List<Path> resolved = (List<Path>) DataBundles.resolve(list, ResolveOptions.PATH);
+		assertEquals(test0, resolved.get(0));
+		assertEquals(test1, resolved.get(1));
+		assertEquals(test2, resolved.get(2));
+		assertNull(resolved.get(3));
+		assertEquals(error4, resolved.get(4));		
+    }
+
+    @Test
+    public void resolveReplaceError() throws Exception {
+    	Path inputs = DataBundles.getInputs(dataBundle);
+		Path list = DataBundles.getPort(inputs, "in1");
+		DataBundles.createList(list);
+		// 0 string value
+		DataBundles.setStringValue(DataBundles.newListItem(list), "test0");
+		// 1 error
+		DataBundles.setError(DataBundles.newListItem(list), 
+				"Example error", "1. Tried it\n2. Didn't work");
+		
+		List resolved = (List) DataBundles.resolve(list, ResolveOptions.STRING, ResolveOptions.REPLACE_ERRORS);
+		assertEquals("test0", resolved.get(0));
+		assertNull(resolved.get(1));
+    }
+
+    @Test
+    public void resolveReplaceNull() throws Exception {
+    	Path inputs = DataBundles.getInputs(dataBundle);
+		Path list = DataBundles.getPort(inputs, "in1");
+		DataBundles.createList(list);
+		// 0 string value
+		Path test0 = DataBundles.newListItem(list);
+		DataBundles.setStringValue(test0, "test0");
+		// 1 empty
+		// 2 error
+		DataBundles.setError(DataBundles.getListItem(list, 2), 
+				"Example error", "1. Tried it\n2. Didn't work");
+		
+		List resolved = (List) DataBundles.resolve(list, ResolveOptions.REPLACE_ERRORS, ResolveOptions.REPLACE_NULL);
+		assertEquals(test0, resolved.get(0));
+		assertEquals("", resolved.get(1));
+		assertEquals("", resolved.get(2));
+    }
+    
+
+    @Test
+    public void resolveDefault() throws Exception {
+    	Path inputs = DataBundles.getInputs(dataBundle);
+		Path list = DataBundles.getPort(inputs, "in1");
+		DataBundles.createList(list);
+		// 0 string value
+		Path test0 = DataBundles.newListItem(list);
+		DataBundles.setStringValue(test0, "test0");
+		// 1 http:// reference
+		URI reference = URI.create("http://example.com/");
+		Path test1 = DataBundles.setReference(DataBundles.newListItem(list), reference);
+		// 2 file:/// reference
+		Path tmpFile = Files.createTempFile("test", ".txt");
+		URI fileRef = tmpFile.toUri();
+		assertEquals("file", fileRef.getScheme());
+		Path test2 = DataBundles.setReference(DataBundles.newListItem(list), fileRef);
+		// 3 empty (null)
+		// 4 error
+		Path error4 = DataBundles.setError(DataBundles.getListItem(list,  4), "Example error", "1. Tried it\n2. Didn't work");
+		
+		List resolved = (List) DataBundles.resolve(list, ResolveOptions.DEFAULT);
+		assertEquals(test0, resolved.get(0));
+		assertTrue(resolved.get(1) instanceof URL);
+		assertEquals("http://example.com/", resolved.get(1).toString());
+		assertTrue(resolved.get(2) instanceof File);
+		assertEquals(tmpFile.toFile(), resolved.get(2));
+		assertNull(resolved.get(3));
+		assertTrue(resolved.get(4) instanceof ErrorDocument);
+    }
+    
+    @Test
+    public void resolveBinaries() throws Exception {
+    	Path inputs = DataBundles.getInputs(dataBundle);
+		Path list = DataBundles.getPort(inputs, "in1");
+		Path item = DataBundles.newListItem(list);
+
+		byte[] bytes = new byte[] { 
+				// Those lovely lower bytes who don't work well in UTF-8
+				0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31, 
+				// and some higher ones for fun
+				-1,-2,-3,-4,-5,-6,-7,-8,-9,-10,-11,-12,-13,-14,-15,-16,-17,-18,
+				-19,-20,-21,-22,-23,-24,-25,-26,-27,-28,-29,-30,-31
+		};
+		Files.write(item, bytes);
+		
+		
+		List resolvedBytes = (List)DataBundles.resolve(list, ResolveOptions.BYTES);
+		assertArrayEquals(bytes, (byte[])resolvedBytes.get(0));
+
+		List resolvedString = (List)DataBundles.resolve(list, ResolveOptions.STRING);		
+		// The below will always fail as several of the above bytes are not parsed as valid UTF-8
+		// but instead be substituted with replacement characters. 		
+		//assertArrayEquals(bytes, ((String)resolvedString.get(0)).getBytes(StandardCharsets.UTF_8));
+    }
+    
+    @Test
 	public void setErrorArgs() throws Exception {
 		Path inputs = DataBundles.getInputs(dataBundle);
 		Path portIn1 = DataBundles.getPort(inputs, "in1");
@@ -765,6 +1038,7 @@
                 Files.probeContentType(wf));
     }
 
+    // TODO: Why was this ignored? Check with taverna-language-0.15.x RC emails
     @Ignore
     @Test
     public void getWorkflowBundle() throws Exception {
diff --git a/taverna-robundle/pom.xml b/taverna-robundle/pom.xml
index a6fe7d7..b9d6d75 100644
--- a/taverna-robundle/pom.xml
+++ b/taverna-robundle/pom.xml
@@ -21,7 +21,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-robundle</artifactId>
 	<name>Apache Taverna RO Bundle API</name>
diff --git a/taverna-scufl2-annotation/pom.xml b/taverna-scufl2-annotation/pom.xml
index ca6ee7a..b5d7ea2 100644
--- a/taverna-scufl2-annotation/pom.xml
+++ b/taverna-scufl2-annotation/pom.xml
@@ -20,7 +20,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-scufl2-annotation</artifactId>
 	<packaging>bundle</packaging>
diff --git a/taverna-scufl2-api/pom.xml b/taverna-scufl2-api/pom.xml
index dd8cd5a..4d98c3f 100644
--- a/taverna-scufl2-api/pom.xml
+++ b/taverna-scufl2-api/pom.xml
@@ -20,7 +20,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-scufl2-api</artifactId>
 	<packaging>bundle</packaging>
diff --git a/taverna-scufl2-examples/pom.xml b/taverna-scufl2-examples/pom.xml
index cd5177e..f69d73b 100644
--- a/taverna-scufl2-examples/pom.xml
+++ b/taverna-scufl2-examples/pom.xml
@@ -20,7 +20,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-scufl2-examples</artifactId>
 	<packaging>bundle</packaging>
diff --git a/taverna-scufl2-integration-tests/pom.xml b/taverna-scufl2-integration-tests/pom.xml
index 635b53e..5fcff46 100644
--- a/taverna-scufl2-integration-tests/pom.xml
+++ b/taverna-scufl2-integration-tests/pom.xml
@@ -20,7 +20,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-scufl2-integration-tests</artifactId>
 	<name>Apache Taverna Scufl 2 integration tests</name>
diff --git a/taverna-scufl2-schemas/pom.xml b/taverna-scufl2-schemas/pom.xml
index ab21af4..61aa1bc 100644
--- a/taverna-scufl2-schemas/pom.xml
+++ b/taverna-scufl2-schemas/pom.xml
@@ -20,7 +20,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-scufl2-schemas</artifactId>
 	<packaging>bundle</packaging>
diff --git a/taverna-scufl2-scufl/pom.xml b/taverna-scufl2-scufl/pom.xml
index 2432713..6b5cc81 100644
--- a/taverna-scufl2-scufl/pom.xml
+++ b/taverna-scufl2-scufl/pom.xml
@@ -20,7 +20,7 @@
   <parent>
     <groupId>org.apache.taverna.language</groupId>
     <artifactId>apache-taverna-language</artifactId>
-    <version>0.15.2-incubating-SNAPSHOT</version>
+    <version>0.16.0-incubating-SNAPSHOT</version>
   </parent>
   <artifactId>taverna-scufl2-scufl</artifactId>
   <packaging>bundle</packaging>
diff --git a/taverna-scufl2-t2flow/pom.xml b/taverna-scufl2-t2flow/pom.xml
index b2a0dbd..0e2a0cf 100644
--- a/taverna-scufl2-t2flow/pom.xml
+++ b/taverna-scufl2-t2flow/pom.xml
@@ -20,7 +20,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-scufl2-t2flow</artifactId>
 	<packaging>bundle</packaging>
diff --git a/taverna-scufl2-ucfpackage/pom.xml b/taverna-scufl2-ucfpackage/pom.xml
index dbae3ed..976d299 100644
--- a/taverna-scufl2-ucfpackage/pom.xml
+++ b/taverna-scufl2-ucfpackage/pom.xml
@@ -20,7 +20,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-scufl2-ucfpackage</artifactId>
 	<packaging>bundle</packaging>
diff --git a/taverna-scufl2-wfbundle/pom.xml b/taverna-scufl2-wfbundle/pom.xml
index b810e4a..b7efe33 100644
--- a/taverna-scufl2-wfbundle/pom.xml
+++ b/taverna-scufl2-wfbundle/pom.xml
@@ -20,7 +20,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-scufl2-wfbundle</artifactId>
 	<packaging>bundle</packaging>
diff --git a/taverna-scufl2-wfdesc/pom.xml b/taverna-scufl2-wfdesc/pom.xml
index c60646c..ddf7e91 100755
--- a/taverna-scufl2-wfdesc/pom.xml
+++ b/taverna-scufl2-wfdesc/pom.xml
@@ -14,7 +14,7 @@
 	<parent>
 		<groupId>org.apache.taverna.language</groupId>
 		<artifactId>apache-taverna-language</artifactId>
-		<version>0.15.2-incubating-SNAPSHOT</version>
+		<version>0.16.0-incubating-SNAPSHOT</version>
 	</parent>
 	<artifactId>taverna-scufl2-wfdesc</artifactId>
 	<packaging>bundle</packaging>
diff --git a/taverna-tavlang-tool/pom.xml b/taverna-tavlang-tool/pom.xml
index 4e0317c..2ee2135 100644
--- a/taverna-tavlang-tool/pom.xml
+++ b/taverna-tavlang-tool/pom.xml
@@ -20,7 +20,7 @@
     <parent>
       	<groupId>org.apache.taverna.language</groupId>
       	<artifactId>apache-taverna-language</artifactId>
-      	<version>0.15.2-incubating-SNAPSHOT</version>
+      	<version>0.16.0-incubating-SNAPSHOT</version>
     </parent>
     <artifactId>taverna-tavlang-tool</artifactId>
     <name>Apache Taverna tavlang tool</name>