blob: cbb161dd71ff1e1152c57f7b6ed8e89167ba45c3 [file] [log] [blame]
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);
}
}