/*
* This file is part of the Eclipse Virgo project.
*
* Copyright (c) 2012 VMware Inc.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* VMware Inc. - initial contribution
*/
package org.eclipse.virgo.kernel.artifact.fs.internal;
import java.io.BufferedInputStream;
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.io.OutputStream;
import java.util.HashSet;
import java.util.Set;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
import org.eclipse.virgo.kernel.artifact.fs.ArtifactFS;
import org.eclipse.virgo.kernel.artifact.fs.ArtifactFSEntry;
/**
* {@link JarFileArtifactFSEntry} is an {@link ArtifactFSEntry} implementation for JAR file entries.
* <p/>
* The implementation uses ZipInputStream specifically to avoid JarFile's caching behaviour, inherited from that of
* ZipFile. See the note on caching in http://java.sun.com/developer/technicalArticles/Programming/compression/
* JarFile's caching behaviour is unsuitable as it produces incorrect results when a JAR file is replaced with a new
* version since the cache returns entries from the old version.
* <p/>
* The implementation handles missing directory entries by simulating them. Although this does not faithfully reflect
* the structure of a JAR with a missing directory entry, it is more robust for callers who may not expect, or test
* with, such JARs.
*
* <strong>Concurrent Semantics</strong><br />
*
* Thread safe
*/
final class JarFileArtifactFSEntry implements ArtifactFSEntry {
private final File file;
private final String entryName;
/**
* Constructs a new {@link JarFileArtifactFSEntry} for the given file which is assumed to be in JAR format and the
* given entry name.
*
* @param file a JAR file
* @param entryName the name of an entry
* @throws IOException if the entry cannot be created
*/
JarFileArtifactFSEntry(File file, String entryName) {
this.file = file;
this.entryName = entryName;
}
/**
* {@inheritDoc}
*/
public String getPath() {
return this.entryName;
}
/**
* {@inheritDoc}
*/
public String getName() {
String filePath = removeTrailingSlash();
int lastDir = filePath.lastIndexOf("/");
return filePath.substring(lastDir + 1);
}
private String removeTrailingSlash() {
return this.entryName.endsWith("/") ? this.entryName.substring(0, this.entryName.length() - 1) : this.entryName;
}
/**
* {@inheritDoc}
*/
public boolean delete() {
throw new UnsupportedOperationException("This ArtifactFSEntry is a member of a JAR file. Deleting it is unsupported");
}
/**
* {@inheritDoc}
*/
public boolean isDirectory() {
ZipEntry zipEntry = findZipEntry();
if (zipEntry != null) {
return zipEntry.isDirectory();
} else {
return hasChildren();
}
}
private ZipEntry findZipEntry() {
JarFileScanner scanner = new JarFileScanner();
try {
ZipEntry entry = scanner.getNextEntry();
while (entry != null) {
if (this.entryName.equals(entry.getName())) {
return entry;
}
entry = scanner.getNextEntry();
}
return null;
} finally {
scanner.close();
}
}
// This method copes with non-existent entries.
private boolean hasChildren() {
boolean hasChildren = false;
if (this.entryName.endsWith("/")) {
JarFileScanner scanner = new JarFileScanner();
try {
ZipEntry entry = scanner.getNextEntry();
while (entry != null) {
String name = entry.getName();
if (name != null && name.startsWith(this.entryName)) {
hasChildren = true;
break;
}
entry = scanner.getNextEntry();
}
} finally {
scanner.close();
}
}
return hasChildren;
}
/**
* {@inheritDoc}
*/
@SuppressWarnings("resource")
public InputStream getInputStream() {
JarFileScanner scanner = new JarFileScanner();
ZipEntry entry = scanner.getNextEntry();
while (entry != null && !this.entryName.equals(entry.getName())) {
entry = scanner.getNextEntry();
}
if (entry == null) {
scanner.close();
throw new UnsupportedOperationException("Cannot open an input stream for a non-existent entry");
}
if (entry.isDirectory()) {
scanner.close();
throw new UnsupportedOperationException("Cannot open an input stream for a directory");
}
return scanner.getZipInputStream();
}
/**
* {@inheritDoc}
*/
public OutputStream getOutputStream() {
throw new UnsupportedOperationException("This ArtifactFSEntry is a member of a JAR file. Writing it is unsupported");
}
/**
* {@inheritDoc}
*/
public ArtifactFSEntry[] getChildren() {
if (!isDirectory()) {
throw new UnsupportedOperationException("Cannot get children of a non-directory entry");
}
Set<ArtifactFSEntry> children = new HashSet<ArtifactFSEntry>();
if (exists()) {
JarFileScanner scanner = new JarFileScanner();
try {
ZipEntry entry = scanner.getNextEntry();
while (entry != null) {
String childEntry = entry.getName();
if (childEntry.length() > this.entryName.length() && childEntry.startsWith(this.entryName)) {
children.add(createChildEntry(childEntry));
// Ensure missing parents of this child are added.
addParentDirectories(childEntry, children);
}
entry = scanner.getNextEntry();
}
} finally {
scanner.close();
}
}
return children.toArray(new ArtifactFSEntry[children.size()]);
}
public JarFileArtifactFSEntry createChildEntry(String childEntryName) {
return new JarFileArtifactFSEntry(this.file, childEntryName);
}
// Precondition: childEntry.length() > this.entryName.length() && childEntry.startsWith(this.entryName)
private void addParentDirectories(String childEntry, Set<ArtifactFSEntry> children) {
int l = this.entryName.length();
String childPath = childEntry.substring(l);
String[] childPathComponents = childPath.split("/");
String parentPath = this.entryName;
for (int parent = 0; parent < childPathComponents.length - 1; parent++) {
parentPath = parentPath + childPathComponents[parent]+ "/";
children.add(createChildEntry(parentPath));
}
}
/**
* {@inheritDoc}
*/
public ArtifactFS getArtifactFS() {
throw new UnsupportedOperationException("getArtifactFS method not supported by JarFileArtifactFSEntry");
}
/**
* {@inheritDoc}
*/
public boolean exists() {
if (findZipEntry() != null) {
return true;
} else {
return hasChildren();
}
}
private class JarFileScanner implements Closeable {
private final ZipInputStream zipInputStream;
public JarFileScanner() {
InputStream is = null;
try {
is = new BufferedInputStream(new FileInputStream(file));
} catch (FileNotFoundException ignored) {
}
this.zipInputStream = is == null ? null : new ZipInputStream(is);
}
public ZipEntry getNextEntry() {
if (this.zipInputStream != null) {
try {
return this.zipInputStream.getNextEntry();
} catch (IOException ignored) {
}
}
return null;
}
public ZipInputStream getZipInputStream() {
return this.zipInputStream;
}
public void close() {
if (this.zipInputStream != null) {
try {
this.zipInputStream.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
}
/**
* {@inheritDoc}
*/
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((entryName == null) ? 0 : entryName.hashCode());
result = prime * result + ((file == null) ? 0 : file.hashCode());
return result;
}
/**
* {@inheritDoc}
*/
@Override
public boolean equals(Object obj) {
if (this == obj)
return true;
if (obj == null)
return false;
if (!(obj instanceof JarFileArtifactFSEntry))
return false;
JarFileArtifactFSEntry other = (JarFileArtifactFSEntry) obj;
if (entryName == null) {
if (other.entryName != null)
return false;
} else if (!entryName.equals(other.entryName))
return false;
if (file == null) {
if (other.file != null)
return false;
} else if (!file.equals(other.file))
return false;
return true;
}
}