| package org.purl.wf4ever.robundle.fs; |
| |
| 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.purl.wf4ever.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; |
| } |
| |
| public void force(boolean metaData) throws IOException { |
| fc.force(metaData); |
| } |
| |
| @Override |
| protected void implCloseChannel() throws IOException { |
| fc.close(); |
| // TODO: Update manifest |
| } |
| |
| public FileLock lock(long position, long size, boolean shared) |
| throws IOException { |
| return fc.lock(position, size, shared); |
| } |
| |
| public MappedByteBuffer map(MapMode mode, long position, long size) |
| throws IOException { |
| return fc.map(mode, position, size); |
| } |
| |
| public long position() throws IOException { |
| return fc.position(); |
| } |
| |
| public FileChannel position(long newPosition) throws IOException { |
| return fc.position(newPosition); |
| } |
| |
| public int read(ByteBuffer dst) throws IOException { |
| return fc.read(dst); |
| } |
| |
| public int read(ByteBuffer dst, long position) throws IOException { |
| return fc.read(dst, position); |
| } |
| |
| public long read(ByteBuffer[] dsts, int offset, int length) |
| throws IOException { |
| return fc.read(dsts, offset, length); |
| } |
| |
| public long size() throws IOException { |
| return fc.size(); |
| } |
| |
| public long transferFrom(ReadableByteChannel src, long position, |
| long count) throws IOException { |
| return fc.transferFrom(src, position, count); |
| } |
| |
| public long transferTo(long position, long count, |
| WritableByteChannel target) throws IOException { |
| return fc.transferTo(position, count, target); |
| } |
| |
| public FileChannel truncate(long size) throws IOException { |
| return fc.truncate(size); |
| } |
| |
| public FileLock tryLock(long position, long size, boolean shared) |
| throws IOException { |
| return fc.tryLock(position, size, shared); |
| } |
| |
| public int write(ByteBuffer src) throws IOException { |
| return fc.write(src); |
| } |
| |
| public int write(ByteBuffer src, long position) throws IOException { |
| return fc.write(src, position); |
| } |
| |
| 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); |
| } |
| |
| } |