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:29 UTC

[44/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-robundle/README.md
----------------------------------------------------------------------
diff --git a/taverna-robundle/README.md b/taverna-robundle/README.md
new file mode 100644
index 0000000..d29b3c7
--- /dev/null
+++ b/taverna-robundle/README.md
@@ -0,0 +1,180 @@
+RO bundle API
+=============
+
+
+[![Build Status](https://travis-ci.org/wf4ever/robundle.svg)](https://travis-ci.org/wf4ever/robundle)
+[![DOI](https://zenodo.org/badge/doi/10.5072/zenodo.12703.png)](http://dx.doi.org/10.5072/zenodo.12703)
+
+
+ 
+
+
+API for building researchobject.org RO bundles. 
+
+Complies with [RO bundle specification](https://w3id.org/bundle) version [1.0](https://w3id.org/bundle/2014-11-05/).
+
+This API is built on the Java 7 NIO Files and uses the 
+[Java 7 ZIP file provider](http://docs.oracle.com/javase/7/docs/technotes/guides/io/fsp/zipfilesystemprovider.html) to generate the RO Bundle.
+
+The class 
+[org.apache.taverna.robundle.Bundles](src/main/java/org/purl/wf4ever/robundle/Bundles.java) complements the 
+Java 7 [java.nio.Files](http://docs.oracle.com/javase/7/docs/api/java/nio/file/Files.html) API 
+with more specific helper methods to work with RO Bundles.
+
+This API is the basis for the [Taverna Data Bundles API](https://github.com/myGrid/databundles).
+
+
+Slides
+------
+
+[![Slides](http://image.slidesharecdn.com/2014-04-24-robundles-140424044958-phpapp01/95/slide-1-638.jpg?cb=1398333951)](http://www.slideshare.net/soilandreyes/diving-into-research-objects)
+
+[Slides 2014-04-24](https://onedrive.live.com/view.aspx?cid=37935FEEE4DF1087&resid=37935FEEE4DF1087%21679&app=PowerPoint&authkey=%21AI6c4YT_419J3zY&wdo=1)
+
+
+Usage
+-----
+
+If you use [Maven 3](http://maven.apache.org/), then add to your `pom.xml`:
+
+```xml
+<dependencies>
+    <dependency>
+       	<groupId>org.apache.taverna.robundle</groupId>
+        <artifactId>robundle</artifactId>
+        <version>0.5.0</version>
+    </dependency>
+</dependencies>
+<repositories>
+    <repository>
+        <id>mygrid-repository</id>
+        <name>myGrid Repository</name>
+        <url>http://www.mygrid.org.uk/maven/repository</url>
+        <releases />
+        <snapshots>
+            <enabled>false</enabled>
+        </snapshots>
+    </repository>
+</repositories>
+```
+
+To find the latest `<version>` (in case the above has not been updated), see the
+list of [robundle releases](https://github.com/wf4ever/robundle/releases). To download a precompiled
+binary JAR, see the [myGrid's Maven repository](http://www.mygrid.org.uk/maven/repository/org/purl/wf4ever/robundle/robundle/).
+
+Building
+--------
+If you are building from source (this repository), then:
+
+```mvn clean install```
+
+should normally work, given a recent version of [Maven 3](http://maven.apache.org/download.cgi) and 
+[Java 7 SDK](http://www.oracle.com/technetwork/java/javase/downloads/jdk7-downloads-1880260.html).
+
+[myGrid's Jenkins installation](http://build.mygrid.org.uk/ci/) has automated builds of
+[robundle](http://build.mygrid.org.uk/ci/job/robundle/), which are deployed 
+to [myGrid's snapshot Maven repository](http://www.mygrid.org.uk/maven/snapshot-repository/org/purl/wf4ever/robundle/robundle/).
+
+To use a snapshot build, add this repository to `pom.xml`:
+
+```xml
+<repository>
+    <id>mygrid-snapshot-repository</id>
+    <name>myGrid Snapshot Repository</name>
+    <url>http://www.mygrid.org.uk/maven/snapshot-repository</url>
+    <releases>
+        <enabled>false</enabled>
+    </releases>
+    <snapshots />
+</repository>
+```
+
+Then change your `<dependency>` to match the `-SNAPSHOT` version in this project's [pom.xml](pom.xml).
+
+
+Supported bundle formats
+------------------------
+
+* [RO bundle specification](https://w3id.org/bundle).
+* [Adobe UFC](https://wikidocs.adobe.com/wiki/display/PDFNAV/UCF+overview)
+* [ePub OCF](http://www.idpf.org/epub3/latest/ocf)
+* [Open Document package (ODF)](http://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part3.html#__RefHeading__752807_826425813)
+* [COMBINE Archive (OMEX)](http://co.mbine.org/documents/archive)
+* [ZIP](http://www.pkware.com/documents/casestudies/APPNOTE.TXT)
+
+The `Bundles` API will load a bundle in any of the formats above, converging 
+them to a [Research Object Bundle](https://w3id.org/bundle), 
+while still maintaining the manifests of the other formats,
+if they exist within the bundle.
+
+Thus, if you open say a [COMBINE Archive](http://co.mbine.org/documents/archive) and add a couple of resources,
+indicating their mediatype using `bundle.getManifest().getAggregation(path).setMediaType("a/b")`, then 
+when closing this bundle, the API will generate both an RO Bundle manifest and a COMBINE manifest
+that reflect this.
+
+
+
+
+Example of use
+--------------
+
+Example in full is at [org.apache.taverna.robundle.TestExample](src/test/java/org/purl/wf4ever/robundle/TestExample.java)
+
+```java
+		// Create a new (temporary) RO bundle
+		Bundle bundle = Bundles.createBundle();
+
+		// Get the inputs
+		Path inputs = bundle.getRoot().resolve("inputs");
+		Files.createDirectory(inputs);
+
+		// Get an input port:
+		Path in1 = inputs.resolve("in1");
+
+		// Setting a string value for the input port:
+		Bundles.setStringValue(in1, "Hello");
+
+		// And retrieving it
+		if (Bundles.isValue(in1)) {
+			System.out.println(Bundles.getStringValue(in1));
+		}
+
+		// Or just use the regular Files methods:
+		for (String line : Files.readAllLines(in1, Charset.forName("UTF-8"))) {
+			System.out.println(line);
+		}
+
+		// Binaries and large files are done through the Files API
+		try (OutputStream out = Files.newOutputStream(in1,
+				StandardOpenOption.APPEND)) {
+			out.write(32);
+		}
+		// Or Java 7 style
+		Path localFile = Files.createTempFile("", ".txt");
+		Files.copy(in1, localFile, StandardCopyOption.REPLACE_EXISTING);
+		System.out.println("Written to: " + localFile);
+
+		Files.copy(localFile, bundle.getRoot().resolve("out1"));
+
+		// Representing references
+		URI ref = URI.create("http://example.com/external.txt");
+		Path out3 = bundle.getRoot().resolve("out3");
+		System.out.println(Bundles.setReference(out3, ref));
+		if (Bundles.isReference(out3)) {
+			URI resolved = Bundles.getReference(out3);
+			System.out.println(resolved);
+		}
+
+		// Saving a bundle:
+		Path zip = Files.createTempFile("bundle", ".zip");
+		Bundles.closeAndSaveBundle(bundle, zip);
+		// NOTE: From now "bundle" and its Path's are CLOSED
+		// and can no longer be accessed
+
+		System.out.println("Saved to " + zip);
+
+		// Loading a bundle back from disk
+		try (Bundle bundle2 = Bundles.openBundle(zip)) {
+			assertEquals(zip, bundle2.getSource());
+		}
+ ```

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/pom.xml
----------------------------------------------------------------------
diff --git a/taverna-robundle/pom.xml b/taverna-robundle/pom.xml
new file mode 100644
index 0000000..a1cfa7b
--- /dev/null
+++ b/taverna-robundle/pom.xml
@@ -0,0 +1,132 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+   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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+	<packaging>bundle</packaging>
+	<parent>
+		<groupId>org.apache.taverna.language</groupId>
+		<artifactId>taverna-language</artifactId>
+		<version>0.16.1-incubating-SNAPSHOT</version>
+	</parent>
+	<artifactId>taverna-robundle</artifactId>
+	<name>Apache Taverna RO Bundle API</name>
+	<description>API for dealing with RO Bundles</description>
+	<dependencies>
+		<dependency>
+			<groupId>junit</groupId>
+			<artifactId>junit</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>commons-io</groupId>
+			<artifactId>commons-io</artifactId>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-core</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-annotations</artifactId>
+			<version>${jackson.version}</version>
+		</dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+		</dependency>
+		<dependency>
+			<groupId>com.github.jsonld-java</groupId>
+			<artifactId>jsonld-java</artifactId>
+			<version>${jsonld.version}</version>
+			<scope>test</scope>
+		</dependency>
+		<dependency>
+			<groupId>commons-configuration</groupId>
+			<artifactId>commons-configuration</artifactId>
+			<version>1.9</version>
+		</dependency>
+		<dependency>
+			<groupId>org.apache.jena</groupId>
+			<artifactId>jena-arq</artifactId>
+			<version>${jena.version}</version>
+			<exclusions>
+				<exclusion>
+					<artifactId>httpclient</artifactId>
+					<groupId>org.apache.httpcomponents</groupId>
+				</exclusion>
+			</exclusions>
+		</dependency>
+<!--
+		<dependency>
+			<groupId>com.sun.xml.bind</groupId>
+			<artifactId>jaxb-impl</artifactId>
+			<version>2.2.7</version>
+		</dependency>
+-->
+		<dependency>
+			<groupId>com.sun.xml.bind</groupId>
+			<artifactId>jaxb-osgi</artifactId>
+			<version>${jaxb.version}</version>
+		</dependency>
+<!-- for UUIDv5 SHA named
+		<dependency>
+			<groupId>com.fasterxml.uuid</groupId>
+			<artifactId>java-uuid-generator</artifactId>
+			<version>3.1.3</version>
+			<type>bundle</type>
+		</dependency>
+-->
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.apache.felix</groupId>
+				<artifactId>maven-bundle-plugin</artifactId>
+				<extensions>true</extensions>
+				<configuration>
+					<instructions>
+						<Embed-Dependency>*;groupId=com.github.jsonld-java</Embed-Dependency>
+						<Embed-Transitive>true</Embed-Transitive>
+					</instructions>
+				</configuration>
+			</plugin>
+			<plugin>
+				<groupId>org.codehaus.mojo</groupId>
+				<artifactId>jaxb2-maven-plugin</artifactId>
+				<executions>
+					<execution>
+						<id>jaxb-xsd</id>
+						<phase>generate-sources</phase>
+						<goals>
+							<goal>xjc</goal>
+						</goals>
+						<configuration>
+							<!-- <packageName>oasis.names.tc.opendocument.xmlns.manifest</packageName>-->
+						</configuration>
+					</execution>
+				</executions>
+			</plugin>
+			<plugin>
+				<groupId>org.apache.rat</groupId>
+				<artifactId>apache-rat-plugin</artifactId>
+			</plugin>
+		</plugins>
+	</build>
+</project>

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/src/main/java/org/apache/taverna/robundle/Bundle.java
----------------------------------------------------------------------
diff --git a/taverna-robundle/src/main/java/org/apache/taverna/robundle/Bundle.java b/taverna-robundle/src/main/java/org/apache/taverna/robundle/Bundle.java
new file mode 100644
index 0000000..df2d28a
--- /dev/null
+++ b/taverna-robundle/src/main/java/org/apache/taverna/robundle/Bundle.java
@@ -0,0 +1,133 @@
+package org.apache.taverna.robundle;
+
+/*
+ * 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.nio.file.Files.deleteIfExists;
+import static java.nio.file.Files.exists;
+import static java.nio.file.Files.newInputStream;
+import static org.apache.taverna.robundle.Bundles.getManifestPath;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+
+import org.apache.taverna.robundle.fs.BundleFileSystem;
+import org.apache.taverna.robundle.manifest.Manifest;
+import org.apache.taverna.robundle.manifest.RDFToManifest;
+import org.apache.taverna.robundle.manifest.combine.CombineManifest;
+import org.apache.taverna.robundle.manifest.odf.ODFManifest;
+
+public class Bundle implements Closeable {
+	private boolean deleteOnClose;
+	private Manifest manifest;
+	private final Path root;
+
+	public Bundle(Path root, boolean deleteOnClose) {
+		this.root = root;
+		this.setDeleteOnClose(deleteOnClose);
+	}
+
+	@Override
+	public void close() throws IOException {
+		close(isDeleteOnClose());
+	}
+
+	protected void close(boolean deleteOnClose) throws IOException {
+		if (!getFileSystem().isOpen())
+			return;
+
+		if (!deleteOnClose) {
+			// update manifest
+			getManifest().populateFromBundle();
+			getManifest().writeAsJsonLD();
+			if (ODFManifest.containsManifest(this))
+				getManifest().writeAsODFManifest();
+			if (CombineManifest.containsManifest(this))
+				getManifest().writeAsCombineManifest();
+		} else {
+			/*
+			 * FIXME: Enable this if closing temporary bundles is slow doing
+			 * closing (as those files are being compressed):
+			 * RecursiveDeleteVisitor.deleteRecursively(getRoot());
+			 */
+		}
+		getFileSystem().close();
+		if (deleteOnClose)
+			deleteIfExists(getSource());
+	}
+
+	public FileSystem getFileSystem() {
+		return getRoot().getFileSystem();
+	}
+
+	public Manifest getManifest() throws IOException {
+		if (manifest == null)
+			synchronized (this) {
+				if (manifest == null)
+					manifest = readOrPopulateManifest();
+			}
+		return manifest;
+	}
+
+	public Path getPath(String path) {
+		return getRoot().resolve(path);
+	}
+
+	public Path getRoot() {
+		return root;
+	}
+
+	public Path getSource() {
+		BundleFileSystem fs = (BundleFileSystem) getFileSystem();
+		return fs.getSource();
+	}
+
+	public boolean isDeleteOnClose() {
+		return deleteOnClose;
+	}
+
+	protected Manifest readOrPopulateManifest() throws IOException {
+		Manifest newManifest = new Manifest(this);
+		Path manifestPath = getManifestPath(this);
+		if (exists(manifestPath)) {
+			try (InputStream manifestStream = newInputStream(manifestPath)) {
+				new RDFToManifest().readTo(manifestStream, newManifest,
+						manifestPath.toUri());
+			}
+			// TODO: Also support reading manifest.rdf?
+		} else if (ODFManifest.containsManifest(this)) {
+			new ODFManifest(newManifest).readManifestXML();
+		} else if (CombineManifest.containsManifest(this)) {
+			new CombineManifest(newManifest).readCombineArchive();
+		} else {
+			// Fallback (might be a fresh or 3rd party bundle), populate from
+			// zip content
+			newManifest.populateFromBundle();
+		}
+		return newManifest;
+	}
+
+	public void setDeleteOnClose(boolean deleteOnClose) {
+		this.deleteOnClose = deleteOnClose;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/src/main/java/org/apache/taverna/robundle/Bundles.java
----------------------------------------------------------------------
diff --git a/taverna-robundle/src/main/java/org/apache/taverna/robundle/Bundles.java b/taverna-robundle/src/main/java/org/apache/taverna/robundle/Bundles.java
new file mode 100644
index 0000000..eaf0359
--- /dev/null
+++ b/taverna-robundle/src/main/java/org/apache/taverna/robundle/Bundles.java
@@ -0,0 +1,401 @@
+package org.apache.taverna.robundle;
+
+/*
+ * 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.nio.file.Files.copy;
+import static java.nio.file.Files.createDirectories;
+import static java.nio.file.Files.createTempFile;
+import static java.nio.file.Files.deleteIfExists;
+import static java.nio.file.Files.exists;
+import static java.nio.file.Files.isDirectory;
+import static java.nio.file.Files.isRegularFile;
+import static java.nio.file.Files.move;
+import static java.nio.file.Files.newBufferedReader;
+import static java.nio.file.Files.newBufferedWriter;
+import static java.nio.file.Files.newDirectoryStream;
+import static java.nio.file.Files.readAllBytes;
+import static java.nio.file.Files.write;
+import static java.nio.file.StandardCopyOption.ATOMIC_MOVE;
+import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static org.apache.taverna.robundle.fs.BundleFileSystemProvider.APPLICATION_VND_WF4EVER_ROBUNDLE_ZIP;
+import static org.apache.taverna.robundle.fs.BundleFileSystemProvider.MIMETYPE_FILE;
+import static org.apache.taverna.robundle.fs.BundleFileSystemProvider.newFileSystemFromExisting;
+import static org.apache.taverna.robundle.fs.BundleFileSystemProvider.newFileSystemFromNew;
+import static org.apache.taverna.robundle.fs.BundleFileSystemProvider.newFileSystemFromTemporary;
+import static org.apache.taverna.robundle.utils.PathHelper.relativizeFromBase;
+import static org.apache.taverna.robundle.utils.TemporaryFiles.temporaryBundle;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.nio.file.AtomicMoveNotSupportedException;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryNotEmptyException;
+import java.nio.file.DirectoryStream;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.text.MessageFormat;
+
+import org.apache.commons.configuration.ConfigurationException;
+import org.apache.commons.configuration.HierarchicalINIConfiguration;
+import org.apache.taverna.robundle.fs.BundleFileSystem;
+import org.apache.taverna.robundle.utils.RecursiveCopyFileVisitor;
+import org.apache.taverna.robundle.utils.RecursiveDeleteVisitor;
+
+/**
+ * Utility functions for dealing with RO bundles.
+ * <p>
+ * The style of using this class is similar to that of {@link Files}. In fact, a
+ * RO bundle is implemented as a set of {@link Path}s.
+ * 
+ * @author Stian Soiland-Reyes
+ */
+public class Bundles {
+	private static final String ANNOTATIONS = "annotations";
+	private static final Charset ASCII = Charset.forName("ASCII");
+	private static final String DOT_RO = ".ro";
+
+	protected static final String DOT_URL = ".url";
+
+	private static final String INI_INTERNET_SHORTCUT = "InternetShortcut";
+	private static final String INI_URL = "URL";
+	private static final Charset LATIN1 = Charset.forName("Latin1");
+	private static final String MANIFEST_JSON = "manifest.json";
+	private static final Charset UTF8 = Charset.forName("UTF-8");
+
+	public static void closeAndSaveBundle(Bundle bundle, Path destination)
+			throws IOException {
+		Path zipPath = closeBundle(bundle);
+		if (bundle.isDeleteOnClose()) {
+			safeMove(zipPath, destination);
+		} else {
+			safeCopy(zipPath, destination);
+		}
+	}
+
+	public static Path closeBundle(Bundle bundle) throws IOException {
+		Path path = bundle.getSource();
+		bundle.close(false);
+		return path;
+	}
+
+	public static void copyRecursively(final Path source,
+			final Path destination, final CopyOption... copyOptions)
+			throws IOException {
+		RecursiveCopyFileVisitor.copyRecursively(source, destination,
+				copyOptions);
+	}
+
+	public static Bundle createBundle() throws IOException {
+		BundleFileSystem fs = newFileSystemFromTemporary();
+		return new Bundle(fs.getRootDirectory(), true);
+	}
+
+	public static Bundle createBundle(Path path) throws IOException {
+		BundleFileSystem fs = newFileSystemFromNew(path);
+		return new Bundle(fs.getRootDirectory(), false);
+	}
+
+	public static void deleteRecursively(Path p) throws IOException {
+		RecursiveDeleteVisitor.deleteRecursively(p);
+	}
+
+	protected static String filenameWithoutExtension(Path entry) {
+		String fileName = entry.getFileName().toString();
+		int lastDot = fileName.lastIndexOf(".");
+		if (lastDot < 0)
+			// return fileName;
+			return fileName.replace("/", "");
+		return fileName.substring(0, lastDot);
+	}
+
+	public static Path getAnnotations(Bundle bundle) throws IOException {
+		Path dir = bundle.getFileSystem().getPath(DOT_RO, ANNOTATIONS);
+		createDirectories(dir);
+		return dir;
+	}
+
+	public static Path getManifestPath(Bundle bundle) {
+		return bundle.getRoot().resolve(DOT_RO).resolve(MANIFEST_JSON);
+	}
+
+	public static String getMimeType(Bundle bundle) throws IOException {
+		Path mimetypePath = bundle.getRoot().resolve(MIMETYPE_FILE);
+		String mimetype = getStringValue(mimetypePath);
+		if (mimetype == null || mimetype.isEmpty())
+			return APPLICATION_VND_WF4EVER_ROBUNDLE_ZIP;
+		return mimetype.trim();
+	}
+
+	public static URI getReference(Path path) throws IOException {
+		if (path == null || isMissing(path))
+			return null;
+		if (!isReference(path))
+			throw new IllegalArgumentException("Not a reference: " + path);
+		// Note: Latin1 is chosen here because it would not bail out on
+		// "strange" characters. We actually parse the URL as ASCII
+		path = withExtension(path, DOT_URL);
+		try (BufferedReader r = newBufferedReader(path, LATIN1)) {
+			HierarchicalINIConfiguration ini = new HierarchicalINIConfiguration();
+			ini.load(r);
+
+			String urlStr = ini.getSection(INI_INTERNET_SHORTCUT).getString(
+					INI_URL);
+
+			// String urlStr = ini.get(INI_INTERNET_SHORTCUT, INI_URL);
+			if (urlStr == null)
+				throw new IOException("Invalid/unsupported URL format: " + path);
+			return URI.create(urlStr);
+		} catch (ConfigurationException e) {
+			throw new IOException("Can't parse reference: " + path, e);
+		}
+	}
+
+	public static String getStringValue(Path path) throws IOException {
+		if (path == null || isMissing(path))
+			return null;
+		if (!isValue(path))
+			throw new IllegalArgumentException("Not a value: " + path);
+		return new String(readAllBytes(path), UTF8);
+	}
+
+	public static boolean isMissing(Path item) {
+		return !exists(item) && !isReference(item);
+	}
+
+	public static boolean isReference(Path path) {
+		return isRegularFile(withExtension(path, DOT_URL));
+	}
+
+	public static boolean isValue(Path path) {
+		return !isReference(path) && isRegularFile(path);
+	}
+
+	public static Bundle openBundle(InputStream in) throws IOException {
+		Path path = temporaryBundle();
+		copy(in, path);
+		Bundle bundle = openBundle(path);
+		bundle.setDeleteOnClose(true);
+		return bundle;
+	}
+
+	public static Bundle openBundle(Path zip) throws IOException {
+		BundleFileSystem fs = newFileSystemFromExisting(zip);
+		return new Bundle(fs.getRootDirectory(), false);
+	}
+
+	public static Bundle openBundle(URL url) throws IOException {
+		try {
+			if ("file".equals(url.getProtocol()))
+				return openBundle(Paths.get(url.toURI()));
+			else
+				try (InputStream in = url.openStream()) {
+					return openBundle(in);
+				}
+		} catch (URISyntaxException e) {
+			throw new IllegalArgumentException("Invalid URL " + url, e);
+		}
+	}
+
+	public static Bundle openBundleReadOnly(Path zip) throws IOException {
+		Path tmpBundle = temporaryBundle();
+		// BundleFileSystemProvider requires write-access, so we'll have to copy
+		// it
+		copy(zip, tmpBundle);
+		BundleFileSystem fs = newFileSystemFromExisting(tmpBundle);
+		// And this temporary file will be deleted afterwards
+		return new Bundle(fs.getRootDirectory(), true);
+	}
+
+	public static void safeCopy(Path source, Path destination)
+			throws IOException {
+		safeMoveOrCopy(source, destination, false);
+	}
+
+	public static void safeMove(Path source, Path destination)
+			throws IOException {
+		safeMoveOrCopy(source, destination, true);
+	}
+
+	protected static void safeMoveOrCopy(Path source, Path destination,
+			boolean move) throws IOException {
+		// First just try to do an atomic move with overwrite
+		try {
+			if (move
+					&& source.getFileSystem().provider()
+							.equals(destination.getFileSystem().provider())) {
+				move(source, destination, ATOMIC_MOVE, REPLACE_EXISTING);
+				return;
+			}
+		} catch (AtomicMoveNotSupportedException ex) {
+			// Do the fallback by temporary files below
+		}
+
+		destination = destination.toAbsolutePath();
+
+		String tmpName = destination.getFileName().toString();
+		Path tmpDestination = createTempFile(destination.getParent(), tmpName,
+				".tmp");
+		Path backup = null;
+		try {
+			if (move) {
+				/*
+				 * This might do a copy if filestores differ .. hence to avoid
+				 * an incomplete (and partially overwritten) destination, we do
+				 * it first to a temporary file
+				 */
+				move(source, tmpDestination, REPLACE_EXISTING);
+			} else {
+				copy(source, tmpDestination, REPLACE_EXISTING);
+			}
+
+			if (exists(destination)) {
+				if (isDirectory(destination))
+					// ensure it is empty
+					try (DirectoryStream<Path> ds = newDirectoryStream(destination)) {
+						if (ds.iterator().hasNext())
+							throw new DirectoryNotEmptyException(
+									destination.toString());
+					}
+				// Keep the files for roll-back in case it goes bad
+				backup = createTempFile(destination.getParent(), tmpName,
+						".orig");
+				move(destination, backup, REPLACE_EXISTING);
+			}
+			// OK ; let's swap over
+			try {
+				// prefer ATOMIC_MOVE
+				move(tmpDestination, destination, REPLACE_EXISTING, ATOMIC_MOVE);
+			} catch (AtomicMoveNotSupportedException ex) {
+				/*
+				 * possibly a network file system as src/dest should be in same
+				 * folder
+				 */
+				move(tmpDestination, destination, REPLACE_EXISTING);
+			} finally {
+				if (!exists(destination) && backup != null)
+					// Restore the backup
+					move(backup, destination);
+			}
+			// It went well, tidy up
+			if (backup != null)
+				deleteIfExists(backup);
+		} finally {
+			deleteIfExists(tmpDestination);
+		}
+	}
+
+	public static void setMimeType(Bundle bundle, String mimetype)
+			throws IOException {
+		if (!ASCII.newEncoder().canEncode(mimetype))
+			throw new IllegalArgumentException("mimetype must be ASCII, not "
+					+ mimetype);
+		if (mimetype.contains("\n") || mimetype.contains("\r"))
+			throw new IllegalArgumentException(
+					"mimetype can't contain newlines");
+		if (!mimetype.contains("/"))
+			throw new IllegalArgumentException("Invalid mimetype: " + mimetype);
+
+		Path root = bundle.getRoot();
+		Path mimetypePath = root.resolve(MIMETYPE_FILE);
+		if (!isRegularFile(mimetypePath)) {
+			/*
+			 * It would require low-level zip-modification to properly add
+			 * 'mimetype' now
+			 */
+			throw new IOException("Special file '" + MIMETYPE_FILE
+					+ "' missing from bundle, can't set mimetype");
+		}
+		setStringValue(mimetypePath, mimetype);
+	}
+
+	public static Path setReference(Path path, URI ref) throws IOException {
+		path = withExtension(path, DOT_URL);
+
+		// We'll save a IE-like .url "Internet shortcut" in INI format.
+
+		// HierarchicalINIConfiguration ini = new
+		// HierarchicalINIConfiguration();
+		// ini.getSection(INI_INTERNET_SHORTCUT).addProperty(INI_URL,
+		// ref.toASCIIString());
+
+		// Ini ini = new Wini();
+		// ini.getConfig().setLineSeparator("\r\n");
+		// ini.put(INI_INTERNET_SHORTCUT, INI_URL, ref.toASCIIString());
+
+		/*
+		 * Neither of the above create a .url that is compatible with Safari on
+		 * Mac OS (which expects "URL=" rather than "URL = ", so instead we make
+		 * it manually with MessageFormat.format:
+		 */
+
+		// Includes a terminating double line-feed -- which Safari might also
+		// need
+		String iniTmpl = "[{0}]\r\n{1}={2}\r\n\r\n";
+		String ini = MessageFormat.format(iniTmpl, INI_INTERNET_SHORTCUT,
+				INI_URL, ref.toASCIIString());
+
+		// NOTE: We use Latin1 here, but because of
+		try (BufferedWriter w = newBufferedWriter(path, ASCII,
+				TRUNCATE_EXISTING, CREATE)) {
+			// ini.save(w);
+			// ini.store(w);
+			w.write(ini);
+			// } catch (ConfigurationException e) {
+			// throw new IOException("Can't write shortcut to " + path, e);
+		}
+		return path;
+	}
+
+	public static void setStringValue(Path path, String string)
+			throws IOException {
+		write(path, string.getBytes(UTF8), TRUNCATE_EXISTING, CREATE);
+	}
+
+	public static Path uriToBundlePath(Bundle bundle, URI uri) {
+		URI rootUri = bundle.getRoot().toUri();
+		uri = relativizeFromBase(uri, rootUri);
+		if (uri.isAbsolute() || uri.getFragment() != null)
+			return null;
+		return bundle.getFileSystem().provider().getPath(rootUri.resolve(uri));
+	}
+
+	protected static Path withExtension(Path path, String extension) {
+		if (!extension.isEmpty() && !extension.startsWith("."))
+			throw new IllegalArgumentException(
+					"Extension must be empty or start with .");
+		String p = path.getFileName().toString();
+		if (!extension.isEmpty()
+				&& p.toLowerCase().endsWith(extension.toLowerCase()))
+			return path;
+		// Everything after the last . - or just the end
+		String newP = p.replaceFirst("(\\.[^.]*)?$", extension);
+		return path.resolveSibling(newP);
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileStore.java
----------------------------------------------------------------------
diff --git a/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileStore.java b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileStore.java
new file mode 100644
index 0000000..208f979
--- /dev/null
+++ b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileStore.java
@@ -0,0 +1,98 @@
+package org.apache.taverna.robundle.fs;
+
+/*
+ * 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.IOException;
+import java.nio.file.FileStore;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.attribute.FileStoreAttributeView;
+
+public class BundleFileStore extends FileStore {
+
+	// private final BundleFileSystem fs;
+	private final FileStore origFileStore;
+
+	protected BundleFileStore(BundleFileSystem fs, FileStore origFileStore) {
+		if (fs == null || origFileStore == null) {
+			throw new NullPointerException();
+		}
+		// this.fs = fs;
+		this.origFileStore = origFileStore;
+	}
+
+	@Override
+	public Object getAttribute(String attribute) throws IOException {
+		return origFileStore.getAttribute(attribute);
+	}
+
+	@Override
+	public <V extends FileStoreAttributeView> V getFileStoreAttributeView(
+			Class<V> type) {
+		return origFileStore.getFileStoreAttributeView(type);
+	}
+
+	@Override
+	public long getTotalSpace() throws IOException {
+		return origFileStore.getTotalSpace();
+	}
+
+	@Override
+	public long getUnallocatedSpace() throws IOException {
+		return origFileStore.getUnallocatedSpace();
+	}
+
+	@Override
+	public long getUsableSpace() throws IOException {
+		return origFileStore.getUsableSpace();
+	}
+
+	@Override
+	public boolean isReadOnly() {
+		return origFileStore.isReadOnly();
+	}
+
+	@Override
+	public String name() {
+		return origFileStore.name();
+	}
+
+	@Override
+	public boolean supportsFileAttributeView(
+			Class<? extends FileAttributeView> type) {
+		return origFileStore.supportsFileAttributeView(type);
+	}
+
+	@Override
+	public boolean supportsFileAttributeView(String name) {
+		return origFileStore.supportsFileAttributeView(name);
+	}
+
+	@Override
+	public String toString() {
+		return origFileStore.toString();
+	}
+
+	@Override
+	public String type() {
+		return "bundle";
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileSystem.java
----------------------------------------------------------------------
diff --git a/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileSystem.java b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileSystem.java
new file mode 100644
index 0000000..363b8cf
--- /dev/null
+++ b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileSystem.java
@@ -0,0 +1,223 @@
+package org.apache.taverna.robundle.fs;
+
+/*
+ * 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.IOException;
+import java.net.URI;
+import java.nio.file.ClosedFileSystemException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.Path;
+import java.nio.file.PathMatcher;
+import java.nio.file.Paths;
+import java.nio.file.WatchService;
+import java.nio.file.attribute.UserPrincipalLookupService;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.Set;
+
+public class BundleFileSystem extends FileSystem {
+
+	protected final URI baseURI;
+	private FileSystem origFS;
+	private final String separator;
+	private final Path source;
+
+	protected BundleFileSystem(FileSystem origFS, URI baseURI) {
+		if (origFS == null || baseURI == null) {
+			throw new NullPointerException();
+		}
+		this.origFS = origFS;
+		this.baseURI = baseURI;
+		this.separator = origFS.getSeparator();
+		this.source = findSource();
+
+	}
+
+	@Override
+	public void close() throws IOException {
+		if (origFS == null) {
+			return;
+		}
+		origFS.close();
+		// De-reference the original ZIP file system so it can be
+		// garbage collected
+		origFS = null;
+	}
+
+	protected Path findSource() {
+		Path zipRoot = getRootDirectory().getZipPath();
+		URI uri = zipRoot.toUri();
+		String schemeSpecific;
+		if (provider().getJarDoubleEscaping()) {
+			schemeSpecific = uri.getSchemeSpecificPart();
+		} else {
+			// http://dev.mygrid.org.uk/issues/browse/T3-954
+			schemeSpecific = uri.getRawSchemeSpecificPart();
+		}
+		if (!schemeSpecific.endsWith("!/")) { // sanity check
+			throw new IllegalStateException("Can't parse JAR URI: " + uri);
+		}
+		URI zip = URI.create(schemeSpecific.substring(0,
+				schemeSpecific.length() - 2));
+		return Paths.get(zip); // Look up our path
+	}
+
+	public URI getBaseURI() {
+		return baseURI;
+	}
+
+	protected BundleFileStore getFileStore() {
+		// We assume there's only one file store, as is true for ZipProvider
+		return new BundleFileStore(this, getOrigFS().getFileStores().iterator()
+				.next());
+	}
+
+	@Override
+	public Iterable<FileStore> getFileStores() {
+		return Collections.<FileStore> singleton(getFileStore());
+	}
+
+	/**
+	 * Thread-safe ClosedFileSystemException test
+	 * 
+	 * @return
+	 */
+	protected FileSystem getOrigFS() {
+		FileSystem orig = origFS;
+		if (orig == null || !orig.isOpen()) {
+			throw new ClosedFileSystemException();
+		}
+		return orig;
+	}
+
+	@Override
+	public Path getPath(String first, String... more) {
+		Path zipPath = getOrigFS().getPath(first, more);
+		return wrap(zipPath);
+	}
+
+	@Override
+	public PathMatcher getPathMatcher(String syntaxAndPattern) {
+		final PathMatcher zipMatcher = getOrigFS().getPathMatcher(
+				syntaxAndPattern);
+		return new PathMatcher() {
+			@Override
+			public boolean matches(Path path) {
+				return zipMatcher.matches(unwrap(path));
+			}
+		};
+	}
+
+	@Override
+	public Iterable<Path> getRootDirectories() {
+		return Collections.<Path> singleton(getRootDirectory());
+	}
+
+	public BundlePath getRootDirectory() {
+		return wrap(getOrigFS().getRootDirectories().iterator().next());
+	}
+
+	@Override
+	public String getSeparator() {
+		return separator;
+	}
+
+	public Path getSource() {
+		return source;
+	}
+
+	@Override
+	public UserPrincipalLookupService getUserPrincipalLookupService() {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public boolean isOpen() {
+		if (origFS == null) {
+			return false;
+		}
+		return origFS.isOpen();
+	}
+
+	@Override
+	public boolean isReadOnly() {
+		return getOrigFS().isReadOnly();
+	}
+
+	@Override
+	public WatchService newWatchService() throws IOException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public BundleFileSystemProvider provider() {
+		return BundleFileSystemProvider.getInstance();
+	}
+
+	@Override
+	public Set<String> supportedFileAttributeViews() {
+		if (origFS == null) {
+			throw new ClosedFileSystemException();
+		}
+		return origFS.supportedFileAttributeViews();
+	}
+
+	protected Path unwrap(Path bundlePath) {
+		if (!(bundlePath instanceof BundlePath)) {
+			// assume it's already unwrapped for some reason (for instance being
+			// null)
+			return bundlePath;
+		}
+		return ((BundlePath) bundlePath).getZipPath();
+	}
+
+	protected BundlePath wrap(Path zipPath) {
+		if (zipPath == null) {
+			return null;
+		}
+		if (zipPath instanceof BundlePath) {
+			throw new IllegalArgumentException("Did not expect BundlePath: "
+					+ zipPath);
+		}
+		return new BundlePath(this, zipPath);
+	}
+
+	protected Iterator<Path> wrapIterator(final Iterator<Path> iterator) {
+		return new Iterator<Path>() {
+			@Override
+			public boolean hasNext() {
+				return iterator.hasNext();
+			}
+
+			@Override
+			public Path next() {
+				return wrap(iterator.next());
+			}
+
+			@Override
+			public void remove() {
+				iterator.remove();
+			}
+		};
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileSystemProvider.java
----------------------------------------------------------------------
diff --git a/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileSystemProvider.java b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileSystemProvider.java
new file mode 100644
index 0000000..807c9fd
--- /dev/null
+++ b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileSystemProvider.java
@@ -0,0 +1,695 @@
+package org.apache.taverna.robundle.fs;
+
+/*
+ * 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.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.lang.ref.WeakReference;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.nio.ByteBuffer;
+import java.nio.MappedByteBuffer;
+import java.nio.channels.FileChannel;
+import java.nio.channels.FileLock;
+import java.nio.channels.ReadableByteChannel;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.channels.WritableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.file.AccessMode;
+import java.nio.file.CopyOption;
+import java.nio.file.DirectoryStream;
+import java.nio.file.DirectoryStream.Filter;
+import java.nio.file.FileAlreadyExistsException;
+import java.nio.file.FileStore;
+import java.nio.file.FileSystem;
+import java.nio.file.FileSystemAlreadyExistsException;
+import java.nio.file.FileSystemNotFoundException;
+import java.nio.file.FileSystems;
+import java.nio.file.Files;
+import java.nio.file.LinkOption;
+import java.nio.file.NoSuchFileException;
+import java.nio.file.OpenOption;
+import java.nio.file.Path;
+import java.nio.file.Paths;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileAttribute;
+import java.nio.file.attribute.FileAttributeView;
+import java.nio.file.spi.FileSystemProvider;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+import java.util.UUID;
+import java.util.zip.CRC32;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipOutputStream;
+
+import org.apache.taverna.robundle.utils.TemporaryFiles;
+
+public class BundleFileSystemProvider extends FileSystemProvider {
+	public class BundleFileChannel extends FileChannel {
+
+		@SuppressWarnings("unused")
+		private FileAttribute<?>[] attrs;
+		private FileChannel fc;
+		@SuppressWarnings("unused")
+		private Set<? extends OpenOption> options;
+		@SuppressWarnings("unused")
+		private Path path;
+
+		public BundleFileChannel(FileChannel fc, Path path,
+				Set<? extends OpenOption> options, FileAttribute<?>[] attrs) {
+			this.fc = fc;
+			this.path = path;
+			this.options = options;
+			this.attrs = attrs;
+		}
+
+		@Override
+		public void force(boolean metaData) throws IOException {
+			fc.force(metaData);
+		}
+
+		@Override
+		protected void implCloseChannel() throws IOException {
+			fc.close();
+			// TODO: Update manifest
+		}
+
+		@Override
+		public FileLock lock(long position, long size, boolean shared)
+				throws IOException {
+			return fc.lock(position, size, shared);
+		}
+
+		@Override
+		public MappedByteBuffer map(MapMode mode, long position, long size)
+				throws IOException {
+			return fc.map(mode, position, size);
+		}
+
+		@Override
+		public long position() throws IOException {
+			return fc.position();
+		}
+
+		@Override
+		public FileChannel position(long newPosition) throws IOException {
+			return fc.position(newPosition);
+		}
+
+		@Override
+		public int read(ByteBuffer dst) throws IOException {
+			return fc.read(dst);
+		}
+
+		@Override
+		public int read(ByteBuffer dst, long position) throws IOException {
+			return fc.read(dst, position);
+		}
+
+		@Override
+		public long read(ByteBuffer[] dsts, int offset, int length)
+				throws IOException {
+			return fc.read(dsts, offset, length);
+		}
+
+		@Override
+		public long size() throws IOException {
+			return fc.size();
+		}
+
+		@Override
+		public long transferFrom(ReadableByteChannel src, long position,
+				long count) throws IOException {
+			return fc.transferFrom(src, position, count);
+		}
+
+		@Override
+		public long transferTo(long position, long count,
+				WritableByteChannel target) throws IOException {
+			return fc.transferTo(position, count, target);
+		}
+
+		@Override
+		public FileChannel truncate(long size) throws IOException {
+			return fc.truncate(size);
+		}
+
+		@Override
+		public FileLock tryLock(long position, long size, boolean shared)
+				throws IOException {
+			return fc.tryLock(position, size, shared);
+		}
+
+		@Override
+		public int write(ByteBuffer src) throws IOException {
+			return fc.write(src);
+		}
+
+		@Override
+		public int write(ByteBuffer src, long position) throws IOException {
+			return fc.write(src, position);
+		}
+
+		@Override
+		public long write(ByteBuffer[] srcs, int offset, int length)
+				throws IOException {
+			return fc.write(srcs, offset, length);
+		}
+
+	}
+
+	private static class Singleton {
+		// Fallback for OSGi environments
+		private static final BundleFileSystemProvider INSTANCE = new BundleFileSystemProvider();
+	}
+
+	private static final String APP = "app";
+
+	public static final String APPLICATION_VND_WF4EVER_ROBUNDLE_ZIP = "application/vnd.wf4ever.robundle+zip";
+	public static final String MIMETYPE_FILE = "mimetype";
+
+	/**
+	 * The list of open file systems. This is static so that it is shared across
+	 * eventual multiple instances of this provider (such as when running in an
+	 * OSGi environment). Access to this map should be synchronized to avoid
+	 * opening a file system that is not in the map.
+	 */
+	protected static Map<URI, WeakReference<BundleFileSystem>> openFilesystems = new HashMap<>();
+
+	private static final Charset UTF8 = Charset.forName("UTF-8");
+
+	protected static void addMimeTypeToZip(ZipOutputStream out, String mimetype)
+			throws IOException {
+		if (mimetype == null) {
+			mimetype = APPLICATION_VND_WF4EVER_ROBUNDLE_ZIP;
+		}
+		// FIXME: Make the mediatype a parameter
+		byte[] bytes = mimetype.getBytes(UTF8);
+
+		// We'll have to do the mimetype file quite low-level
+		// in order to ensure it is STORED and not COMPRESSED
+
+		ZipEntry entry = new ZipEntry(MIMETYPE_FILE);
+		entry.setMethod(ZipEntry.STORED);
+		entry.setSize(bytes.length);
+		CRC32 crc = new CRC32();
+		crc.update(bytes);
+		entry.setCrc(crc.getValue());
+
+		out.putNextEntry(entry);
+		out.write(bytes);
+		out.closeEntry();
+	}
+
+	protected static void createBundleAsZip(Path bundle, String mimetype)
+			throws FileNotFoundException, IOException {
+		// Create ZIP file as
+		// http://docs.oracle.com/javase/7/docs/technotes/guides/io/fsp/zipfilesystemprovider.html
+		try (ZipOutputStream out = new ZipOutputStream(Files.newOutputStream(
+				bundle, StandardOpenOption.CREATE,
+				StandardOpenOption.TRUNCATE_EXISTING))) {
+			addMimeTypeToZip(out, mimetype);
+		}
+	}
+
+	public static BundleFileSystemProvider getInstance() {
+		for (FileSystemProvider provider : FileSystemProvider
+				.installedProviders()) {
+			if (provider instanceof BundleFileSystemProvider) {
+				return (BundleFileSystemProvider) provider;
+			}
+		}
+		// Not installed!
+		// Fallback for OSGi environments
+		return Singleton.INSTANCE;
+	}
+
+	public static BundleFileSystem newFileSystemFromExisting(Path bundle)
+			throws FileNotFoundException, IOException {
+		URI w;
+		try {
+			w = new URI(APP, bundle.toUri().toASCIIString(), null);
+		} catch (URISyntaxException e) {
+			throw new IllegalArgumentException("Can't create app: URI for "
+					+ bundle);
+		}
+
+		Map<String, Object> options = new HashMap<>();
+
+		// useTempFile not needed as we override
+		// newByteChannel to use newFileChannel() - which don't
+		// consume memory
+		// options.put("useTempFile", true);
+
+		FileSystem fs = FileSystems.newFileSystem(w, options,
+				BundleFileSystemProvider.class.getClassLoader());
+		return (BundleFileSystem) fs;
+
+		// To avoid multiple instances of this provider in an OSGi environment,
+		// the above official API calls could be replaced with:
+
+		// return getInstance().newFileSystem(w, Collections.<String, Object>
+		// emptyMap());
+
+		// which would fall back to Singleton.INSTANCE if there is no provider.
+	}
+
+	public static BundleFileSystem newFileSystemFromNew(Path bundle)
+			throws FileNotFoundException, IOException {
+		return newFileSystemFromNew(bundle,
+				APPLICATION_VND_WF4EVER_ROBUNDLE_ZIP);
+	}
+
+	public static BundleFileSystem newFileSystemFromNew(Path bundle,
+			String mimetype) throws FileNotFoundException, IOException {
+		createBundleAsZip(bundle, mimetype);
+		return newFileSystemFromExisting(bundle);
+	}
+
+	public static BundleFileSystem newFileSystemFromTemporary()
+			throws IOException {
+		Path bundle = TemporaryFiles.temporaryBundle();
+		BundleFileSystem fs = BundleFileSystemProvider.newFileSystemFromNew(
+				bundle, null);
+		return fs;
+	}
+
+	private Boolean jarDoubleEscaping;
+
+	/**
+	 * Public constructor provided for FileSystemProvider.installedProviders().
+	 * Use #getInstance() instead.
+	 * 
+	 * @deprecated
+	 */
+	@Deprecated
+	public BundleFileSystemProvider() {
+	}
+
+	private boolean asBoolean(Object object, boolean defaultValue) {
+		if (object instanceof Boolean) {
+			return (Boolean) object;
+		}
+		if (object instanceof String) {
+			return Boolean.valueOf((String) object);
+		}
+		return defaultValue;
+	}
+
+	protected URI baseURIFor(URI uri) {
+		if (!(uri.getScheme().equals(APP))) {
+			throw new IllegalArgumentException("Unsupported scheme in: " + uri);
+		}
+		if (!uri.isOpaque()) {
+			return uri.resolve("/");
+		}
+		Path localPath = localPathFor(uri);
+		Path realPath;
+		try {
+			realPath = localPath.toRealPath();
+		} catch (IOException ex) {
+			realPath = localPath.toAbsolutePath();
+		}
+		// Generate a UUID from the MD5 of the URI of the real path (!)
+		UUID uuid = UUID.nameUUIDFromBytes(realPath.toUri().toASCIIString()
+				.getBytes(UTF8));
+		try {
+			return new URI(APP, uuid.toString(), "/", null);
+		} catch (URISyntaxException e) {
+			throw new IllegalStateException("Can't create app:// URI for: "
+					+ uuid);
+		}
+	}
+
+	@Override
+	public void checkAccess(Path path, AccessMode... modes) throws IOException {
+		BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		origProvider(path).checkAccess(fs.unwrap(path), modes);
+	}
+
+	@Override
+	public void copy(Path source, Path target, CopyOption... options)
+			throws IOException {
+		BundleFileSystem fs = (BundleFileSystem) source.getFileSystem();
+		origProvider(source)
+				.copy(fs.unwrap(source), fs.unwrap(target), options);
+	}
+
+	@Override
+	public void createDirectory(Path dir, FileAttribute<?>... attrs)
+			throws IOException {
+		// Workaround http://stackoverflow.com/questions/16588321/
+		if (Files.exists(dir)) {
+			throw new FileAlreadyExistsException(dir.toString());
+		}
+		BundleFileSystem fs = (BundleFileSystem) dir.getFileSystem();
+		origProvider(dir).createDirectory(fs.unwrap(dir), attrs);
+	}
+
+	@Override
+	public void delete(Path path) throws IOException {
+		BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		origProvider(path).delete(fs.unwrap(path));
+	}
+
+	@Override
+	public boolean equals(Object obj) {
+		return getClass() == obj.getClass();
+	}
+
+	@Override
+	public <V extends FileAttributeView> V getFileAttributeView(Path path,
+			Class<V> type, LinkOption... options) {
+		BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		if (path.toAbsolutePath().equals(fs.getRootDirectory())) {
+			// Bug in ZipFS, it will fall over as there is no entry for /
+			//
+			// Instead we'll just give a view of the source (e.g. the zipfile
+			// itself).
+			// Modifying its times is a bit futile since they are likely to be
+			// overriden when closing, but this avoids a NullPointerException
+			// in Files.setTimes().
+			return Files.getFileAttributeView(fs.getSource(), type, options);
+		}
+		return origProvider(path).getFileAttributeView(fs.unwrap(path), type,
+				options);
+	}
+
+	@Override
+	public FileStore getFileStore(Path path) throws IOException {
+		BundlePath bpath = (BundlePath) path;
+		return bpath.getFileSystem().getFileStore();
+	}
+
+	@Override
+	public BundleFileSystem getFileSystem(URI uri) {
+		synchronized (openFilesystems) {
+			URI baseURI = baseURIFor(uri);
+			WeakReference<BundleFileSystem> ref = openFilesystems.get(baseURI);
+			if (ref == null) {
+				throw new FileSystemNotFoundException(uri.toString());
+			}
+			BundleFileSystem fs = ref.get();
+			if (fs == null) {
+				openFilesystems.remove(baseURI);
+				throw new FileSystemNotFoundException(uri.toString());
+			}
+			return fs;
+		}
+	}
+
+	protected boolean getJarDoubleEscaping() {
+		if (jarDoubleEscaping != null) {
+			return jarDoubleEscaping;
+		}
+		// https://bugs.openjdk.java.net/browse/JDK-8001178 introduced an
+		// inconsistent
+		// URI syntax. Before 7u40, jar: URIs to ZipFileSystemProvided had to
+		// have
+		// double-escaped the URI for the ZIP file, after 7u40 it is only
+		// escaped once.
+		// E.g.
+		// to open before 7u40 you needed
+		// jar:file:///file%2520with%2520spaces.zip, now you need
+		// jar:file:///file%20with%20spaces.zip
+		//
+		// The new format is now consistent with URL.openStream() and
+		// URLClassLoader's traditional jar: syntax, but somehow
+		// zippath.toUri() still returns the double-escaped one, which
+		// should only affects BundleFileSystem.findSource(). To help
+		// findSource()
+		// if this new bug is later fixed, we here detect which escaping style
+		// is used.
+
+		String name = "jar test";
+		try {
+			Path tmp = Files.createTempFile(name, ".zip");
+			if (!tmp.toUri().toASCIIString().contains("jar%20test")) {
+				// Hmm.. spaces not allowed in tmp? As we don't know, we'll
+				// assume Java 7 behaviour
+				jarDoubleEscaping = false;
+				return jarDoubleEscaping;
+			}
+			createBundleAsZip(tmp, null);
+			try (FileSystem fs = FileSystems.newFileSystem(tmp, null)) {
+				URI root = fs.getRootDirectories().iterator().next().toUri();
+				if (root.toASCIIString().contains("jar%2520test")) {
+					jarDoubleEscaping = true;
+				} else {
+					jarDoubleEscaping = false;
+				}
+			}
+			Files.delete(tmp);
+		} catch (IOException e) {
+			// Unknown error.. we'll assume Java 7 behaviour
+			jarDoubleEscaping = true;
+		}
+		return jarDoubleEscaping;
+
+	}
+
+	@Override
+	public Path getPath(URI uri) {
+		BundleFileSystem fs = getFileSystem(uri);
+		Path r = fs.getRootDirectory();
+		if (uri.isOpaque()) {
+			return r;
+		} else {
+			return r.resolve(uri.getPath());
+		}
+	}
+
+	@Override
+	public String getScheme() {
+		return APP;
+	}
+
+	@Override
+	public int hashCode() {
+		return getClass().hashCode();
+	}
+
+	@Override
+	public boolean isHidden(Path path) throws IOException {
+		BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		return origProvider(path).isHidden(fs.unwrap(path));
+	}
+
+	@Override
+	public boolean isSameFile(Path path, Path path2) throws IOException {
+		BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		return origProvider(path).isSameFile(fs.unwrap(path), fs.unwrap(path2));
+	}
+
+	private Path localPathFor(URI uri) {
+		URI localUri = URI.create(uri.getSchemeSpecificPart());
+		return Paths.get(localUri);
+	}
+
+	@Override
+	public void move(Path source, Path target, CopyOption... options)
+			throws IOException {
+		BundleFileSystem fs = (BundleFileSystem) source.getFileSystem();
+		origProvider(source)
+				.copy(fs.unwrap(source), fs.unwrap(target), options);
+	}
+
+	@Override
+	public SeekableByteChannel newByteChannel(Path path,
+			Set<? extends OpenOption> options, FileAttribute<?>... attrs)
+			throws IOException {
+		final BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		Path zipPath = fs.unwrap(path);
+		if (options.contains(StandardOpenOption.WRITE)
+				|| options.contains(StandardOpenOption.APPEND)) {
+
+			if (Files.isDirectory(zipPath)) {
+				// Workaround for ZIPFS allowing dir and folder to somewhat
+				// co-exist
+				throw new FileAlreadyExistsException("Directory <"
+						+ zipPath.toString() + "> exists");
+			}
+			Path parent = zipPath.getParent();
+
+			if (parent != null && !Files.isDirectory(parent)) {
+				throw new NoSuchFileException(zipPath.toString(),
+						parent.toString(), "Parent of file is not a directory");
+			}
+			if (options.contains(StandardOpenOption.CREATE_NEW)) {
+			} else if (options.contains(StandardOpenOption.CREATE)
+					&& !Files.exists(zipPath)) {
+				// Workaround for bug in ZIPFS in Java 7 -
+				// it only creates new files on
+				// StandardOpenOption.CREATE_NEW
+				//
+				// We'll fake it and just create file first using the legacy
+				// newByteChannel()
+				// - we can't inject CREATE_NEW option as it
+				// could be that there are two concurrent calls to CREATE
+				// the very same file,
+				// with CREATE_NEW the second thread would then fail.
+
+				EnumSet<StandardOpenOption> opts = EnumSet
+						.of(StandardOpenOption.WRITE,
+								StandardOpenOption.CREATE_NEW);
+				origProvider(path).newFileChannel(zipPath, opts, attrs).close();
+
+			}
+		}
+
+		// Implement by newFileChannel to avoid memory leaks and
+		// allow manifest to be updated
+		return newFileChannel(path, options, attrs);
+	}
+
+	@Override
+	public DirectoryStream<Path> newDirectoryStream(Path dir,
+			final Filter<? super Path> filter) throws IOException {
+		final BundleFileSystem fs = (BundleFileSystem) dir.getFileSystem();
+		final DirectoryStream<Path> stream = origProvider(dir)
+				.newDirectoryStream(fs.unwrap(dir), new Filter<Path>() {
+					@Override
+					public boolean accept(Path entry) throws IOException {
+						return filter.accept(fs.wrap(entry));
+					}
+				});
+		return new DirectoryStream<Path>() {
+			@Override
+			public void close() throws IOException {
+				stream.close();
+			}
+
+			@Override
+			public Iterator<Path> iterator() {
+				return fs.wrapIterator(stream.iterator());
+			}
+		};
+	}
+
+	@Override
+	public FileChannel newFileChannel(Path path,
+			Set<? extends OpenOption> options, FileAttribute<?>... attrs)
+			throws IOException {
+		final BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		FileChannel fc = origProvider(path).newFileChannel(fs.unwrap(path),
+				options, attrs);
+		return new BundleFileChannel(fc, path, options, attrs);
+	}
+
+	@Override
+	public FileSystem newFileSystem(Path path, Map<String, ?> env)
+			throws IOException {
+		URI uri;
+		try {
+			uri = new URI(APP, path.toUri().toASCIIString(), null);
+		} catch (URISyntaxException e) {
+			throw new IllegalArgumentException("Can't create app: URI for "
+					+ path);
+		}
+		return newFileSystem(uri, env);
+	}
+
+	@Override
+	public BundleFileSystem newFileSystem(URI uri, Map<String, ?> env)
+			throws IOException {
+
+		Path localPath = localPathFor(uri);
+		URI baseURI = baseURIFor(uri);
+
+		if (asBoolean(env.get("create"), false)) {
+			createBundleAsZip(localPath, (String) env.get("mimetype"));
+		}
+
+		BundleFileSystem fs;
+		synchronized (openFilesystems) {
+			WeakReference<BundleFileSystem> existingRef = openFilesystems
+					.get(baseURI);
+			if (existingRef != null) {
+				BundleFileSystem existing = existingRef.get();
+				if (existing != null && existing.isOpen()) {
+					throw new FileSystemAlreadyExistsException(
+							baseURI.toASCIIString());
+				}
+			}
+			FileSystem origFs = FileSystems.newFileSystem(localPath, null);
+			fs = new BundleFileSystem(origFs, baseURI);
+			openFilesystems.put(baseURI,
+					new WeakReference<BundleFileSystem>(fs));
+		}
+		return fs;
+	}
+
+	@Override
+	public InputStream newInputStream(Path path, OpenOption... options)
+			throws IOException {
+		// Avoid copying out to a file, like newByteChannel / newFileChannel
+		BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		return origProvider(path).newInputStream(fs.unwrap(path), options);
+	}
+
+	@Override
+	public OutputStream newOutputStream(Path path, OpenOption... options)
+			throws IOException {
+		BundleFileSystem fileSystem = (BundleFileSystem) path.getFileSystem();
+		if (fileSystem.getRootDirectory().resolve(path)
+				.equals(fileSystem.getRootDirectory().resolve(MIMETYPE_FILE))) {
+			// Special case to avoid compression
+			return origProvider(path).newOutputStream(fileSystem.unwrap(path),
+					options);
+		}
+		return super.newOutputStream(path, options);
+	}
+
+	private FileSystemProvider origProvider(Path path) {
+		return ((BundlePath) path).getFileSystem().getOrigFS().provider();
+	}
+
+	@Override
+	public <A extends BasicFileAttributes> A readAttributes(Path path,
+			Class<A> type, LinkOption... options) throws IOException {
+		BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		return origProvider(path)
+				.readAttributes(fs.unwrap(path), type, options);
+	}
+
+	@Override
+	public Map<String, Object> readAttributes(Path path, String attributes,
+			LinkOption... options) throws IOException {
+		BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		return origProvider(path).readAttributes(fs.unwrap(path), attributes,
+				options);
+	}
+
+	@Override
+	public void setAttribute(Path path, String attribute, Object value,
+			LinkOption... options) throws IOException {
+		BundleFileSystem fs = (BundleFileSystem) path.getFileSystem();
+		origProvider(path).setAttribute(fs.unwrap(path), attribute, value,
+				options);
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileTypeDetector.java
----------------------------------------------------------------------
diff --git a/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileTypeDetector.java b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileTypeDetector.java
new file mode 100644
index 0000000..03f8272
--- /dev/null
+++ b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundleFileTypeDetector.java
@@ -0,0 +1,90 @@
+package org.apache.taverna.robundle.fs;
+
+/*
+ * 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.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.ByteBuffer;
+import java.nio.channels.SeekableByteChannel;
+import java.nio.charset.Charset;
+import java.nio.file.Files;
+import java.nio.file.Path;
+import java.nio.file.StandardOpenOption;
+import java.nio.file.spi.FileTypeDetector;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipError;
+import java.util.zip.ZipException;
+import java.util.zip.ZipInputStream;
+
+public class BundleFileTypeDetector extends FileTypeDetector {
+
+	private static final String APPLICATION_ZIP = "application/zip";
+	private static final Charset ASCII = Charset.forName("ASCII");
+	private static final Charset LATIN1 = Charset.forName("ISO-8859-1");
+	private static final String MIMETYPE = "mimetype";
+
+	@Override
+	public String probeContentType(Path path) throws IOException {
+
+		ByteBuffer buf = ByteBuffer.allocate(256);
+		try (SeekableByteChannel byteChannel = Files.newByteChannel(path,
+				StandardOpenOption.READ)) {
+			int read = byteChannel.read(buf);
+			if (read < 38) {
+				return null;
+			}
+			;
+		}
+		buf.flip();
+
+		// Look for PK
+
+		byte[] firstBytes = buf.array();
+		String pk = new String(firstBytes, 0, 2, LATIN1);
+		if (!(pk.equals("PK") && firstBytes[2] == 3 && firstBytes[3] == 4)) {
+			// Did not match magic numbers of ZIP as specified in ePub OCF
+			// http://www.idpf.org/epub/30/spec/epub30-ocf.html#app-media-type
+			return null;
+		}
+
+		String mimetype = new String(firstBytes, 30, 8, LATIN1);
+		if (!mimetype.equals(MIMETYPE)) {
+			return APPLICATION_ZIP;
+		}
+		// Read the 'mimetype' file.
+		try (ZipInputStream is = new ZipInputStream(new ByteArrayInputStream(
+				firstBytes))) {
+			ZipEntry entry = is.getNextEntry();
+			if (!MIMETYPE.equals(entry.getName())) {
+				return APPLICATION_ZIP;
+			}
+			byte[] mediaTypeBuffer = new byte[256];
+			int size = is.read(mediaTypeBuffer);
+			if (size < 1) {
+				return APPLICATION_ZIP;
+			}
+			return new String(mediaTypeBuffer, 0, size, ASCII);
+		} catch (ZipException | ZipError e) {
+			return null;
+		}
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundlePath.java
----------------------------------------------------------------------
diff --git a/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundlePath.java b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundlePath.java
new file mode 100644
index 0000000..93da6c0
--- /dev/null
+++ b/taverna-robundle/src/main/java/org/apache/taverna/robundle/fs/BundlePath.java
@@ -0,0 +1,221 @@
+package org.apache.taverna.robundle.fs;
+
+/*
+ * 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.net.URI;
+import java.net.URISyntaxException;
+import java.nio.file.LinkOption;
+import java.nio.file.Path;
+import java.nio.file.WatchEvent.Kind;
+import java.nio.file.WatchEvent.Modifier;
+import java.nio.file.WatchKey;
+import java.nio.file.WatchService;
+import java.util.Iterator;
+
+public class BundlePath implements Path {
+
+	private final BundleFileSystem fs;
+
+	private final Path zipPath;
+
+	protected BundlePath(BundleFileSystem fs, Path zipPath) {
+		if (fs == null || zipPath == null) {
+			throw new NullPointerException();
+		}
+		this.fs = fs;
+		this.zipPath = zipPath;
+	}
+
+	@Override
+	public int compareTo(Path other) {
+		return zipPath.compareTo(fs.unwrap(other));
+	}
+
+	@Override
+	public boolean endsWith(Path other) {
+		return zipPath.endsWith(fs.unwrap(other));
+	}
+
+	@Override
+	public boolean endsWith(String other) {
+		return zipPath.endsWith(other);
+	}
+
+	@Override
+	public boolean equals(Object other) {
+		if (!(other instanceof BundlePath)) {
+			return false;
+		}
+		BundlePath bundlePath = (BundlePath) other;
+		return zipPath.equals(fs.unwrap(bundlePath));
+	}
+
+	@Override
+	public BundlePath getFileName() {
+		return fs.wrap(zipPath.getFileName());
+	}
+
+	@Override
+	public BundleFileSystem getFileSystem() {
+		return fs;
+	}
+
+	@Override
+	public BundlePath getName(int index) {
+		return fs.wrap(zipPath.getName(index));
+	}
+
+	@Override
+	public int getNameCount() {
+		return zipPath.getNameCount();
+	}
+
+	@Override
+	public BundlePath getParent() {
+		return fs.wrap(zipPath.getParent());
+	}
+
+	@Override
+	public BundlePath getRoot() {
+		return fs.wrap(zipPath.getRoot());
+	}
+
+	protected Path getZipPath() {
+		return zipPath;
+	}
+
+	@Override
+	public int hashCode() {
+		return zipPath.hashCode();
+	}
+
+	@Override
+	public boolean isAbsolute() {
+		return zipPath.isAbsolute();
+	}
+
+	@Override
+	public Iterator<Path> iterator() {
+		return fs.wrapIterator(zipPath.iterator());
+	}
+
+	@Override
+	public BundlePath normalize() {
+		return fs.wrap(zipPath.normalize());
+	}
+
+	@Override
+	public WatchKey register(WatchService watcher, Kind<?>... events)
+			throws IOException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public WatchKey register(WatchService watcher, Kind<?>[] events,
+			Modifier... modifiers) throws IOException {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public BundlePath relativize(Path other) {
+		return fs.wrap(zipPath.relativize(fs.unwrap(other)));
+	}
+
+	@Override
+	public BundlePath resolve(Path other) {
+		return fs.wrap(zipPath.resolve(fs.unwrap(other)));
+	}
+
+	@Override
+	public BundlePath resolve(String other) {
+		return fs.wrap(zipPath.resolve(other));
+	}
+
+	@Override
+	public BundlePath resolveSibling(Path other) {
+		return fs.wrap(zipPath.resolveSibling(fs.unwrap(other)));
+	}
+
+	@Override
+	public BundlePath resolveSibling(String other) {
+		return fs.wrap(zipPath.resolveSibling(other));
+	}
+
+	@Override
+	public boolean startsWith(Path other) {
+		return zipPath.startsWith(fs.unwrap(other));
+	}
+
+	@Override
+	public boolean startsWith(String other) {
+		return zipPath.startsWith(other);
+	}
+
+	@Override
+	public BundlePath subpath(int beginIndex, int endIndex) {
+		return fs.wrap(zipPath.subpath(beginIndex, endIndex));
+	}
+
+	@Override
+	public BundlePath toAbsolutePath() {
+		return fs.wrap(zipPath.toAbsolutePath());
+	}
+
+	@Override
+	public File toFile() {
+		throw new UnsupportedOperationException();
+	}
+
+	@Override
+	public BundlePath toRealPath(LinkOption... options) throws IOException {
+		return fs.wrap(zipPath.toRealPath(options));
+	}
+
+	/**
+	 * Note: This method is used by JSON serialization and should return a valid
+	 * relative path from .ro/ or /
+	 */
+	@Override
+	public String toString() {
+		if (zipPath.isAbsolute() && zipPath.startsWith("/.ro/")) {
+			Path base = fs.getRootDirectory().zipPath.resolve(".ro");
+			return base.relativize(zipPath).toString();
+		} else {
+			return zipPath.toString();
+		}
+	}
+
+	@Override
+	public URI toUri() {
+		Path abs = zipPath.toAbsolutePath();
+		URI pathRel;
+		try {
+			pathRel = new URI(null, null, abs.toString(), null);
+		} catch (URISyntaxException e) {
+			throw new IllegalStateException("Can't create URL for " + zipPath,
+					e);
+		}
+		return fs.getBaseURI().resolve(pathRel);
+	}
+
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/src/main/java/org/apache/taverna/robundle/manifest/Agent.java
----------------------------------------------------------------------
diff --git a/taverna-robundle/src/main/java/org/apache/taverna/robundle/manifest/Agent.java b/taverna-robundle/src/main/java/org/apache/taverna/robundle/manifest/Agent.java
new file mode 100644
index 0000000..17a7c42
--- /dev/null
+++ b/taverna-robundle/src/main/java/org/apache/taverna/robundle/manifest/Agent.java
@@ -0,0 +1,63 @@
+package org.apache.taverna.robundle.manifest;
+
+/*
+ * 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 com.fasterxml.jackson.annotation.JsonPropertyOrder;
+
+@JsonPropertyOrder(value = { "uri", "orcid", "name" })
+public class Agent {
+	private String name;
+	private URI orcid;
+	private URI uri;
+
+	public Agent() {
+	}
+
+	public Agent(String name) {
+		setName(name);
+	}
+
+	public String getName() {
+		return name;
+	}
+
+	public URI getOrcid() {
+		return orcid;
+	}
+
+	public URI getUri() {
+		return uri;
+	}
+
+	public void setName(String name) {
+		this.name = name;
+	}
+
+	public void setOrcid(URI orcid) {
+		this.orcid = orcid;
+	}
+
+	public void setUri(URI uri) {
+		this.uri = uri;
+	}
+}

http://git-wip-us.apache.org/repos/asf/incubator-taverna-language/blob/c08405cb/taverna-robundle/src/main/java/org/apache/taverna/robundle/manifest/Manifest.java
----------------------------------------------------------------------
diff --git a/taverna-robundle/src/main/java/org/apache/taverna/robundle/manifest/Manifest.java b/taverna-robundle/src/main/java/org/apache/taverna/robundle/manifest/Manifest.java
new file mode 100644
index 0000000..e8a6394
--- /dev/null
+++ b/taverna-robundle/src/main/java/org/apache/taverna/robundle/manifest/Manifest.java
@@ -0,0 +1,450 @@
+package org.apache.taverna.robundle.manifest;
+
+/*
+ * 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 com.fasterxml.jackson.databind.SerializationFeature.FAIL_ON_EMPTY_BEANS;
+import static com.fasterxml.jackson.databind.SerializationFeature.INDENT_OUTPUT;
+import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_EMPTY_JSON_ARRAYS;
+import static com.fasterxml.jackson.databind.SerializationFeature.WRITE_NULL_MAP_VALUES;
+import static java.nio.file.FileVisitResult.CONTINUE;
+import static java.nio.file.FileVisitResult.SKIP_SUBTREE;
+import static java.nio.file.Files.createDirectories;
+import static java.nio.file.Files.getLastModifiedTime;
+import static java.nio.file.Files.isDirectory;
+import static java.nio.file.Files.newBufferedWriter;
+import static java.nio.file.Files.walkFileTree;
+import static java.nio.file.StandardOpenOption.CREATE;
+import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
+import static java.nio.file.StandardOpenOption.WRITE;
+import static java.nio.file.attribute.FileTime.fromMillis;
+import static org.apache.taverna.robundle.Bundles.uriToBundlePath;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.net.URI;
+import java.nio.charset.Charset;
+import java.nio.file.FileVisitResult;
+import java.nio.file.Path;
+import java.nio.file.SimpleFileVisitor;
+import java.nio.file.attribute.BasicFileAttributes;
+import java.nio.file.attribute.FileTime;
+import java.util.ArrayList;
+import java.util.GregorianCalendar;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+import java.util.logging.Logger;
+
+import org.apache.taverna.robundle.Bundle;
+import org.apache.taverna.robundle.manifest.combine.CombineManifest;
+import org.apache.taverna.robundle.manifest.odf.ODFManifest;
+
+import com.fasterxml.jackson.annotation.JsonIgnore;
+import com.fasterxml.jackson.annotation.JsonInclude.Include;
+import com.fasterxml.jackson.annotation.JsonProperty;
+import com.fasterxml.jackson.annotation.JsonPropertyOrder;
+import com.fasterxml.jackson.annotation.JsonValue;
+import com.fasterxml.jackson.databind.ObjectMapper;
+
+@JsonPropertyOrder(value = { "@context", "id", "manifest", "createdOn",
+		"createdBy", "createdOn", "authoredOn", "authoredBy", "history",
+		"aggregates", "annotations", "@graph" })
+public class Manifest {
+	public abstract class FileTimeMixin {
+		@Override
+		@JsonValue
+		public abstract String toString();
+	}
+
+	public abstract class PathMixin {
+		@Override
+		@JsonValue
+		public abstract String toString();
+	}
+
+	private static Logger logger = Logger.getLogger(Manifest.class
+			.getCanonicalName());
+
+	private static final String MANIFEST_JSON = "manifest.json";
+
+	private static final String META_INF = "/META-INF";
+
+	private static final String MIMETYPE = "/mimetype";
+	private static final String RO = "/.ro";
+	private static URI ROOT = URI.create("/");
+
+	public static FileTime now() {
+		return fromMillis(new GregorianCalendar().getTimeInMillis());
+	}
+
+	protected static Path withSlash(Path dir) {
+		if (dir == null)
+			return null;
+		if (isDirectory(dir)) {
+			Path fname = dir.getFileName();
+			if (fname == null)
+				return dir;
+			String fnameStr = fname.toString();
+			if (fnameStr.endsWith("/"))
+				return dir;
+			return dir.resolveSibling(fnameStr + "/");
+		}
+		return dir;
+	}
+
+	private Map<URI, PathMetadata> aggregates = new LinkedHashMap<>();
+	private List<PathAnnotation> annotations = new ArrayList<>();
+	private List<Agent> authoredBy = new ArrayList<>();
+	private FileTime authoredOn;
+	private Bundle bundle;
+	private Agent createdBy = null;
+	private FileTime createdOn = now();
+	private List<String> graph;
+	private List<Path> history = new ArrayList<>();
+	private URI id = URI.create("/");
+	private List<Path> manifest = new ArrayList<>();
+
+	public Manifest(Bundle bundle) {
+		this.bundle = bundle;
+	}
+
+	public List<PathMetadata> getAggregates() {
+		return new ArrayList<>(aggregates.values());
+	}
+
+	public PathMetadata getAggregation(Path file) {
+		URI fileUri = file.toUri();
+		return getAggregation(fileUri);
+	}
+
+	public PathMetadata getAggregation(URI uri) {
+		uri = relativeToBundleRoot(uri);
+		PathMetadata metadata = aggregates.get(uri);
+		if (metadata == null) {
+			metadata = new PathMetadata();
+			if (!uri.isAbsolute() && uri.getFragment() == null) {
+				Path path = uriToBundlePath(bundle, uri);
+				metadata.setFile(path);
+				metadata.setMediatype(guessMediaType(path));
+			} else {
+				metadata.setUri(uri);
+			}
+			aggregates.put(uri, metadata);
+		}
+		return metadata;
+	}
+
+	public List<PathAnnotation> getAnnotations() {
+		return annotations;
+	}
+
+	public List<Agent> getAuthoredBy() {
+		return authoredBy;
+	}
+
+	public FileTime getAuthoredOn() {
+		return authoredOn;
+	}
+
+	@JsonIgnore
+	public URI getBaseURI() {
+		return getBundle().getRoot().toUri();
+	}
+
+	@JsonIgnore
+	public Bundle getBundle() {
+		return bundle;
+	}
+
+	@JsonProperty(value = "@context")
+	public List<Object> getContext() {
+		ArrayList<Object> context = new ArrayList<>();
+		// HashMap<Object, Object> map = new HashMap<>();
+		// map.put("@base", getBaseURI());
+		// context.add(map);
+		context.add(URI.create("https://w3id.org/bundle/context"));
+		return context;
+	}
+
+	public Agent getCreatedBy() {
+		return createdBy;
+	}
+
+	public FileTime getCreatedOn() {
+		return createdOn;
+	}
+
+	public List<String> getGraph() {
+		return graph;
+	}
+
+	public List<Path> getHistory() {
+		return history;
+	}
+
+	public URI getId() {
+		return id;
+	}
+
+	public List<Path> getManifest() {
+		return manifest;
+	}
+
+	/**
+	 * Guess media type based on extension
+	 * 
+	 * @see http://wf4ever.github.io/ro/bundle/#media-types
+	 * 
+	 * @param file
+	 *            A Path to a file
+	 * @return media-type, e.g. <code>application/xml</code> or
+	 *         <code>text/plain; charset="utf-8"</code>
+	 */
+	public String guessMediaType(Path file) {
+		if (file.getFileName() == null)
+			return null;
+		String filename = file.getFileName().toString()
+				.toLowerCase(Locale.ENGLISH);
+		if (filename.endsWith(".txt"))
+			return "text/plain; charset=\"utf-8\"";
+		if (filename.endsWith(".ttl"))
+			return "text/turtle; charset=\"utf-8\"";
+		if (filename.endsWith(".rdf") || filename.endsWith(".owl"))
+			return "application/rdf+xml";
+		if (filename.endsWith(".json"))
+			return "application/json";
+		if (filename.endsWith(".jsonld"))
+			return "application/ld+json";
+		if (filename.endsWith(".xml"))
+			return "application/xml";
+
+		// A few extra, common ones
+
+		if (filename.endsWith(".png"))
+			return "image/png";
+		if (filename.endsWith(".svg"))
+			return "image/svg+xml";
+		if (filename.endsWith(".jpg") || filename.endsWith(".jpeg"))
+			return "image/jpeg";
+		if (filename.endsWith(".pdf"))
+			return "application/pdf";
+		return "application/octet-stream";
+	}
+
+	public void populateFromBundle() throws IOException {
+		final Set<Path> potentiallyEmptyFolders = new LinkedHashSet<>();
+
+		final Set<URI> existingAggregationsToPrune = new HashSet<>(
+				aggregates.keySet());
+
+		walkFileTree(bundle.getRoot(), new SimpleFileVisitor<Path>() {
+			@SuppressWarnings("deprecation")
+			@Override
+			public FileVisitResult postVisitDirectory(Path dir, IOException exc)
+					throws IOException {
+				super.postVisitDirectory(dir, exc);
+				if (potentiallyEmptyFolders.remove(dir)) {
+					URI uri = relativeToBundleRoot(dir.toUri());
+					existingAggregationsToPrune.remove(uri);
+					PathMetadata metadata = aggregates.get(uri);
+					if (metadata == null) {
+						metadata = new PathMetadata();
+						aggregates.put(uri, metadata);
+					}
+					metadata.setFile(withSlash(dir));
+					metadata.setFolder(withSlash(dir.getParent()));
+					metadata.setProxy();
+					metadata.setCreatedOn(getLastModifiedTime(dir));
+					potentiallyEmptyFolders.remove(withSlash(dir.getParent()));
+					return CONTINUE;
+				}
+				return CONTINUE;
+			}
+
+			@Override
+			public FileVisitResult preVisitDirectory(Path dir,
+					BasicFileAttributes attrs) throws IOException {
+				if (dir.startsWith(RO) || dir.startsWith(META_INF))
+					return SKIP_SUBTREE;
+				potentiallyEmptyFolders.add(withSlash(dir));
+				potentiallyEmptyFolders.remove(withSlash(dir.getParent()));
+				return CONTINUE;
+			}
+
+			@SuppressWarnings("deprecation")
+			@Override
+			public FileVisitResult visitFile(Path file,
+					BasicFileAttributes attrs) throws IOException {
+				potentiallyEmptyFolders.remove(withSlash(file.getParent()));
+				if (file.startsWith(MIMETYPE))
+					return CONTINUE;
+				if (manifest.contains(file))
+					// Don't aggregate the manifests
+					return CONTINUE;
+
+				// super.visitFile(file, attrs);
+				URI uri = relativeToBundleRoot(file.toUri());
+				existingAggregationsToPrune.remove(uri);
+				PathMetadata metadata = aggregates.get(uri);
+				if (metadata == null) {
+					metadata = new PathMetadata();
+					aggregates.put(uri, metadata);
+				}
+				metadata.setFile(file);
+				if (metadata.getMediatype() == null)
+					// Don't override if already set
+					metadata.setMediatype(guessMediaType(file));
+				metadata.setFolder(withSlash(file.getParent()));
+				metadata.setProxy();
+				metadata.setCreatedOn(getLastModifiedTime(file));
+				potentiallyEmptyFolders.remove(file.getParent());
+				return CONTINUE;
+			}
+		});
+		for (URI preExisting : existingAggregationsToPrune) {
+			PathMetadata meta = aggregates.get(preExisting);
+			if (meta.getFile() != null)
+				/*
+				 * Don't remove 'virtual' resources, only aggregations that went
+				 * to files
+				 */
+				aggregates.remove(preExisting);
+		}
+	}
+
+	public URI relativeToBundleRoot(URI uri) {
+		uri = ROOT.resolve(bundle.getRoot().toUri().relativize(uri));
+		return uri;
+	}
+
+	@SuppressWarnings("deprecation")
+	public void setAggregates(List<PathMetadata> aggregates) {
+		this.aggregates.clear();
+
+		for (PathMetadata meta : aggregates) {
+			URI uri = null;
+			if (meta.getFile() != null) {
+				uri = relativeToBundleRoot(meta.getFile().toUri());
+			} else if (meta.getUri() != null) {
+				uri = relativeToBundleRoot(meta.getUri());
+			} else {
+				uri = relativeToBundleRoot(meta.getProxy());
+			}
+			if (uri == null) {
+				logger.warning("Unknown URI for aggregation " + meta);
+				continue;
+			}
+			this.aggregates.put(uri, meta);
+		}
+	}
+
+	public void setAnnotations(List<PathAnnotation> annotations) {
+		this.annotations = annotations;
+	}
+
+	public void setAuthoredBy(List<Agent> authoredBy) {
+		if (authoredBy == null)
+			throw new NullPointerException("authoredBy can't be null");
+		this.authoredBy = authoredBy;
+	}
+
+	public void setAuthoredOn(FileTime authoredOn) {
+		this.authoredOn = authoredOn;
+	}
+
+	public void setBundle(Bundle bundle) {
+		this.bundle = bundle;
+	}
+
+	public void setCreatedBy(Agent createdBy) {
+		this.createdBy = createdBy;
+	}
+
+	public void setCreatedOn(FileTime createdOn) {
+		this.createdOn = createdOn;
+	}
+
+	public void setGraph(List<String> graph) {
+		this.graph = graph;
+	}
+
+	public void setHistory(List<Path> history) {
+		if (history == null)
+			throw new NullPointerException("history can't be null");
+		this.history = history;
+	}
+
+	public void setId(URI id) {
+		this.id = id;
+	}
+
+	public void setManifest(List<Path> manifest) {
+		this.manifest = manifest;
+	}
+
+	public void writeAsCombineManifest() throws IOException {
+		new CombineManifest(this).createManifestXML();
+	}
+
+	/**
+	 * Write as an RO Bundle JSON-LD manifest
+	 * 
+	 * @return The path of the written manifest (e.g. ".ro/manifest.json")
+	 * @throws IOException
+	 */
+	public Path writeAsJsonLD() throws IOException {
+		Path jsonld = bundle.getFileSystem().getPath(RO, MANIFEST_JSON);
+		createDirectories(jsonld.getParent());
+		// Files.createFile(jsonld);
+		if (!getManifest().contains(jsonld))
+			getManifest().add(0, jsonld);
+		ObjectMapper om = new ObjectMapper();
+		om.addMixInAnnotations(Path.class, PathMixin.class);
+		om.addMixInAnnotations(FileTime.class, FileTimeMixin.class);
+		om.enable(INDENT_OUTPUT);
+		om.disable(WRITE_EMPTY_JSON_ARRAYS);
+		om.disable(FAIL_ON_EMPTY_BEANS);
+		om.disable(WRITE_NULL_MAP_VALUES);
+
+		om.setSerializationInclusion(Include.NON_NULL);
+		try (Writer w = newBufferedWriter(jsonld, Charset.forName("UTF-8"),
+				WRITE, TRUNCATE_EXISTING, CREATE)) {
+			om.writeValue(w, this);
+		}
+		return jsonld;
+	}
+
+	/**
+	 * Write as a ODF manifest.xml
+	 * 
+	 * @see http
+	 *      ://docs.oasis-open.org/office/v1.2/os/OpenDocument-v1.2-os-part3.
+	 *      html#__RefHeading__752807_826425813
+	 * @return The path of the written manifest (e.g. "META-INF/manifest.xml")
+	 * @throws IOException
+	 */
+	public Path writeAsODFManifest() throws IOException {
+		return new ODFManifest(this).createManifestXML();
+	}
+}