package org.smartly.commons.io.repository; import org.smartly.commons.util.StringUtils; import java.io.File; import java.io.IOException; import java.lang.ref.WeakReference; import java.net.MalformedURLException; import java.net.URL; import java.util.*; import java.util.zip.ZipEntry; import java.util.zip.ZipException; import java.util.zip.ZipFile; public final class ZipRepository extends AbstractRepository { // zip file serving sub-repositories and zip file resources private File file; // weak reference to the zip file private WeakReference<ZipFile> zipFile; // the relative path of this repository within the zip file private final String entryPath; // the nested directory depth of this repository within the zip file private int depth; private long lastModified = -1; private boolean exists; /** * Constructs a ZipRespository using the given zip file. * * @param path path to zip file * @throws java.util.zip.ZipException a zip encoding related error occurred * @throws java.io.IOException an I/O error occurred */ public ZipRepository(String path) throws ZipException, IOException { this(new File(path)); } /** * Constructs a ZipRespository using the given zip file. * * @param file zip file * @throws java.util.zip.ZipException a zip encoding related error occurred * @throws java.io.IOException an I/O error occurred */ public ZipRepository(File file) throws ZipException, IOException { // make sure our file has an absolute path, // see http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4117557 if (!file.isAbsolute()) { file = file.getAbsoluteFile(); } this.file = file; this.parent = null; name = file.getName(); path = file.getPath() + '/'; depth = 0; entryPath = ""; } /** * Constructs a ZipRepository using the zip entryName belonging to the given * zip file and top-level repository * * @param file the zip file * @param parent repository * @param entryPath the entry path name * @throws java.util.zip.ZipException a zip encoding related error occurred * @throws java.io.IOException an I/O error occurred */ private ZipRepository(File file, ZipRepository parent, String entryPath) throws ZipException, IOException { if (entryPath == null) { throw new NullPointerException("entryPath must not be null"); } else if (entryPath.equals("")) { throw new IllegalArgumentException("entryPath must not be empty"); } this.file = file; this.parent = parent; // Make sure entryPath ends with slash. This is the only way we // can be sure the zip entry isn't actually a file. this.entryPath = entryPath.endsWith("/") ? entryPath : entryPath + "/"; String[] pathArray = StringUtils.split(entryPath, SEPARATOR); depth = pathArray.length; name = pathArray[depth - 1]; path = parent.getPath() + name + '/'; } /** * Returns a java.util.zip.ZipFile for this repository. * * @return a ZipFile for reading * @throws java.io.IOException an I/O related error occurred */ protected synchronized ZipFile getZipFile() throws IOException { if (parent instanceof ZipRepository) { return ((ZipRepository) parent).getZipFile(); } ZipFile zip = zipFile == null ? null : zipFile.get(); if (zip == null || lastModified != file.lastModified()) { if (zip != null) { try { zip.close(); } catch (Exception ignore) { } } zip = new ZipFile(file); zipFile = new WeakReference<ZipFile>(zip); lastModified = file.lastModified(); } return zip; } /** * Called to create a child resource for this repository. */ @Override protected Resource lookupResource(String name) throws IOException { AbstractResource res = resources.get(name); if (res == null) { String childName = entryPath + name; ZipFile zip = getZipFile(); // getEntry() will return a directory entry without trailing slash, // but it will not return a file entry with trailing slash. // Thus, the only way to make sure an entry is not a directory is // to explicitly try to fetch the entry as directory before fetching // it as file. ZipEntry entry = zip.getEntry(childName + "/"); if (entry == null) { entry = zip.getEntry(childName); } res = new ZipResource(childName, this, entry); resources.put(name, res); } return res; } /** * Checks wether this resource actually (still) exists * * @return true if the resource exists */ public boolean exists() throws IOException { if (lastModified != file.lastModified()) { try { ZipFile zip = getZipFile(); exists = entryPath.length() == 0 ? zip != null : zip.getEntry(entryPath) != null; } catch (IOException ex) { exists = false; } } return exists; } protected AbstractRepository createChildRepository(String name) throws IOException { return new ZipRepository(file, this, entryPath + name); } protected void getResources(List<Resource> list, boolean recursive) throws IOException { Map<String, ZipEntry> entries = getChildEntries(); for (Map.Entry<String, ZipEntry> entry : entries.entrySet()) { String entryName = entry.getKey(); if (!entry.getValue().isDirectory()) { AbstractResource res = resources.get(entryName); if (res == null) { ZipEntry zipEntry = entry.getValue(); res = new ZipResource(zipEntry.getName(), this, zipEntry); resources.put(entryName, res); } list.add(res); } else if (recursive) { lookupRepository(entryName).getResources(list, true); } } } public Repository[] getRepositories() throws IOException { List<Repository> list = new ArrayList<Repository>(); Map<String, ZipEntry> entries = getChildEntries(); for (Map.Entry<String, ZipEntry> entry : entries.entrySet()) { if (entry.getValue().isDirectory()) { list.add(lookupRepository(entry.getKey())); } } return list.toArray(new Repository[list.size()]); } public URL getUrl() throws MalformedURLException { // return a Jar URL as described in // http://java.sun.com/j2se/1.5.0/docs/api/java/net/JarURLConnection.html if (parent instanceof ZipRepository) { return new URL(parent.getUrl() + name + "/"); } else { String baseUrl = "jar:file:" + file + "!/"; return entryPath.length() == 0 ? new URL(baseUrl) : new URL(baseUrl + entryPath); } } public long lastModified() { return file.lastModified(); } public long getChecksum() { return file.lastModified(); } @Override public int hashCode() { return 17 + (37 * file.hashCode()) + (37 * path.hashCode()); } @Override public boolean equals(Object obj) { if (!(obj instanceof ZipRepository)) { return false; } ZipRepository rep = (ZipRepository) obj; return (file.equals(rep.file) && path.equals(rep.path)); } @Override public String toString() { return "ZipRepository[" + path + "]"; } private Map<String, ZipEntry> getChildEntries() throws IOException { ZipFile zipfile = getZipFile(); Map<String, ZipEntry> map = new TreeMap<String, ZipEntry>(); Enumeration en = zipfile.entries(); while (en.hasMoreElements()) { ZipEntry entry = (ZipEntry) en.nextElement(); String entryName = entry.getName(); if (!entryName.regionMatches(0, entryPath, 0, entryPath.length())) { // names don't match - not a child of ours continue; } String[] entrypath = StringUtils.split(entryName, SEPARATOR); if (depth > 0 && !name.equals(entrypath[depth - 1])) { // catch case where our name is Foo and other's is FooBar continue; } if (entrypath.length == depth + 1) { map.put(entrypath[depth], entry); } } return map; } }