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