org.apache.taverna.robundle.Bundles.java Source code

Java tutorial

Introduction

Here is the source code for org.apache.taverna.robundle.Bundles.java

Source

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