/*
* Sun Public License Notice
*
* The contents of this file are subject to the Sun Public License
* Version 1.0 (the "License"). You may not use this file except in
* compliance with the License. A copy of the License is available at
* http://www.sun.com/
*
* The Original Code is NetBeans. The Initial Developer of the Original
* Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun
* Microsystems, Inc. All Rights ReFserved.
*/
package org.openide.filesystems;
import java.beans.*;
import java.io.*;
import java.util.*;
import java.util.zip.*;
import java.util.jar.*;
import java.lang.ref.*;
import org.openide.util.enums.*;
import org.openide.util.*;
import org.openide.util.enums.*;
/** A virtual filesystem based on a JAR archive.
* <p>For historical reasons many AbstractFileSystem.* methods are implemented
* as protected in this class. Do not call them! Subclasses might override
* them, or (better) use delegation.
* @author Jan Jancura, Jaroslav Tulach, Petr Hamernik, Radek Matous
*/
public class JarFileSystem extends AbstractFileSystem {
/** generated Serialized Version UID */
static final long serialVersionUID = -98124752801761145L;
/**
* Opened zip file of this filesystem is stored here or null.
*/
private transient JarFile jar;
/** Manifest file for jar
*/
private transient Manifest manifest;
/** Archive file.1
*/
private File root = new File ("."); // NOI18N
/** One request proccesor shared for all instances of JarFileSystem*/
private static RequestProcessor req = new RequestProcessor ("JarFs - modification watcher"); // NOI18N
/** Watches modification on root file */
private transient RequestProcessor.Task watcherTask = null;
private transient long lastModification = 0;
/*Should help to prevent closing JarFile if anybody has InputStream. Also this variable
is used as object for synchronization: synchronized(closeSync)*/
private transient Object closeSync = new Object () ;
/** Controlls the LocalFileSystem's automatic refresh.
* If the refresh time interval is set from the System.property, than this value is used.
* Otherwise, the refresh time interval is set to 0, which means the refresh is disabled. */
private static final int REFRESH_TIME = Integer.getInteger
("org.openide.filesystems.JarFileSystem.REFRESH_TIME",0).intValue(); // NOI18N
private int checkTime = REFRESH_TIME;
/** number of FileObjects in using. If no one is used then the cached data
* is freed */
private transient long aliveCount = 0;
/** Cached image of JarFile capable of answering queries on type and children.
* There is a strong reference held while there is a living FileObject
* and a SoftReference for caching after all FOs are freed.*/
private transient Cache strongCache;
/** The soft part of the cache reference. For simplicity never null*/
private transient Reference softCache = new SoftReference(null);
/**
* Default constructor.
*/
public JarFileSystem () {
Impl impl = new Impl (this);
this.list = impl;
this.info = impl;
this.change = impl;
this.attr = impl;
}
/**
* Constructor that can provide own capability for the filesystem.
* @param cap the capability
*/
public JarFileSystem (FileSystemCapability cap) {
this ();
setCapability (cap);
}
/* Creates Reference. In FileSystem, which subclasses AbstractFileSystem, you can overload method
* createReference(FileObject fo) to achieve another type of Reference (weak, strong etc.)
* @param fo is FileObject. It`s reference yourequire to get.
* @return Reference to FileObject
*/
protected Reference createReference(FileObject fo) {
aliveCount++;
if (checkTime > 0 && watcherTask == null)
watcherTask = req.post (watcherTask (), checkTime);
return new Ref(fo);
}
/** Use soft-references to not throw away the data that quickly.
* JarFS if often queried for its FOs e.g. by java parser, which
* leaves the references immediatelly.
*/
private class Ref extends WeakReference implements Runnable {
public Ref(FileObject fo) {
super(fo, Utilities.activeReferenceQueue ());
}
// do the cleanup
public void run() {
freeReference();
}
}
private void freeReference() {
aliveCount--;
// Nobody uses this JarFileSystem => stop watcher, close JarFile and throw away cache.
if (aliveCount == 0) {
if (watcherTask != null) {
watcherTask.cancel();
watcherTask = null;
}
strongCache = null; // no more active FO, keep only soft ref
closeCurrentRoot();
}
}
/** Get the JAR manifest.
* It will be lazily initialized.
* @return parsed manifest file for this archive
*/
public Manifest getManifest() {
if (manifest == null) {
try {
synchronized (closeSync) {
JarFile j = reOpenJarFile ();
manifest = j == null ? null : j.getManifest ();
}
} catch (IOException ex) {
}
if (manifest == null)
manifest = new Manifest ();
}
return manifest;
}
/**
* Set name of the ZIP/JAR file.
* @param aRoot path to new ZIP or JAR file
* @throws IOException if the file is not valid
*/
public void setJarFile (File aRoot) throws IOException, PropertyVetoException {
FileObject newRoot = null;
String oldDisplayName = getDisplayName ();
if (getRefreshTime() > 0) setRefreshTime(0);
if (aRoot == null)
FSException.io ("EXC_NotValidFile", aRoot); // NOI18N
if (!aRoot.exists ())
FSException.io ("EXC_FileNotExists", aRoot.getAbsolutePath ()); // NOI18N
if (!aRoot.canRead ())
FSException.io ("EXC_CanntRead", aRoot.getAbsolutePath ()); // NOI18N
if (!aRoot.isFile ())
FSException.io ("EXC_NotValidFile", aRoot.getAbsolutePath ()); // NOI18N
if (aRoot.equals(root))
return;
String s;
s = aRoot.getCanonicalPath ();
s = s.intern();
JarFile tempJar = null;
try {
tempJar = new JarFile (s);
} catch (ZipException e) {
FSException.io ("EXC_NotValidJarFile",s); // NOI18N
}
synchronized (closeSync) {
try {
setSystemName (s);
} catch (PropertyVetoException vex) {
throw vex;
}
closeCurrentRoot ();
jar = tempJar;
root = new File (s);
strongCache = null;
softCache.clear();
aliveCount = 0;
newRoot = refreshRoot ();
manifest = null;
lastModification = 0;
}
firePropertyChange ("root", null, newRoot); // NOI18N
firePropertyChange (PROP_DISPLAY_NAME, oldDisplayName, getDisplayName ());
}
/** Get the file path for the ZIP or JAR file.
* @return the file path
*/
public File getJarFile () { // JST
return root;
}
/*
* Provides name of the system that can be presented to the user.
* @return user presentable name of the filesystem
*/
public String getDisplayName () {
if (root != null && root.isFile () && root.exists () && root.canRead ())
return root.getName();
return getString ("JAR_NotValidJarFileSystem");
}
/** This filesystem is read-only.
* @return <code>true</code>
*/
public boolean isReadOnly () {
return true;
}
/* Closes associated JAR file on cleanup, if possible. */
public void removeNotify () {
closeCurrentRoot ();
}
/* initialization of jar variable, that is necessary after JarFileSystem was removed from Repository */
// public void addNotify () {
// super.addNotify ();
// }
/** Prepare environment for external compilation or execution.
* <P>
* Adds name of the ZIP/JAR file, if it has been set, to the class path.
*/
public void prepareEnvironment (Environment env) {
if (root != null) {
env.addClassPath (root.getAbsolutePath ());
}
}
//
// List
//
protected String[] children (String name) {
Cache cache = getCache();
if (cache != null) return cache.getChildrenOf(name);
return new String[0]; // fallback for wrong jar
}
//
// Change
//
protected void createFolder (String name) throws java.io.IOException {
throw new IOException ();
}
protected void createData (String name) throws IOException {
throw new IOException ();
}
protected void rename(String oldName, String newName) throws IOException {
throw new IOException ();
}
protected void delete (String name) throws IOException {
throw new IOException ();
}
//
// Info
//
protected java.util.Date lastModified(String name) {
/** JarEntry.getTime returns wrong value: already reported in bugtraq 4319781
* Fixed in jdk1.4 */
return new java.util.Date (getEntry (name).getTime ());
}
protected boolean folder (String name) {
if ("".equals (name)) return true; // NOI18N
Cache cache = getCache();
if (cache != null) return cache.isFolder(name);
return false;
}
protected boolean readOnly (String name) {
return true;
}
protected String mimeType (String name) {
return null;
}
protected long size (String name) {
long retVal = getEntry (name).getSize ();
return (retVal == -1) ? 0 : retVal;
}
protected InputStream inputStream (String name) throws java.io.FileNotFoundException {
InputStream is = null;
AbstractFolder fo = null;
try {
synchronized (closeSync) {
JarFile j = reOpenJarFile ();
if (j != null) {
JarEntry je = j.getJarEntry (name);
if (je != null) {
// JDK 1.3 contains bug #4336753
//is = j.getInputStream (je);
is = getInputStream4336753(j, je);
if (is != null) {
//EntryRef eref = (EntryRef)findReference(name);
Reference eref = findReference(name);
if (eref != null) fo = (AbstractFolder) eref.get ();
if (fo != null) is = StreamPool.createInputStream(fo, is);
}
}
}
}
} catch (java.io.FileNotFoundException e) {
throw e;
} catch (IOException e) {
throw new java.io.FileNotFoundException (e.getMessage ());
} catch (RuntimeException e) {
throw new java.io.FileNotFoundException (e.getMessage ());
}
if (is == null)
throw new java.io.FileNotFoundException (name);
return is;
}
// 4336753 workaround
private InputStream getInputStream4336753(JarFile j, JarEntry je) throws IOException {
InputStream in = null;
while (in == null) {
try {
in = j.getInputStream (je);
break;
} catch (NullPointerException ex) {
// ignore, it occured during reseting reused Inflanter
// try again until there will be no Inflanter to reuse
}
}
return in;
}
protected OutputStream outputStream (String name) throws java.io.IOException {
throw new IOException ();
}
protected void lock (String name) throws IOException {
FSException.io ("EXC_CannotLock", name, getDisplayName (), name); // NOI18N
}
protected void unlock (String name) {
}
protected void markUnimportant (String name) {
}
protected Object readAttribute(String name, String attrName) {
Attributes attr = getManifest ().getAttributes (name);
try {
return attr == null ? null : attr.getValue (attrName);
} catch (IllegalArgumentException iax) {
return null;
}
}
protected void writeAttribute(String name, String attrName, Object value) throws IOException {
throw new IOException ();
}
protected Enumeration attributes(String name) {
Attributes attr = getManifest ().getAttributes (name);
if (attr != null) {
return new AlterEnumeration(Collections.enumeration (attr.keySet ())) {
public Object alter(Object obj) {
return obj.toString();
}
};
} else {
return EmptyEnumeration.EMPTY;
}
}
protected void renameAttributes (String oldName, String newName) {
}
protected void deleteAttributes (String name) {
}
/** Close the jar file when we go away...*/
protected void finalize () throws Throwable {
super.finalize();
closeCurrentRoot ();
}
/** Initializes the root of FS.
*/
private void readObject (ObjectInputStream ois)
throws IOException, ClassNotFoundException {
ois.defaultReadObject ();
closeSync = new Object();
strongCache = null;
softCache = new SoftReference(null);
aliveCount = 0;
try {
setJarFile (root);
} catch (PropertyVetoException ex) {
throw new IOException (ex.getMessage ());
} catch (IOException iex) {
ExternalUtil.log (iex.getLocalizedMessage());
}
}
/** Performs a clean-up
* After close of JarFile must be always reference to JarFile set to null
*/
private void closeCurrentRoot() {
synchronized (closeSync) {
if (jar != null) {
try {
// Prevents to close JarFile if anybody uses InputStream (finalizer will close it anyway)
StreamPool pool = StreamPool.find(this);
if (pool == null || (!pool.isInputStreamOpen() && !pool.isOutputStreamOpen()))
jar.close();
/*else { // uncomment for debug purposes
// exception is fired
IOException e = new IOException();
pool.annotate(e);
ExternalUtil.exception(e);
}*/
} catch (Exception exc) {
// ignore exception during closing, just log it
ExternalUtil.exception(exc);
} finally {
jar = null;
}
}
}
}
private Cache getCache() {
Cache ret = strongCache;
if (ret == null) ret = (Cache)softCache.get();
if (ret == null) ret = parse(false);
return ret;
}
/** refreshes children recursively.*/
private void refreshExistingFileObjects () {
Cache cache = getCache();
String[] empty = new String[0];
Enumeration en = existingFileObjects (getRoot ());
while (en.hasMoreElements()) {
AbstractFolder fo = (AbstractFolder)en.nextElement();
if (fo.isFolder() && !fo.isInitialized()) continue;
String[] children = cache.getChildrenOf(fo.getPath());
if (children == null) children = empty;
fo.refresh(null,null,true,true,children);
}
}
/**parses entries of JarFile into EntryCache hierarchical structure and sets
* lastModified to actual value.
*/
private Cache parse (boolean refresh) {
// force watcher to reschedule us if not succesfull
JarFile j = null;
long start;
beginAtomicAction ();
try {
synchronized (closeSync) {
start = System.currentTimeMillis();
lastModification = 0;
closeCurrentRoot();
for (int i = 0; i <= 2; i++) {
try {
j = reOpenJarFile ();
break;
}
catch (IOException ex) {
if (i >= 2) return null;
continue;
}
}
try {
Enumeration en = j.entries();
Cache newCache = new Cache(en);
lastModification = root.lastModified();
strongCache = newCache;
softCache = new SoftReference(newCache);
return newCache;
} catch(Throwable t) {
// jar is invalid; perhaps it's being rebuilt
// don't touch filesystem
return null;
}
}
} finally {
if (refresh)
refreshExistingFileObjects();
if (checkTime > 0 && watcherTask == null)
watcherTask = req.post (watcherTask (), checkTime);
finishAtomicAction ();
}
}
/* Anonymous Runnable class - responsible for checking whether JarFile was modified => standalone thread.
* If JarFile was modified, parsing is invoked.
*/
private Runnable watcherTask() {
return new Runnable() {
public void run() {
try {
if (root == null) return;
/** JarFile was modified => parse it and refresh existing FileObjects*/
if (root.lastModified() != lastModification) {
parse(true);
}
} finally {
/** reschedule watcherTask*/
if (watcherTask != null) {
watcherTask.schedule(checkTime);
}
}
}
};
}
/** Getter for entry.
*/
private final JarEntry getEntry (String file) {
JarFile j = null;
try {
synchronized (closeSync) {
j = reOpenJarFile ();
JarEntry je = j.getJarEntry (file);
if (je != null) return je;
}
} catch (IOException iox) {
}
return new JarEntry (file);
}
/** Must be called from synchronized block*/
private JarFile reOpenJarFile () throws IOException {
JarFile j = jar;
if (j != null) return j;
synchronized (closeSync) {
if (jar == null && root != null) {
jar = new JarFile(root);
}
return jar;
}
}
/** Implementation of all interfaces List, Change, Info and Attr
* that delegates to JarFileSystem
*/
public static class Impl extends Object
implements AbstractFileSystem.List, AbstractFileSystem.Info,
AbstractFileSystem.Change, AbstractFileSystem.Attr {
/** generated Serialized Version UID */
static final long serialVersionUID = -67233308132567232L;
/** the pointer to filesystem */
private JarFileSystem fs;
/** Constructor.
* @param fs the filesystem to delegate to
*/
public Impl (JarFileSystem fs) {
this.fs = fs;
}
/*
*
* Scans children for given name
*/
public String[] children (String name) {
return fs.children (name);
}
//
// Change
//
/*
* Creates new folder named name.
* @param name name of folder
* @throws IOException if operation fails
*/
public void createFolder (String name) throws java.io.IOException {
fs.createFolder (name);
}
/*
* Create new data file.
*
* @param name name of the file
*
* @return the new data file object
* @exception IOException if the file cannot be created (e.g. already exists)
*/
public void createData (String name) throws IOException {
fs.createData (name);
}
/*
* Renames a file.
*
* @param oldName old name of the file
* @param newName new name of the file
*/
public void rename(String oldName, String newName) throws IOException {
fs.rename (oldName, newName);
}
/*
* Delete the file.
*
* @param name name of file
* @exception IOException if the file could not be deleted
*/
public void delete (String name) throws IOException {
fs.delete (name);
}
//
// Info
//
/*
*
* Get last modification time.
* @param name the file to test
* @return the date
*/
public java.util.Date lastModified(String name) {
return fs.lastModified (name);
}
/*
* Test if the file is folder or contains data.
* @param name name of the file
* @return true if the file is folder, false otherwise
*/
public boolean folder (String name) {
return fs.folder (name);
}
/*
* Test whether this file can be written to or not.
* @param name the file to test
* @return <CODE>true</CODE> if file is read-only
*/
public boolean readOnly (String name) {
return fs.readOnly (name);
}
/*
* Get the MIME type of the file.
* Uses {@link FileUtil#getMIMEType}.
*
* @param name the file to test
* @return the MIME type textual representation, e.g. <code>"text/plain"</code>
*/
public String mimeType (String name) {
return fs.mimeType (name);
}
/*
* Get the size of the file.
*
* @param name the file to test
* @return the size of the file in bytes or zero if the file does not contain data (does not
* exist or is a folder).
*/
public long size (String name) {
return fs.size (name);
}
/*
* Get input stream.
*
* @param name the file to test
* @return an input stream to read the contents of this file
* @exception FileNotFoundException if the file does not exists or is invalid
*/
public InputStream inputStream (String name) throws java.io.FileNotFoundException {
return fs.inputStream (name);
}
/*
* Get output stream.
*
* @param name the file to test
* @return output stream to overwrite the contents of this file
* @exception IOException if an error occures (the file is invalid, etc.)
*/
public OutputStream outputStream (String name) throws java.io.IOException {
return fs.outputStream (name);
}
/*
* Does nothing to lock the file.
*
* @param name name of the file
*/
public void lock (String name) throws IOException {
fs.lock (name);
}
/*
* Does nothing to unlock the file.
*
* @param name name of the file
*/
public void unlock (String name) {
fs.unlock (name);
}
/*
* Does nothing to mark the file as unimportant.
*
* @param name the file to mark
*/
public void markUnimportant (String name) {
fs.markUnimportant (name);
}
/*
* Get the file attribute with the specified name.
* @param name the file
* @param attrName name of the attribute
* @return appropriate (serializable) value or <CODE>null</CODE> if the attribute is unset (or could not be properly restored for some reason)
*/
public Object readAttribute(String name, String attrName) {
return fs.readAttribute (name, attrName);
}
/*
* Set the file attribute with the specified name.
* @param name the file
* @param attrName name of the attribute
* @param value new value or <code>null</code> to clear the attribute. Must be serializable, although particular filesystems may or may not use serialization to store attribute values.
* @exception IOException if the attribute cannot be set. If serialization is used to store it, this may in fact be a subclass such as {@link NotSerializableException}.
*/
public void writeAttribute(String name, String attrName, Object value) throws IOException {
fs.writeAttribute (name, attrName, value);
}
/*
* Get all file attribute names for the file.
* @param name the file
* @return enumeration of keys (as strings)
*/
public Enumeration attributes(String name) {
return fs.attributes (name);
}
/*
* Called when a file is renamed, to appropriatelly update its attributes.
* <p>
* @param oldName old name of the file
* @param newName new name of the file
*/
public void renameAttributes (String oldName, String newName) {
fs.renameAttributes (oldName, newName);
}
/*
* Called when a file is deleted to also delete its attributes.
*
* @param name name of the file
*/
public void deleteAttributes (String name) {
fs.deleteAttributes (name);
}
}
private static class Cache {
byte[] names = new byte[1000];
private int nameOffset = 0;
int[] EMPTY = new int[0];
private HashMap folders = new HashMap();
public Cache(Enumeration en) {
parse(en);
trunc();
}
public boolean isFolder(String name) {
return folders.get(name) != null;
}
public String[] getChildrenOf(String folder) {
Folder fol = (Folder)folders.get(folder);
if (fol != null) return fol.getNames();
return null;
}
private void parse(Enumeration en) {
folders.put("", new Folder()); // root folder
while (en.hasMoreElements()) {
JarEntry je = (JarEntry)en.nextElement();
String name = je.getName();
boolean isFolder = false;
// work only with slashes
name = name.replace('\\', '/');
if (name.startsWith("/")) name = name.substring(1); // NOI18N
if (name.endsWith("/")) {
name = name.substring(0, name.length() - 1); // NOI18N
isFolder = true;
}
int lastSlash = name.lastIndexOf('/');
String dirName = ""; // root
String realName = name;
if (lastSlash > 0) {
dirName = name.substring(0, lastSlash); // or folder
realName = name.substring(lastSlash+1);
}
if (isFolder) {
getFolder(name); // will create the folder item
} else {
Folder fl = getFolder(dirName);
fl.addChild(realName);
}
}
}
private Folder getFolder(String name) {
Folder fl = (Folder)folders.get(name);
if (fl == null) {
// add all the superfolders on the way to the root
int lastSlash = name.lastIndexOf('/');
String dirName = ""; // root
String realName = name;
if (lastSlash > 0) {
dirName = name.substring(0, lastSlash); // or folder
realName = name.substring(lastSlash+1);
}
getFolder(dirName).addChild(realName);
fl = new Folder();
folders.put(name, fl);
}
return fl;
}
private void trunc() {
// strip the name array:
byte[] newNames = new byte[nameOffset];
System.arraycopy(names, 0, newNames, 0, nameOffset);
names = newNames;
// strip all the indices arrays:
for (Iterator it = folders.values().iterator(); it.hasNext(); ) {
((Folder)it.next()).trunc();
}
}
private int putName(byte[] name) {
int start = nameOffset;
if (start+name.length > names.length) {
byte[] newNames = new byte[names.length*2+name.length];
System.arraycopy(names, 0, newNames, 0, start);
names = newNames;
}
System.arraycopy(name, 0, names, start, name.length);
nameOffset += name.length;
return start;
}
private class Folder {
private int[] indices = EMPTY;
private int idx = 0;
public Folder() {}
public String[] getNames() {
String[] ret = new String[idx/2];
for(int i=0; i<ret.length; i++) {
byte[] name = new byte[indices[2*i+1]];
System.arraycopy(names, indices[2*i], name, 0, name.length);
try {
ret[i] = new String(name, "UTF-8");
} catch (UnsupportedEncodingException e) {
throw new InternalError("No UTF-8");
}
}
return ret;
}
void addChild(String name) {
// ensure enough space
if (idx+2 > indices.length) {
int[] newInd = new int[2*indices.length+2];
System.arraycopy(indices, 0, newInd, 0, idx);
indices = newInd;
}
try {
byte[] bytes = name.getBytes("UTF-8");
indices[idx++] = putName(bytes);
indices[idx++] = bytes.length;
} catch (UnsupportedEncodingException e) {
throw new InternalError("No UTF-8");
}
}
void trunc() {
if (indices.length > idx) {
int[] newInd= new int[idx];
System.arraycopy(indices, 0, newInd, 0, idx);
indices = newInd;
}
}
}
}
}