/************************************************************** * * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information * regarding copyright ownership. The ASF licenses this file * to you under the Apache License, Version 2.0 (the * "License"); you may not use this file except in compliance * with the License. You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY * KIND, either express or implied. See the License for the * specific language governing permissions and limitations * under the License. * *************************************************************/ package org.openoffice.netbeans.modules.office.filesystem; import java.beans.*; import java.io.*; import java.util.*; import java.util.zip.*; import org.openide.ErrorManager; import org.openide.filesystems.*; import org.openide.filesystems.FileSystem; // override java.io.FileSystem import org.openide.util.NbBundle; // ISSUES: // - This FS saves (updates) the file on 'setDocument' or 'removeNotify'. // It has to let the user to decide to update or not. // // TODOS: // - 'Update' action on the mounted document which saves all recent modifications. // - To introduce 'scope' editable property to control editable portion of // the mounted document. // - Acceptable document type identification before mount. /** * OpenOffice.org Document filesystem. * * @author misha */ public class OpenOfficeDocFileSystem extends AbstractFileSystem { public static final String SCRIPTS_ROOT = "Scripts"; // must be a folder public static final String SEPARATOR = "/"; // zip file separator private static final int OS_UNKNOWN = 0; private static final int OS_UNIX = 1; private static final int OS_MACOS = 2; private static final int OS_WINDOWS = 3; private static final int REFRESH_OFF = -1; // -1 is desabled private static final int REFRESH_TIME = REFRESH_OFF; // (mS) private static final String TMP_FILE_PREF = "sx_"; private static final String TMP_FILE_SUFX = ".sxx"; private transient Map cache; // filesystem cache private transient File docFile; // OpenOffice document private transient ZipFile zipFile; private static transient int osType; // type of OS private transient ChildrenStrategy childrenStrategy; private transient EditableStrategy editableStrategy; private transient boolean isModified; // true if an entry has been removed /** * Static constructor. */ static { // Identify the type of OS String osname = System.getProperty("os.name"); if (osname.startsWith("Mac OS")) osType = OS_MACOS; else if (osname.startsWith("Windows")) osType = OS_WINDOWS; else osType = OS_UNIX; } /** * Default constructor. Initializes new OpenOffice filesystem. */ public OpenOfficeDocFileSystem() { // Create the filesystem cache cache = new HashMap(); // Initialize strategies editableStrategy = new EditableStrategy(SCRIPTS_ROOT); childrenStrategy = new ChildrenStrategy(); // Create and use implementations of filesystem functionality: info = new InfoImpl(); change = new ChangeImpl(); // Handle filesystem.attributes files normally: DefaultAttributes defattr = new DefaultAttributes( info, change, new ListImpl()); // Handle filesystem.attributes files normally + adds virtual attribute // "java.io.File" that is used in conversion routines FileUtil.toFile and // FileUtil.fromFile //defattr = new InnerAttrs(this, info, change, new ListImpl()); // (Otherwise set attr to a special implementation, and use ListImpl for list.) attr = defattr; list = defattr; // transfer = new TransferImpl(); setRefreshTime(REFRESH_OFF); } /** * Constructor. Initializes new OpenOffice filesystem with FS capability. */ public OpenOfficeDocFileSystem(FileSystemCapability cap) { this(); setCapability(cap); } /** * Provides unique signature of an instance of the filesystem. * NOTE: The scope is not a part of the signature so it is impossible * to mount the same archive more then once. */ public static String computeSystemName(File file) { return OpenOfficeDocFileSystem.class.getName() + "[" + file + "]"; } // ----------- PROPERTIES -------------- /** * Provides the 'human readable' name of the instance of the filesystem. */ public String getDisplayName() { if (!isValid()) return NbBundle.getMessage(OpenOfficeDocFileSystem.class, "LAB_invalid_file_system", ((docFile != null)? docFile.toString(): "")); else return NbBundle.getMessage(OpenOfficeDocFileSystem.class, "LAB_valid_file_system", docFile.toString()); } /** * Retrieves the 'document' property. */ public File getDocument() { return docFile; } /** * Sets the 'document' property. */ // Bean setter. Changing the OpenOffice document (or in general, the identity // of the root file object) should cause everything using this filesystem // to refresh. The system name must change and refreshRoot should be used // to ensure that everything is correctly updated. public synchronized void setDocument(File file) throws java.beans.PropertyVetoException, java.io.IOException { System.out.println("OpenOfficeDocFileSystem.setDocument: file=\"" + file.toString() + "\""); if((file.exists() == false) || (file.isFile() == false)) { IOException ioe = new IOException( file.toString() + " does not exist"); ErrorManager.getDefault().annotate(ioe, NbBundle.getMessage( OpenOfficeDocFileSystem.class, "EXC_root_dir_does_not_exist", file.toString())); throw ioe; } // update the document try { updateDocument(); } catch(IOException ioe) { // cannot save all!!! System.out.println("*** OpenOfficeDocFileSystem.setDocument:"); System.out.println(" file: " + ((docFile != null)? docFile.toString(): "")); System.out.println(" exception: " + ioe.getMessage()); } // new document type verification!!! closeDocument(); // open a new document try { openDocument(file); firePropertyChange(PROP_ROOT, null, refreshRoot()); setRefreshTime(REFRESH_TIME); } catch(IOException ioe) { // cannot open a new document!!! System.out.println("*** OpenOfficeDocFileSystem.setDocument:"); System.out.println(" file: " + ((file != null)? file.toString(): "")); System.out.println(" exception: " + ioe.getMessage()); } } /** * Retrieves 'readonly' property. * NOTE: The portion of the mounted document available to the user is * always editable. */ public boolean isReadOnly() { return false; } /** * Sets 'readonly' property. * NOTE: The portion of the mounted document available to the user is * always editable. */ public void setReadOnly(boolean flag) { // sorry! it is not supported. } // ----------- SPECIAL CAPABILITIES -------------- /** * Participates in the environment configuration. * This is how you can affect the classpath for execution, compilation, etc. */ public void prepareEnvironment(FileSystem.Environment environment) { // BUG: the compiller cannot access files within the OpenOffice document. //environment.addClassPath(docFile.toString()); } /* ----------------------------------------------------------- * Affect the name and icon of files on this filesystem according to their * "status", e.g. version-control modification-commit state: /* private class StatusImpl implements Status { public Image annotateIcon(Image icon, int iconType, Set files) { // You may first modify it, e.g. by adding a check mark to the icon // if that makes sense for this file or group of files. return icon; } public String annotateName(String name, Set files) { // E.g. add some sort of suffix to the name if some of the // files are modified but not backed up or committed somehow: if (theseFilesAreModified(files)) return NbBundle.getMessage(OpenOfficeDocFileSystem.class, "LBL_modified_files", name); else return name; } } private transient Status status; public Status getStatus() { if (status == null) { status = new StatusImpl(); } return status; } // And use fireFileStatusChanged whenever you know something has changed. */ /* // Filesystem-specific actions, such as version-control operations. // The actions should typically be CookieActions looking for DataObject // cookies, where the object's primary file is on this type of filesystem. public SystemAction[] getActions() { // ------>>>> UPDATE OPENOFFICE DOCUMENT <<<<------ return new SystemAction[] { SystemAction.get(SomeAction.class), null, // separator SystemAction.get(SomeOtherAction.class) }; } */ /** * Notifies this filesystem that it has been removed from the repository. * Concrete filesystem implementations could perform clean-up here. * The default implementation does nothing. *

Note that this method is advisory and serves as an optimization * to avoid retaining resources for too long etc. Filesystems should maintain correct * semantics regardless of whether and when this method is called. */ public void removeNotify() { setRefreshTime(REFRESH_OFF); // disable refresh // update the document try { updateDocument(); } catch(IOException ioe) { // cannot save all!!! System.out.println("*** OpenOfficeDocFileSystem.removeNotify:"); System.out.println(" exception: " + ioe.getMessage()); } closeDocument(); super.removeNotify(); } /* * Opens (mounts) an OpenOffice document. */ private void openDocument(File file) throws IOException, PropertyVetoException { synchronized(cache) { setSystemName(computeSystemName(file)); docFile = file; zipFile = new ZipFile(docFile); cacheDocument(zipFile.entries(), editableStrategy); isModified = false; } // synchronized } /* * Closes the document and cleans up the cache. */ private void closeDocument() { synchronized(cache) { // if a document mounted - close it if(docFile != null) { // close the document archive if(zipFile != null) { try { zipFile.close(); } catch(IOException ioe) { // sorry! we can do nothing about it. } } zipFile = null; // clean up cache scanDocument(new CleanStrategy()); docFile = null; isModified = false; } } // synchronized } /* * Creates a document cache. */ private void cacheDocument(Enumeration entries, Strategy editables) { Entry cacheEntry; ZipEntry archEntry; synchronized(cache) { cache.clear(); // root folder cacheEntry = new ReadWriteEntry(null); cache.put(cacheEntry.getName(), cacheEntry); // the rest of items while(entries.hasMoreElements()) { archEntry = (ZipEntry)entries.nextElement(); cacheEntry = new Entry(archEntry); if(editables.evaluate(cacheEntry)) cacheEntry = new ReadWriteEntry(archEntry); cache.put(cacheEntry.getName(), cacheEntry); } } // synchronized } /* * Updates the document. */ private void updateDocument() throws IOException { if(docFile == null) return; synchronized(cache) { ModifiedStrategy modifiedStrategy = new ModifiedStrategy(); scanDocument(modifiedStrategy); if((isModified == true) || (modifiedStrategy.isModified() == true)) { File tmpFile = null; try { // create updated document tmpFile = File.createTempFile( TMP_FILE_PREF, TMP_FILE_SUFX, docFile.getParentFile()); saveDocument(tmpFile); } catch(IOException ioe) { if(tmpFile != null) tmpFile.delete(); throw ioe; } // close the document archive if(zipFile != null) { try { zipFile.close(); } catch(IOException ioe) { } } zipFile = null; // create the document and backup File newFile = new File(docFile.getParentFile() + File.separator + "~" + docFile.getName()); if(newFile.exists()) newFile.delete(); // delete old backup docFile.renameTo(newFile); tmpFile.renameTo(docFile); // open the document archive zipFile = new ZipFile(docFile); } isModified = false; } // synchronized } /* * Saves the document in a new archive. */ private void saveDocument(File file) throws IOException { synchronized(cache) { SaveStrategy saver = new SaveStrategy(file); scanDocument(saver); saver.close(); } // synchronized } /* * Provides each individual entry in the cached document to an apraiser. */ private void scanDocument(Strategy strategy) { synchronized(cache) { Iterator itr = cache.values().iterator(); while(itr.hasNext()) { strategy.evaluate((Entry)itr.next()); } } // synchronized } /* * Retrieves or creates a file. */ private Entry getFileEntry(String name) throws IOException { Entry cEntry = null; synchronized(cache) { cEntry = (Entry)cache.get(name); if(cEntry == null) { // create a new file ZipEntry zEntry = new ZipEntry(name); zEntry.setTime(new Date().getTime()); cEntry = new Entry(zEntry); if(editableStrategy.evaluate(cEntry) == false) { throw new IOException( "cannot create/edit readonly file"); // I18N } cEntry = new ReadWriteEntry(zEntry); cache.put(cEntry.getName(), cEntry); isModified = true; } } // synchronized return cEntry; } /* * Retrieves or creates a folder. */ private Entry getFolderEntry(String name) throws IOException { Entry cEntry = null; synchronized(cache) { cEntry = (Entry)cache.get(name); if(cEntry == null) { // create a new folder ZipEntry zEntry = new ZipEntry(name + SEPARATOR); zEntry.setMethod(ZipEntry.STORED); zEntry.setSize(0); CRC32 crc = new CRC32(); zEntry.setCrc(crc.getValue()); zEntry.setTime(new Date().getTime()); cEntry = new Entry(zEntry); if(editableStrategy.evaluate(cEntry) == false) { throw new IOException( "cannot create folder"); // I18N } cEntry = new ReadWriteEntry(zEntry); cEntry.getOutputStream(); // sets up modified flag cache.put(cEntry.getName(), cEntry); isModified = true; } else { if(cEntry.isFolder() == false) cEntry = null; } } // synchronized return cEntry; } /* * Converts the name to ZIP file name. * Removes the leading file separator if there is one. * This is WORKAROUND of the BUG in AbstractFileObject: * While AbstractFileObject reprecents the root of the filesystem it uses * the absolute path (the path starts with '/'). It is inconsistent with * the rest of the code. * WORKAROUND: we have to strip leading '/' if it is in the name. */ private static String zipName(String name) { String zname = ((name.startsWith(File.separator))? name.substring(File.separator.length()): name); switch(osType) { case OS_MACOS: zname = zname.replace(':', '/'); // ':' by '/' break; case OS_WINDOWS: zname = zname.replace((char)0x5c, '/'); // '\' by '/' break; default: break; } return zname; } // ----------- IMPLEMENTATIONS OF ABSTRACT FUNCTIONALITY ---------- /* ----------------------------------------------------------- * Information about files and operations on the contents which do * not affect the file's presence or name. */ private class InfoImpl implements Info { public boolean folder(String name) { synchronized(cache) { String zname = zipName(name); Entry entry = (Entry)cache.get(zname); if(entry != null) return entry.isFolder(); // logical zip file entry childrenStrategy.setParent(zname); scanDocument(childrenStrategy); return (childrenStrategy.countChildren() > 0); } } public Date lastModified(String name) { synchronized(cache) { Entry entry = (Entry)cache.get(zipName(name)); return new Date((entry != null)? entry.getTime(): 0L); } } public boolean readOnly(String name) { synchronized(cache) { Entry entry = (Entry)cache.get(zipName(name)); return (entry != null)? entry.isReadOnly(): false; } } public String mimeType(String name) { // Unless you have some special means of determining MIME type // (e.g. HTTP headers), ask IDE to use its normal heuristics: // the MIME resolver pool and then file extensions, or if nothing // matches, just content/unknown. return null; } public long size(String name) { synchronized(cache) { Entry entry = (Entry)cache.get(zipName(name)); return (entry != null)? entry.getSize(): 0; } // synchronized } public InputStream inputStream(String name) throws FileNotFoundException { synchronized(cache) { Entry entry = (Entry)cache.get(zipName(name)); return (entry != null)? entry.getInputStream(): null; } // synchronized } public OutputStream outputStream(String name) throws IOException { return getFileEntry(zipName(name)).getOutputStream(); } // AbstractFileSystem handles locking the file to the rest of the IDE. // This only means that you should define how the file should be locked // to the outside world--perhaps it does not need to be. public void lock(String name) throws IOException { /* File file = getFile(name); if (file.exists() == true && file.canWrite() == false) { IOException ioe = new IOException("file " + file + " could not be locked"); ErrorManager.getDefault().annotate(ioe, NbBundle.getMessage( OpenOfficeDocFileSystem.class, "EXC_file_could_not_be_locked", file.getName(), getDisplayName(), file.getPath())); throw ioe; } */ } public void unlock(String name) { // Nothing special needed to unlock a file to the outside world. } public void markUnimportant(String name) { // Do nothing special. Version-control systems may use this to mark // certain files (e.g. *.class) as not needing to be stored in the VCS // while others (source files) are by default important. } } /* ----------------------------------------------------------- * Operations that change the available files. */ private class ChangeImpl implements Change { public void createFolder(String name) throws IOException { synchronized(cache) { String zname = zipName(name); if(cache.get(zname) != null) { throw new IOException( "cannot create new folder: " + name); // I18N } getFolderEntry(zname); } // synchronized } public void createData(String name) throws IOException { synchronized(cache) { String zname = zipName(name); if(cache.get(zname) != null) { throw new IOException( "cannot create new data: " + name); // I18N } OutputStream os = getFileEntry(zname).getOutputStream(); os.close(); } // synchronized } public void rename(String oldName, String newName) throws IOException { String oname = zipName(oldName); String nname = zipName(newName); if((oname.length() == 0) || (nname.length() == 0)) { throw new IOException( "cannot move or rename the root folder"); // I18N } synchronized(cache) { if(cache.get(nname) != null) { throw new IOException( "target file/folder " + newName + " exists"); // I18N } Entry entry = (Entry)cache.get(oname); if(entry == null) { throw new IOException( "there is no such a file/folder " + oldName); // I18N } if(entry.isReadOnly() == true) { throw new IOException( "file/folder " + oldName + " is readonly"); // I18N } entry.rename(nname); if(editableStrategy.evaluate(entry) == false) { entry.rename(oname); throw new IOException( "cannot create file/folder"); // I18N } cache.remove(oname); cache.put(entry.getName(), entry); } // synchronized } public void delete(String name) throws IOException { String zname = zipName(name); if(zname.length() == 0) { throw new IOException( "cannot delete the root folder"); // I18N } synchronized(cache) { Entry entry = (Entry)cache.remove(zname); if(entry != null) { // BUG: this is the design bug. Cache has to // remember that the entry was removed. isModified = true; entry.clean(); } } // synchronized } } /* ----------------------------------------------------------- * Operation which provides the directory structure. */ private class ListImpl implements List { public String[] children(String name) { String[] children = null; synchronized(cache) { String zname = zipName(name); Entry entry = (Entry)cache.get(zname); if(entry != null) { // real zip file entry if(entry.isFolder()) { childrenStrategy.setParent(entry.getName()); } } else { // logical zip file entry // (portion of the path of a real zip file entry) childrenStrategy.setParent(zname); } scanDocument(childrenStrategy); children = childrenStrategy.getChildren(); } // synchronize return children; } } /** ----------------------------------------------------------- * This class adds new virtual attribute "java.io.File". * Because of the fact that FileObjects of __Sample__FileSystem are convertible * to java.io.File by means of attributes. */ /*private static class InnerAttrs extends DefaultAttributes { //static final long serialVersionUID = 1257351369229921993L; __Sample__FileSystem sfs; public InnerAttrs(__Sample__FileSystem sfs, AbstractFileSystem.Info info, AbstractFileSystem.Change change,AbstractFileSystem.List list ) { super(info, change, list); this.sfs = sfs; } public Object readAttribute(String name, String attrName) { if (attrName.equals("java.io.File")) // NOI18N return sfs.getFile(name); return super.readAttribute(name, attrName); } }*/ /* ----------------------------------------------------------- // Optional special implementations of copy and (cross-directory) move. private class TransferImpl implements Transfer { public boolean copy(String name, Transfer target, String targetName) throws IOException { // Only permit special implementation within single FS // (or you could implement it across filesystems if you wished): if (target != this) return false; // Specially copy the file in an efficient way, e.g. implement // a copy-on-write algorithm. return true; } public boolean move(String name, Transfer target, String targetName) throws IOException { // Only permit special implementation within single FS // (or you could implement it across filesystems if you wished): if (target != this) return false; // Specially move the file, e.g. retain rename information even // across directories in a version-control system. return true; } } */ /* ----------------------------------------------------------- * This interface hides an action will be performed on an entry. */ private interface Strategy { public boolean evaluate(Entry entry); } /* ----------------------------------------------------------- * Recognizes editable (read-write) entires */ private class EditableStrategy implements Strategy { private String scope; public EditableStrategy(String scope) { this.scope = scope; } public boolean evaluate(Entry entry) { // recognizes all entries in a subtree of the // 'scope' as editable entries return (entry != null)? entry.getName().startsWith(scope): false; } } /* ----------------------------------------------------------- * Recognizes and accumulates immediate children of the parent. */ private class ChildrenStrategy implements Strategy { private String parent; private Collection children = new HashSet(); public ChildrenStrategy() { } public void setParent(String name) { parent = (name.length() > 0)? (name + SEPARATOR): ""; if(children == null) children = (java.util.List)new LinkedList(); children.clear(); } public boolean evaluate(Entry entry) { // do not accept "children" of a file // ignore "read only" part of the filesystem if(entry.isReadOnly() == false) { // identify a child if( (entry.getName().length() > 0) && (entry.getName().startsWith(parent))) { // identify an immediate child String child = entry.getName(); if(parent.length() > 0) { child = entry.getName().substring(parent.length()); } int idx = child.indexOf(SEPARATOR); if(idx > 0) // more path elements ahead child = child.substring(0, idx); return children.add(child); } } return false; } public int countChildren() { return children.size(); } public String[] getChildren() { String[] chn = new String[children.size()]; Iterator itr = children.iterator(); int idx = 0; while(itr.hasNext()) { chn[idx++] = (String)itr.next(); } return chn; } } /* ----------------------------------------------------------- * Recognizes cache entries which have to be save into new archive. */ private class ModifiedStrategy implements Strategy { private boolean modified; public boolean evaluate(Entry entry) { modified |= entry.isModified(); return entry.isModified(); } public boolean isModified() { return modified; } } /* ----------------------------------------------------------- * Saves each entry in the filesystem cache. */ private class SaveStrategy implements Strategy { ZipOutputStream docos; IOException ioexp; public SaveStrategy(File newdoc) throws IOException { docos = new ZipOutputStream(new FileOutputStream(newdoc)); ioexp = null; // success by default } public boolean evaluate(Entry entry) { if(entry.getName().length() == 0) return false; try { entry.save(docos); } catch(IOException ioe) { if(ioexp == null) ioexp = ioe; } return true; } public void close() throws IOException { if(docos != null) { try { docos.close(); } catch (IOException ioe) { ioexp = ioe; } finally { docos = null; } if(ioexp != null) { throw ioexp; } } } } /* ----------------------------------------------------------- * Cleans each entiry in the filesystem cache. */ private class CleanStrategy implements Strategy { public boolean evaluate(Entry entry) { try { entry.clean(); } catch(java.lang.Exception exp) { // sorry! can do nothing about it. } return true; } } /* ----------------------------------------------------------- * ReadOnly cache entry */ private class Entry { private String name; private boolean folder; private long size; private long time; private File node; // data files only public Entry(ZipEntry entry) { if(entry != null) { name = entry.getName(); folder = entry.isDirectory(); size = entry.getSize(); time = entry.getTime(); // removes tail file separator from a folder name if((folder == true) && (name.endsWith(SEPARATOR))) { name = name.substring( 0, name.length() - SEPARATOR.length()); } } else { // 'null' is special cace of root folder name = ""; folder = true; size = 0; time = -1; } } public boolean isReadOnly() { return true; } public boolean isFolder() { return folder; } public boolean isModified() { return false; } public String getName() { return name; } public long getSize() { return size; } public long getTime() { // ajust last modified time to the java.io.File return (time >= 0)? time: 0; } public InputStream getInputStream() throws FileNotFoundException { return (isFolder() == false)? new FileInputStream(getFile()): null; } public OutputStream getOutputStream() throws IOException { return null; } public void rename(String name) throws IOException { // throw new IOException( // "cannot rename readonly file: " + getName()); // I18N // BUG: this is the design bug. Cache has to mamage such kind // of operation in order to keep the data integrity. this.name = name; } public void save(ZipOutputStream arch) throws IOException { InputStream is = null; ZipEntry entry = new ZipEntry( getName() + ((isFolder())? SEPARATOR: "")); try { if(isFolder()) { // folder entry.setMethod(ZipEntry.STORED); entry.setSize(0); CRC32 crc = new CRC32(); entry.setCrc(crc.getValue()); entry.setTime(getTime()); arch.putNextEntry(entry); } else { // file if(isModified() == false) entry.setTime(getTime()); else entry.setTime(node.lastModified()); arch.putNextEntry(entry); is = getInputStream(); FileUtil.copy(is, arch); } } finally { // close streams if(is != null) { try { is.close(); } catch(java.io.IOException ioe) { // sorry! can do nothing about it. } } if(arch != null) arch.closeEntry(); } } public void clean() throws IOException { if(node != null) node.delete(); } public String toString() { return ( ((isReadOnly())? "RO ": "RW ") + ((isFolder())? "D": "F") + " \"" + getName() + "\""); } /* package */ File getFile() throws FileNotFoundException { if(node == null) { try { node = File.createTempFile(TMP_FILE_PREF, TMP_FILE_SUFX); // copy the file from archive to the cache OutputStream nos = null; InputStream zis = null; try { ZipEntry entry = zipFile.getEntry(getName()); if(entry != null) { // copy existing file to the cache zis = zipFile.getInputStream(entry); nos = new FileOutputStream(node); FileUtil.copy(zis, nos); } } finally { // close streams if(nos != null) { try { nos.close(); } catch(java.io.IOException ioe) { } } if(zis != null) { try { zis.close(); } catch(java.io.IOException ioe) { } } } } catch(java.lang.Exception exp) { // delete cache file if(node != null) node.delete(); node = null; throw new FileNotFoundException( "cannot access file: " + getName()); // I18N } } return node; } } /* ----------------------------------------------------------- * ReadWrite cache entry */ private class ReadWriteEntry extends Entry { private boolean modified; // 'null' is special cace of root folder public ReadWriteEntry(ZipEntry entry) { super(entry); } public boolean isReadOnly() { return false; } public boolean isModified() { return modified; } public void rename(String name) throws IOException { modified = true; super.rename(name); } public OutputStream getOutputStream() throws IOException { modified = true; return (isFolder() == false)? new FileOutputStream(getFile()): null; } } }