/*
* JBoss, Home of Professional Open Source
* Copyright 2009, JBoss Inc., and individual contributors as indicated
* by the @authors tag. See the copyright.txt in the distribution for a
* full listing of individual contributors.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.teiid.query.metadata;
import java.io.Closeable;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.security.CodeSigner;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import org.jboss.vfs.VFS;
import org.jboss.vfs.VFSUtils;
import org.jboss.vfs.VirtualFile;
import org.jboss.vfs.spi.FileSystem;
import org.jboss.vfs.util.PathTokenizer;
import org.teiid.common.buffer.AutoCleanupUtil;
import org.teiid.common.buffer.AutoCleanupUtil.Removable;
/**
* {@inheritDoc}
* <p/>
* This implementation is backed by a zip file. The provided file must be owned by this instance; otherwise, if the
* file disappears unexpectedly, the filesystem will malfunction.
*
* @author <a href="mailto:david.lloyd@redhat.com">David M. Lloyd</a>
* @author <a href="mailto:ales.justin@jboss.org">Ales Justin</a>
*/
public final class PureZipFileSystem implements FileSystem {
private static AtomicInteger counter = new AtomicInteger();
public static VirtualFile mount(URL url) throws IOException, URISyntaxException {
//we treat each zip as unique if it's possible that it
String fileName = "teiid-vdb-" + url.toExternalForm(); //$NON-NLS-1$
VirtualFile root = VFS.getChild(fileName);
File f = new File(url.toURI());
if (root.exists()) {
long lastModified = f.lastModified();
if (root.getLastModified() != lastModified) {
fileName += counter.get();
//TODO: should check existence
root = VFS.getChild(fileName);
}
}
synchronized (PureZipFileSystem.class) {
if (!root.exists()) {
final Closeable c = VFS.mount(root, new PureZipFileSystem(f));
//in theory we don't need to track the closable as we're not using any resources
AutoCleanupUtil.setCleanupReference(root, new Removable() {
@Override
public void remove() {
try {
c.close();
} catch (IOException e) {
}
}
});
}
}
return root;
}
private final JarFile zipFile;
private final File archiveFile;
private final long zipTime;
private final ZipNode rootNode;
/**
* Create a new instance.
*
* @param archiveFile the original archive file
*
* @throws java.io.IOException if an I/O error occurs
*/
public PureZipFileSystem(File archiveFile) throws IOException {
zipTime = archiveFile.lastModified();
final JarFile zipFile;
this.zipFile = zipFile = new JarFile(archiveFile);
this.archiveFile = archiveFile;
final Enumeration<? extends JarEntry> entries = zipFile.entries();
final ZipNode rootNode = new ZipNode(new HashMap<String, ZipNode>(), "", null);
FILES:
for (JarEntry entry : iter(entries)) {
final String name = entry.getName();
final boolean isDirectory = entry.isDirectory();
final List<String> tokens = PathTokenizer.getTokens(name);
ZipNode node = rootNode;
final Iterator<String> it = tokens.iterator();
while (it.hasNext()) {
String token = it.next();
if (PathTokenizer.isCurrentToken(token) || PathTokenizer.isReverseToken(token)) {
// invalid file name
continue FILES;
}
final Map<String, ZipNode> children = node.children;
if (children == null) {
// todo - log bad zip entry
continue FILES;
}
ZipNode child = children.get(token);
if (child == null) {
child = it.hasNext() || isDirectory ? new ZipNode(new HashMap<String, ZipNode>(), token, null) : new ZipNode(null, token, entry);
children.put(token, child);
}
node = child;
}
}
this.rootNode = rootNode;
}
/** {@inheritDoc} */
private static <T> Iterable<T> iter(final Enumeration<T> entries) {
return Collections.list(entries);
}
/** {@inheritDoc} */
public File getFile(VirtualFile mountPoint, VirtualFile target) throws IOException {
final ZipNode zipNode = getExistingZipNode(mountPoint, target);
final JarEntry zipEntry = zipNode.entry;
try {
return new File(new URI("jar", archiveFile.toURI().toString() + "!/", zipEntry.getName()));
} catch (URISyntaxException e) {
throw new IOException(e);
}
}
/** {@inheritDoc} */
public InputStream openInputStream(VirtualFile mountPoint, VirtualFile target) throws IOException {
final ZipNode zipNode = getExistingZipNode(mountPoint, target);
if (rootNode == zipNode) {
return new FileInputStream(archiveFile);
}
final JarEntry entry = zipNode.entry;
if (entry == null) {
throw new IOException("Not a file: \"" + target.getPathName() + "\"");
}
return zipFile.getInputStream(entry);
}
/** {@inheritDoc} */
public boolean delete(VirtualFile mountPoint, VirtualFile target) {
return false;
}
/** {@inheritDoc} */
public long getSize(VirtualFile mountPoint, VirtualFile target) {
final ZipNode zipNode = getZipNode(mountPoint, target);
if (zipNode == null) {
return 0L;
}
final JarEntry entry = zipNode.entry;
if (zipNode == rootNode) {
return archiveFile.length();
}
return entry == null ? 0L : entry.getSize();
}
/** {@inheritDoc} */
public long getLastModified(VirtualFile mountPoint, VirtualFile target) {
final ZipNode zipNode = getZipNode(mountPoint, target);
if (zipNode == null) {
return 0L;
}
final JarEntry entry = zipNode.entry;
return entry == null ? zipTime : entry.getTime();
}
/** {@inheritDoc} */
public boolean exists(VirtualFile mountPoint, VirtualFile target) {
final ZipNode zipNode = rootNode.find(mountPoint, target);
return zipNode != null;
}
/** {@inheritDoc} */
public boolean isFile(final VirtualFile mountPoint, final VirtualFile target) {
final ZipNode zipNode = rootNode.find(mountPoint, target);
return zipNode != null && zipNode.entry != null;
}
/** {@inheritDoc} */
public boolean isDirectory(VirtualFile mountPoint, VirtualFile target) {
final ZipNode zipNode = rootNode.find(mountPoint, target);
return zipNode != null && zipNode.entry == null;
}
/** {@inheritDoc} */
public List<String> getDirectoryEntries(VirtualFile mountPoint, VirtualFile target) {
final ZipNode zipNode = getZipNode(mountPoint, target);
if (zipNode == null) {
return Collections.emptyList();
}
final Map<String, ZipNode> children = zipNode.children;
if (children == null) {
return Collections.emptyList();
}
final Collection<ZipNode> values = children.values();
final List<String> names = new ArrayList<String>(values.size());
for (ZipNode node : values) {
names.add(node.name);
}
return names;
}
/**
* {@inheritDoc}
*/
public CodeSigner[] getCodeSigners(VirtualFile mountPoint, VirtualFile target) {
final ZipNode zipNode = getZipNode(mountPoint, target);
if (zipNode == null) {
return null;
}
JarEntry jarEntry = zipNode.entry;
return jarEntry.getCodeSigners();
}
private ZipNode getZipNode(VirtualFile mountPoint, VirtualFile target) {
return rootNode.find(mountPoint, target);
}
private ZipNode getExistingZipNode(VirtualFile mountPoint, VirtualFile target)
throws FileNotFoundException {
final ZipNode zipNode = rootNode.find(mountPoint, target);
if (zipNode == null) {
throw new FileNotFoundException(target.getPathName());
}
return zipNode;
}
/** {@inheritDoc} */
public boolean isReadOnly() {
return true;
}
/** {@inheritDoc} */
public File getMountSource() {
return archiveFile;
}
public URI getRootURI() throws URISyntaxException {
return new URI("jar", archiveFile.toURI().toString() + "!/", null);
}
/** {@inheritDoc} */
public void close() throws IOException {
VFSUtils.safeClose(new Closeable() {
public void close() throws IOException {
zipFile.close();
}
});
}
private File buildFile(File contentsDir, String name) {
List<String> tokens = PathTokenizer.getTokens(name);
File currentFile = contentsDir;
for(String token : tokens) {
currentFile = new File(currentFile, token);
}
currentFile.getParentFile().mkdirs();
return currentFile;
}
private static final class ZipNode {
// immutable child map
private final Map<String, ZipNode> children;
private final String name;
private final JarEntry entry;
private ZipNode(Map<String, ZipNode> children, String name, JarEntry entry) {
this.children = children;
this.name = name;
this.entry = entry;
}
private ZipNode find(VirtualFile mountPoint, VirtualFile target) {
if (mountPoint.equals(target)) {
return this;
} else {
final ZipNode parent = find(mountPoint, target.getParent());
if (parent == null) {
return null;
}
final Map<String, ZipNode> children = parent.children;
if (children == null) {
return null;
}
return children.get(target.getName());
}
}
}
}