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