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();
+ }
+}