1 /**************************************************************
2  *
3  * Licensed to the Apache Software Foundation (ASF) under one
4  * or more contributor license agreements.  See the NOTICE file
5  * distributed with this work for additional information
6  * regarding copyright ownership.  The ASF licenses this file
7  * to you under the Apache License, Version 2.0 (the
8  * "License"); you may not use this file except in compliance
9  * with the License.  You may obtain a copy of the License at
10  *
11  *   http://www.apache.org/licenses/LICENSE-2.0
12  *
13  * Unless required by applicable law or agreed to in writing,
14  * software distributed under the License is distributed on an
15  * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16  * KIND, either express or implied.  See the License for the
17  * specific language governing permissions and limitations
18  * under the License.
19  *
20  *************************************************************/
21 
22 
23 
24 package org.openoffice.netbeans.modules.office.filesystem;
25 
26 import java.beans.*;
27 import java.io.*;
28 import java.util.*;
29 import java.util.zip.*;
30 
31 import org.openide.ErrorManager;
32 import org.openide.filesystems.*;
33 import org.openide.filesystems.FileSystem; // override java.io.FileSystem
34 import org.openide.util.NbBundle;
35 
36 // ISSUES:
37 // - This FS saves (updates) the file on 'setDocument' or 'removeNotify'.
38 //   It has to let the user to decide to update or not.
39 //
40 // TODOS:
41 // - 'Update' action on the mounted document which saves all recent modifications.
42 // - To introduce 'scope' editable property to control editable portion of
43 //   the mounted document.
44 // - Acceptable document type identification before mount.
45 
46 /**
47  * OpenOffice.org Document filesystem.
48  *
49  * @author misha <misha@openoffice.org>
50  */
51 public class OpenOfficeDocFileSystem
52     extends AbstractFileSystem
53 {
54     public static final  String SCRIPTS_ROOT  = "Scripts";   // must be a folder
55     public static final  String SEPARATOR     = "/";         // zip file separator
56 
57     private static final int    OS_UNKNOWN   = 0;
58     private static final int    OS_UNIX      = 1;
59     private static final int    OS_MACOS     = 2;
60     private static final int    OS_WINDOWS   = 3;
61 
62     private static final int    REFRESH_OFF   = -1;          // -1 is desabled
63     private static final int    REFRESH_TIME  = REFRESH_OFF; // (mS)
64     private static final String TMP_FILE_PREF = "sx_";
65     private static final String TMP_FILE_SUFX = ".sxx";
66 
67     private transient Map       cache;      // filesystem cache
68     private transient File      docFile;    // OpenOffice document
69     private transient ZipFile   zipFile;
70 
71     private static transient int osType;    // type of OS
72 
73     private transient ChildrenStrategy childrenStrategy;
74     private transient EditableStrategy editableStrategy;
75 
76     private transient boolean isModified;   // true if an entry has been removed
77 
78     /**
79      * Static constructor.
80      */
81     static {
82         // Identify the type of OS
83         String osname = System.getProperty("os.name");
84         if (osname.startsWith("Mac OS"))
85             osType = OS_MACOS;
86         else if (osname.startsWith("Windows"))
87             osType = OS_WINDOWS;
88         else
89             osType = OS_UNIX;
90     }
91 
92     /**
93      * Default constructor. Initializes new OpenOffice filesystem.
94      */
OpenOfficeDocFileSystem()95     public OpenOfficeDocFileSystem()
96     {
97         // Create the filesystem cache
98         cache            = new HashMap();
99 
100         // Initialize strategies
101         editableStrategy = new EditableStrategy(SCRIPTS_ROOT);
102         childrenStrategy = new ChildrenStrategy();
103 
104         // Create and use implementations of filesystem functionality:
105         info    = new InfoImpl();
106         change  = new ChangeImpl();
107 
108         // Handle filesystem.attributes files normally:
109         DefaultAttributes defattr = new DefaultAttributes(
110             info, change, new ListImpl());
111 
112         // Handle filesystem.attributes files normally + adds virtual attribute
113         // "java.io.File" that is used in conversion routines FileUtil.toFile and
114         // FileUtil.fromFile
115         //defattr = new InnerAttrs(this, info, change, new ListImpl());
116         // (Otherwise set attr to a special implementation, and use ListImpl for list.)
117         attr = defattr;
118         list = defattr;
119 
120         // transfer = new TransferImpl();
121         setRefreshTime(REFRESH_OFF);
122     }
123 
124     /**
125      * Constructor. Initializes new OpenOffice filesystem with FS capability.
126      */
OpenOfficeDocFileSystem(FileSystemCapability cap)127     public OpenOfficeDocFileSystem(FileSystemCapability cap)
128     {
129         this();
130         setCapability(cap);
131     }
132 
133     /**
134      * Provides unique signature of an instance of the filesystem.
135      * NOTE: The scope is not a part of the signature so it is impossible
136      *       to mount the same archive more then once.
137      */
computeSystemName(File file)138     public static String computeSystemName(File file)
139     {
140         return OpenOfficeDocFileSystem.class.getName() + "[" + file + "]";
141     }
142 
143     // ----------- PROPERTIES --------------
144 
145     /**
146      * Provides the 'human readable' name of the instance of the filesystem.
147      */
getDisplayName()148     public String getDisplayName()
149     {
150         if (!isValid())
151             return NbBundle.getMessage(OpenOfficeDocFileSystem.class,
152                 "LAB_invalid_file_system", ((docFile != null)? docFile.toString(): ""));
153         else
154             return NbBundle.getMessage(OpenOfficeDocFileSystem.class,
155                 "LAB_valid_file_system", docFile.toString());
156     }
157 
158     /**
159      * Retrives the 'document' property.
160      */
getDocument()161     public File getDocument()
162     {
163         return docFile;
164     }
165 
166     /**
167      * Sets the 'document' property.
168      */
169     // Bean setter. Changing the OpenOffice document (or in general, the identity
170     // of the root file object) should cause everything using this filesystem
171     // to refresh. The system name must change and refreshRoot should be used
172     // to ensure that everything is correctly updated.
setDocument(File file)173     public synchronized void setDocument(File file)
174         throws java.beans.PropertyVetoException, java.io.IOException
175     {
176 System.out.println("OpenOfficeDocFileSystem.setDocument: file=\"" + file.toString() + "\"");
177         if((file.exists() == false) || (file.isFile() == false)) {
178             IOException ioe = new IOException(
179                 file.toString() + " does not exist");
180             ErrorManager.getDefault().annotate(ioe, NbBundle.getMessage(
181                 OpenOfficeDocFileSystem.class, "EXC_root_dir_does_not_exist",
182                 file.toString()));
183             throw ioe;
184         }
185         // update the document
186         try {
187           updateDocument();
188         } catch(IOException ioe) {
189             // cannot save all!!!
190 System.out.println("*** OpenOfficeDocFileSystem.setDocument:");
191 System.out.println("    file: " + ((docFile != null)? docFile.toString(): ""));
192 System.out.println("    exception: " + ioe.getMessage());
193         }
194         // new document type verification!!!
195         closeDocument();
196         // open a new document
197         try {
198             openDocument(file);
199             firePropertyChange(PROP_ROOT, null, refreshRoot());
200             setRefreshTime(REFRESH_TIME);
201         } catch(IOException ioe) {
202             // cannot open a new document!!!
203 System.out.println("*** OpenOfficeDocFileSystem.setDocument:");
204 System.out.println("    file: " + ((file != null)? file.toString(): ""));
205 System.out.println("    exception: " + ioe.getMessage());
206         }
207     }
208 
209     /**
210      * Retrives 'readonly' property.
211      * NOTE: The portion of the mounted document available to the user is
212      *       always editable.
213      */
isReadOnly()214     public boolean isReadOnly()
215     {
216         return false;
217     }
218 
219     /**
220      * Sets 'readonly' property.
221      * NOTE: The portion of the mounted document available to the user is
222      *       always editable.
223      */
setReadOnly(boolean flag)224     public void setReadOnly(boolean flag)
225     {
226         // sorry! it is not supported.
227     }
228 
229     // ----------- SPECIAL CAPABILITIES --------------
230 
231     /**
232      * Participates in the environment configuration.
233      * This is how you can affect the classpath for execution, compilation, etc.
234      */
prepareEnvironment(FileSystem.Environment environment)235     public void prepareEnvironment(FileSystem.Environment environment)
236     {
237         // BUG: the compiller cannot access files withing the OpenOffice document.
238         //environment.addClassPath(docFile.toString());
239     }
240 
241     /* -----------------------------------------------------------
242      * Affect the name and icon of files on this filesystem according to their
243      * "status", e.g. version-control modification-commit state:
244      /*
245     private class StatusImpl implements Status {
246         public Image annotateIcon(Image icon, int iconType, Set files) {
247             // You may first modify it, e.g. by adding a check mark to the icon
248             // if that makes sense for this file or group of files.
249             return icon;
250         }
251         public String annotateName(String name, Set files) {
252             // E.g. add some sort of suffix to the name if some of the
253             // files are modified but not backed up or committed somehow:
254             if (theseFilesAreModified(files))
255                 return NbBundle.getMessage(OpenOfficeDocFileSystem.class, "LBL_modified_files", name);
256             else
257                 return name;
258         }
259     }
260 
261     private transient Status status;
262 
263     public Status getStatus() {
264         if (status == null) {
265            status = new StatusImpl();
266         }
267         return status;
268     }
269     // And use fireFileStatusChanged whenever you know something has changed.
270      */
271 
272     /*
273     // Filesystem-specific actions, such as version-control operations.
274     // The actions should typically be CookieActions looking for DataObject
275     // cookies, where the object's primary file is on this type of filesystem.
276     public SystemAction[] getActions() {
277 // ------>>>>  UPDATE OPENOFFICE DOCUMENT  <<<<------
278     return new SystemAction[] {
279         SystemAction.get(SomeAction.class),
280         null, // separator
281         SystemAction.get(SomeOtherAction.class)
282     };
283     }
284      */
285 
286     /**
287      * Notifies this filesystem that it has been removed from the repository.
288      * Concrete filesystem implementations could perform clean-up here.
289      * The default implementation does nothing.
290      * <p>Note that this method is <em>advisory</em> and serves as an optimization
291      * to avoid retaining resources for too long etc. Filesystems should maintain correct
292      * semantics regardless of whether and when this method is called.
293      */
removeNotify()294     public void removeNotify()
295     {
296         setRefreshTime(REFRESH_OFF);    // disable refresh
297         // update the document
298         try {
299           updateDocument();
300         } catch(IOException ioe) {
301             // cannot save all!!!
302 System.out.println("*** OpenOfficeDocFileSystem.removeNotify:");
303 System.out.println("    exception: " + ioe.getMessage());
304         }
305         closeDocument();
306         super.removeNotify();
307     }
308 
309     /*
310      * Opens (mounts) an OpenOffice document.
311      */
openDocument(File file)312     private void openDocument(File file)
313         throws IOException, PropertyVetoException
314     {
315         synchronized(cache) {
316             setSystemName(computeSystemName(file));
317             docFile   = file;
318             zipFile   = new ZipFile(docFile);
319             cacheDocument(zipFile.entries(), editableStrategy);
320             isModified = false;
321         } // synchronized
322     }
323 
324     /*
325      * Closes the document and cleans up the cache.
326      */
closeDocument()327     private void closeDocument()
328     {
329         synchronized(cache) {
330             // if a document mounted - close it
331             if(docFile != null) {
332                 // close the document archive
333                 if(zipFile != null) {
334                     try {
335                         zipFile.close();
336                     } catch(IOException ioe) {
337                         // sorry! we can do nothing about it.
338                     }
339                 }
340                 zipFile = null;
341                 // clean up cache
342                 scanDocument(new CleanStrategy());
343                 docFile = null;
344                 isModified = false;
345             }
346         } // synchronized
347     }
348 
349     /*
350      * Creates a document cache.
351      */
cacheDocument(Enumeration entries, Strategy editables)352     private void cacheDocument(Enumeration entries, Strategy editables)
353     {
354         Entry    cacheEntry;
355         ZipEntry archEntry;
356         synchronized(cache) {
357             cache.clear();
358             // root folder
359             cacheEntry = new ReadWriteEntry(null);
360             cache.put(cacheEntry.getName(), cacheEntry);
361             // the rest of items
362             while(entries.hasMoreElements()) {
363                 archEntry   = (ZipEntry)entries.nextElement();
364                 cacheEntry  = new Entry(archEntry);
365                 if(editables.evaluate(cacheEntry))
366                     cacheEntry  = new ReadWriteEntry(archEntry);
367                 cache.put(cacheEntry.getName(), cacheEntry);
368             }
369         } // synchronized
370     }
371 
372     /*
373      * Updates the document.
374      */
updateDocument()375     private void updateDocument()
376         throws IOException
377     {
378         if(docFile == null)
379             return;
380         synchronized(cache) {
381             ModifiedStrategy modifiedStrategy = new ModifiedStrategy();
382             scanDocument(modifiedStrategy);
383             if((isModified == true) ||
384                 (modifiedStrategy.isModified() == true))
385             {
386                 File tmpFile = null;
387                 try {
388                     // create updated document
389                     tmpFile = File.createTempFile(
390                         TMP_FILE_PREF, TMP_FILE_SUFX, docFile.getParentFile());
391                     saveDocument(tmpFile);
392                 } catch(IOException ioe) {
393                     if(tmpFile != null)
394                         tmpFile.delete();
395                     throw ioe;
396                 }
397                 // close the document archive
398                 if(zipFile != null) {
399                     try {
400                         zipFile.close();
401                     } catch(IOException ioe) {
402                     }
403                 }
404                 zipFile = null;
405                 // create the document and backup
406                 File newFile = new File(docFile.getParentFile() + File.separator +
407                     "~" + docFile.getName());
408                 if(newFile.exists())
409                     newFile.delete();   // delete old backup
410                 docFile.renameTo(newFile);
411                 tmpFile.renameTo(docFile);
412                 // open the document archive
413                 zipFile   = new ZipFile(docFile);
414             }
415             isModified = false;
416         } // synchronized
417     }
418 
419     /*
420      * Saves the document in a new archive.
421      */
saveDocument(File file)422     private void saveDocument(File file)
423         throws IOException
424     {
425         synchronized(cache) {
426             SaveStrategy saver = new SaveStrategy(file);
427             scanDocument(saver);
428             saver.close();
429         } // synchronized
430     }
431 
432     /*
433      * Provides each individual entry in the cached document to an apraiser.
434      */
scanDocument(Strategy strategy)435     private void scanDocument(Strategy strategy)
436     {
437         synchronized(cache) {
438             Iterator  itr = cache.values().iterator();
439             while(itr.hasNext()) {
440                 strategy.evaluate((Entry)itr.next());
441             }
442         } // synchronized
443     }
444 
445     /*
446      * Retrives or creates a file.
447      */
getFileEntry(String name)448     private Entry getFileEntry(String name)
449         throws IOException
450     {
451         Entry cEntry = null;
452         synchronized(cache) {
453             cEntry = (Entry)cache.get(name);
454             if(cEntry == null) {
455                 // create a new file
456                 ZipEntry    zEntry = new ZipEntry(name);
457                 zEntry.setTime(new Date().getTime());
458                 cEntry = new Entry(zEntry);
459                 if(editableStrategy.evaluate(cEntry) == false) {
460                     throw new IOException(
461                         "cannot create/edit readonly file");    // I18N
462                 }
463                 cEntry  = new ReadWriteEntry(zEntry);
464                 cache.put(cEntry.getName(), cEntry);
465                 isModified = true;
466             }
467         } // synchronized
468         return cEntry;
469     }
470 
471     /*
472      * Retrives or creates a folder.
473      */
getFolderEntry(String name)474     private Entry getFolderEntry(String name)
475         throws IOException
476     {
477         Entry cEntry = null;
478         synchronized(cache) {
479             cEntry = (Entry)cache.get(name);
480             if(cEntry == null) {
481                 // create a new folder
482                 ZipEntry    zEntry = new ZipEntry(name + SEPARATOR);
483                 zEntry.setMethod(ZipEntry.STORED);
484                 zEntry.setSize(0);
485                 CRC32 crc = new CRC32();
486                 zEntry.setCrc(crc.getValue());
487                 zEntry.setTime(new Date().getTime());
488                 cEntry  = new Entry(zEntry);
489                 if(editableStrategy.evaluate(cEntry) == false) {
490                     throw new IOException(
491                         "cannot create folder");    // I18N
492                 }
493                 cEntry  = new ReadWriteEntry(zEntry);
494                 cEntry.getOutputStream();           // sets up modified flag
495                 cache.put(cEntry.getName(), cEntry);
496                 isModified = true;
497             } else {
498                 if(cEntry.isFolder() == false)
499                     cEntry = null;
500             }
501         } // synchronized
502         return cEntry;
503     }
504 
505     /*
506      * Converts the name to ZIP file name.
507      * Removes the leading file separator if there is one.
508      * This is WORKAROUND of the BUG in AbstractFileObject:
509      * While AbstractFileObject reprecents the root of the filesystem it uses
510      * the absolute path (the path starts with '/'). It is inconsistent with
511      * the rest of the code.
512      * WORKAROUND: we have to strip leading '/' if it is in the name.
513      */
zipName(String name)514     private static String zipName(String name)
515     {
516         String zname = ((name.startsWith(File.separator))?
517             name.substring(File.separator.length()): name);
518         switch(osType) {
519             case OS_MACOS:
520                 zname = zname.replace(':', '/');        // ':' by '/'
521                 break;
522             case OS_WINDOWS:
523                 zname = zname.replace((char)0x5c, '/'); // '\' by '/'
524                 break;
525             default:
526                 break;
527         }
528         return zname;
529     }
530 
531     // ----------- IMPLEMENTATIONS OF ABSTRACT FUNCTIONALITY ----------
532 
533     /* -----------------------------------------------------------
534      * Information about files and operations on the contents which do
535      * not affect the file's presence or name.
536      */
537     private class InfoImpl
538         implements Info
539     {
folder(String name)540         public boolean folder(String name) {
541             synchronized(cache) {
542                 String zname = zipName(name);
543                 Entry entry = (Entry)cache.get(zname);
544                 if(entry != null)
545                     return entry.isFolder();
546                 // logical zip file entry
547                 childrenStrategy.setParent(zname);
548                 scanDocument(childrenStrategy);
549                 return (childrenStrategy.countChildren() > 0);
550             }
551         }
552 
lastModified(String name)553         public Date lastModified(String name) {
554             synchronized(cache) {
555                 Entry entry = (Entry)cache.get(zipName(name));
556                 return new Date((entry != null)? entry.getTime(): 0L);
557             }
558         }
559 
readOnly(String name)560         public boolean readOnly(String name) {
561             synchronized(cache) {
562                 Entry entry = (Entry)cache.get(zipName(name));
563                 return (entry != null)? entry.isReadOnly(): false;
564             }
565         }
566 
mimeType(String name)567         public String mimeType(String name) {
568             // Unless you have some special means of determining MIME type
569             // (e.g. HTTP headers), ask IDE to use its normal heuristics:
570             // the MIME resolver pool and then file extensions, or if nothing
571             // matches, just content/unknown.
572             return null;
573         }
574 
size(String name)575         public long size(String name) {
576             synchronized(cache) {
577                 Entry entry = (Entry)cache.get(zipName(name));
578                 return (entry != null)? entry.getSize(): 0;
579             } // synchronized
580         }
581 
inputStream(String name)582         public InputStream inputStream(String name)
583             throws FileNotFoundException
584         {
585             synchronized(cache) {
586                 Entry entry = (Entry)cache.get(zipName(name));
587                 return (entry != null)? entry.getInputStream(): null;
588             } // synchronized
589         }
590 
outputStream(String name)591         public OutputStream outputStream(String name)
592             throws IOException
593         {
594             return getFileEntry(zipName(name)).getOutputStream();
595         }
596 
597         // AbstractFileSystem handles locking the file to the rest of the IDE.
598         // This only means that you should define how the file should be locked
599         // to the outside world--perhaps it does not need to be.
lock(String name)600         public void lock(String name)
601             throws IOException
602         {
603 /*
604             File file = getFile(name);
605             if (file.exists() == true && file.canWrite() == false) {
606                 IOException ioe = new IOException("file " + file +
607                     " could not be locked");
608                 ErrorManager.getDefault().annotate(ioe, NbBundle.getMessage(
609                     OpenOfficeDocFileSystem.class, "EXC_file_could_not_be_locked",
610                     file.getName(), getDisplayName(), file.getPath()));
611                 throw ioe;
612             }
613 */
614         }
615 
unlock(String name)616         public void unlock(String name) {
617             // Nothing special needed to unlock a file to the outside world.
618         }
619 
markUnimportant(String name)620         public void markUnimportant(String name) {
621             // Do nothing special. Version-control systems may use this to mark
622             // certain files (e.g. *.class) as not needing to be stored in the VCS
623             // while others (source files) are by default important.
624         }
625 
626     }
627 
628     /* -----------------------------------------------------------
629      * Operations that change the available files.
630      */
631     private class ChangeImpl
632         implements Change
633     {
createFolder(String name)634         public void createFolder(String name)
635             throws IOException
636         {
637             synchronized(cache) {
638                 String zname = zipName(name);
639                 if(cache.get(zname) != null) {
640                     throw new IOException(
641                         "cannot create new folder: " + name);           // I18N
642                 }
643                 getFolderEntry(zname);
644             } // synchronized
645         }
646 
createData(String name)647         public void createData(String name)
648             throws IOException
649         {
650             synchronized(cache) {
651                 String zname = zipName(name);
652                 if(cache.get(zname) != null) {
653                     throw new IOException(
654                         "cannot create new data: " + name);             // I18N
655                 }
656                 OutputStream os = getFileEntry(zname).getOutputStream();
657                 os.close();
658             } // synchronized
659         }
660 
rename(String oldName, String newName)661         public void rename(String oldName, String newName)
662             throws IOException
663         {
664             String oname = zipName(oldName);
665             String nname = zipName(newName);
666             if((oname.length() == 0) || (nname.length() == 0)) {
667                 throw new IOException(
668                     "cannot move or rename the root folder");           // I18N
669             }
670             synchronized(cache) {
671                 if(cache.get(nname) != null) {
672                     throw new IOException(
673                         "target file/folder " + newName + " exists");   // I18N
674                 }
675                 Entry entry = (Entry)cache.get(oname);
676                 if(entry == null) {
677                     throw new IOException(
678                         "there is no such a file/folder " + oldName);   // I18N
679                 }
680                 if(entry.isReadOnly() == true) {
681                     throw new IOException(
682                         "file/folder " + oldName + " is readonly");     // I18N
683                 }
684                 entry.rename(nname);
685                 if(editableStrategy.evaluate(entry) == false) {
686                     entry.rename(oname);
687                     throw new IOException(
688                         "cannot create file/folder");                   // I18N
689                 }
690                 cache.remove(oname);
691                 cache.put(entry.getName(), entry);
692             } // synchronized
693         }
694 
delete(String name)695         public void delete(String name)
696             throws IOException
697         {
698             String zname = zipName(name);
699             if(zname.length() == 0) {
700                 throw new IOException(
701                     "cannot delete the root folder");                   // I18N
702             }
703             synchronized(cache) {
704                 Entry entry = (Entry)cache.remove(zname);
705                 if(entry != null) {
706                     // BUG: this is the design bug. Cache has to
707                     //      remember that the entry was removed.
708                     isModified = true;
709                     entry.clean();
710                 }
711             } // synchronized
712         }
713     }
714 
715     /* -----------------------------------------------------------
716      * Operation which provides the directory structure.
717      */
718     private class ListImpl
719         implements List
720     {
children(String name)721         public String[] children(String name)
722         {
723             String[] children = null;
724             synchronized(cache) {
725                 String zname = zipName(name);
726                 Entry entry = (Entry)cache.get(zname);
727                 if(entry != null) {
728                     // real zip file entry
729                     if(entry.isFolder()) {
730                         childrenStrategy.setParent(entry.getName());
731                     }
732                 } else {
733                     // logical zip file entry
734                     // (portion of the path of a real zip file entry)
735                     childrenStrategy.setParent(zname);
736                 }
737                 scanDocument(childrenStrategy);
738                 children = childrenStrategy.getChildren();
739             } // synchronize
740             return children;
741         }
742 
743     }
744 
745     /** -----------------------------------------------------------
746      * This class adds new virtual attribute "java.io.File".
747      * Because of the fact that FileObjects of __Sample__FileSystem are convertible
748      * to java.io.File by means of attributes. */
749     /*private static class InnerAttrs extends DefaultAttributes {
750         //static final long serialVersionUID = 1257351369229921993L;
751         __Sample__FileSystem sfs;
752         public InnerAttrs(__Sample__FileSystem sfs, AbstractFileSystem.Info info,
753         AbstractFileSystem.Change change,AbstractFileSystem.List list ) {
754             super(info, change, list);
755             this.sfs = sfs;
756         }
757         public Object readAttribute(String name, String attrName) {
758             if (attrName.equals("java.io.File"))  // NOI18N
759                 return sfs.getFile(name);
760 
761             return super.readAttribute(name, attrName);
762         }
763     }*/
764 
765     /* -----------------------------------------------------------
766     // Optional special implementations of copy and (cross-directory) move.
767     private class TransferImpl implements Transfer {
768 
769     public boolean copy(String name, Transfer target, String targetName) throws IOException {
770         // Only permit special implementation within single FS
771         // (or you could implement it across filesystems if you wished):
772         if (target != this) return false;
773         // Specially copy the file in an efficient way, e.g. implement
774         // a copy-on-write algorithm.
775         return true;
776     }
777 
778     public boolean move(String name, Transfer target, String targetName) throws IOException {
779         // Only permit special implementation within single FS
780         // (or you could implement it across filesystems if you wished):
781         if (target != this) return false;
782         // Specially move the file, e.g. retain rename information even
783         // across directories in a version-control system.
784         return true;
785     }
786 
787     }
788      */
789 
790     /* -----------------------------------------------------------
791      * This interface hides an action will be performed on an entry.
792      */
793     private interface Strategy
794     {
evaluate(Entry entry)795         public boolean evaluate(Entry entry);
796     }
797 
798     /* -----------------------------------------------------------
799      * Recognizes editable (read-write) entires
800      */
801     private class EditableStrategy
802         implements Strategy
803     {
804         private String scope;
805 
EditableStrategy(String scope)806         public EditableStrategy(String scope)
807         {
808             this.scope = scope;
809         }
810 
evaluate(Entry entry)811         public boolean evaluate(Entry entry)
812         {
813             // recognizes all entries in a subtree of the
814             // 'scope' as editable entries
815             return (entry != null)?
816                 entry.getName().startsWith(scope): false;
817         }
818     }
819 
820     /* -----------------------------------------------------------
821      * Recognizes and accumulates immediate children of the parent.
822      */
823     private class ChildrenStrategy
824         implements Strategy
825     {
826         private String     parent;
827         private Collection children = new HashSet();
828 
ChildrenStrategy()829         public ChildrenStrategy()
830         {
831         }
832 
setParent(String name)833         public void setParent(String name)
834         {
835             parent = (name.length() > 0)? (name + SEPARATOR): "";
836             if(children == null)
837                 children = (java.util.List)new LinkedList();
838             children.clear();
839         }
840 
evaluate(Entry entry)841         public boolean evaluate(Entry entry)
842         {
843             // do not accept "children" of a file
844             // ignore "read only" part of the filesystem
845             if(entry.isReadOnly() == false) {
846                 // identify a child
847                 if( (entry.getName().length() > 0) &&
848                     (entry.getName().startsWith(parent)))
849                 {
850                     // identify an immediate child
851                     String child = entry.getName();
852                     if(parent.length() > 0) {
853                         child = entry.getName().substring(parent.length());
854                     }
855                     int    idx   = child.indexOf(SEPARATOR);
856                     if(idx > 0)     // more path elements ahead
857                         child = child.substring(0, idx);
858                     return children.add(child);
859                 }
860             }
861             return false;
862         }
863 
countChildren()864         public int countChildren()
865         {
866             return children.size();
867         }
868 
getChildren()869         public String[] getChildren()
870         {
871             String[]  chn = new String[children.size()];
872             Iterator  itr = children.iterator();
873             int       idx = 0;
874             while(itr.hasNext()) {
875                 chn[idx++] = (String)itr.next();
876             }
877             return chn;
878         }
879     }
880 
881     /* -----------------------------------------------------------
882      * Recognizes cache entries which have to be save into new archive.
883      */
884     private class ModifiedStrategy
885         implements Strategy
886     {
887         private boolean modified;
888 
evaluate(Entry entry)889         public boolean evaluate(Entry entry)
890         {
891             modified |= entry.isModified();
892             return entry.isModified();
893         }
894 
isModified()895         public boolean isModified()
896         {
897             return modified;
898         }
899     }
900 
901     /* -----------------------------------------------------------
902      * Saves each entry in the filesystem cache.
903      */
904     private class SaveStrategy
905         implements Strategy
906     {
907         ZipOutputStream docos;
908         IOException     ioexp;
909 
SaveStrategy(File newdoc)910         public SaveStrategy(File newdoc)
911             throws IOException
912         {
913             docos = new ZipOutputStream(new FileOutputStream(newdoc));
914             ioexp = null; // success by default
915         }
916 
evaluate(Entry entry)917         public boolean evaluate(Entry entry)
918         {
919             if(entry.getName().length() == 0)
920                 return false;
921             try {
922                 entry.save(docos);
923             } catch(IOException ioe) {
924                 if(ioexp == null)
925                     ioexp = ioe;
926             }
927             return true;
928         }
929 
close()930         public void close()
931             throws IOException
932         {
933             if(docos != null) {
934                 try {
935                     docos.close();
936                 } catch (IOException ioe) {
937                     ioexp = ioe;
938                 } finally {
939                     docos = null;
940                 }
941                 if(ioexp != null) {
942                     throw ioexp;
943                 }
944             }
945         }
946     }
947 
948     /* -----------------------------------------------------------
949      * Cleans each entiry in the filesystem cache.
950      */
951     private class CleanStrategy
952         implements Strategy
953     {
evaluate(Entry entry)954         public boolean evaluate(Entry entry)
955         {
956             try {
957                 entry.clean();
958             } catch(java.lang.Exception exp) {
959                 // sorry! can do nothing about it.
960             }
961             return true;
962         }
963     }
964 
965     /* -----------------------------------------------------------
966      * ReadOnly cache entry
967      */
968     private class Entry
969     {
970         private String  name;
971         private boolean folder;
972         private long    size;
973         private long    time;
974         private File    node;       // data files only
975 
Entry(ZipEntry entry)976         public Entry(ZipEntry entry)
977         {
978             if(entry != null) {
979                 name   = entry.getName();
980                 folder = entry.isDirectory();
981                 size   = entry.getSize();
982                 time   = entry.getTime();
983                 // removes tail file separator from a folder name
984                 if((folder == true) && (name.endsWith(SEPARATOR))) {
985                     name = name.substring(
986                         0, name.length() - SEPARATOR.length());
987                 }
988             } else {
989                 // 'null' is special cace of root folder
990                 name   = "";
991                 folder = true;
992                 size   = 0;
993                 time   = -1;
994             }
995         }
996 
isReadOnly()997         public boolean isReadOnly()
998         {
999             return true;
1000         }
1001 
isFolder()1002         public boolean isFolder()
1003         {
1004             return folder;
1005         }
1006 
isModified()1007         public boolean isModified()
1008         {
1009             return false;
1010         }
1011 
getName()1012         public String getName()
1013         {
1014             return name;
1015         }
1016 
getSize()1017         public long getSize()
1018         {
1019             return size;
1020         }
1021 
getTime()1022         public long getTime()
1023         {
1024             // ajust last modified time to the java.io.File
1025             return (time >= 0)? time: 0;
1026         }
1027 
getInputStream()1028         public InputStream getInputStream()
1029             throws FileNotFoundException
1030         {
1031             return (isFolder() == false)? new FileInputStream(getFile()): null;
1032         }
1033 
getOutputStream()1034         public OutputStream getOutputStream()
1035             throws IOException
1036         {
1037             return null;
1038         }
1039 
rename(String name)1040         public void rename(String name)
1041             throws IOException
1042         {
1043 //            throw new IOException(
1044 //                "cannot rename readonly file: " + getName());   // I18N
1045             // BUG: this is the design bug. Cache has to mamage such kind
1046             //      of operation in order to keep the data integrity.
1047             this.name = name;
1048         }
1049 
save(ZipOutputStream arch)1050         public void save(ZipOutputStream arch)
1051             throws IOException
1052         {
1053             InputStream is    = null;
1054             ZipEntry    entry = new ZipEntry(
1055                 getName() + ((isFolder())? SEPARATOR: ""));
1056             try {
1057                 if(isFolder()) {
1058                     // folder
1059                     entry.setMethod(ZipEntry.STORED);
1060                     entry.setSize(0);
1061                     CRC32 crc = new CRC32();
1062                     entry.setCrc(crc.getValue());
1063                     entry.setTime(getTime());
1064                     arch.putNextEntry(entry);
1065                 } else {
1066                     // file
1067                     if(isModified() == false)
1068                         entry.setTime(getTime());
1069                     else
1070                         entry.setTime(node.lastModified());
1071                     arch.putNextEntry(entry);
1072                     is = getInputStream();
1073                     FileUtil.copy(is, arch);
1074                 }
1075             } finally {
1076                 // close streams
1077                 if(is != null) {
1078                     try {
1079                         is.close();
1080                     } catch(java.io.IOException ioe) {
1081                         // sorry! can do nothing about it.
1082                     }
1083                 }
1084                 if(arch != null)
1085                     arch.closeEntry();
1086             }
1087         }
1088 
clean()1089         public void clean()
1090             throws IOException
1091         {
1092             if(node != null)
1093                 node.delete();
1094         }
1095 
toString()1096         public String toString()
1097         {
1098             return (
1099                 ((isReadOnly())? "RO ": "RW ") +
1100                 ((isFolder())? "D": "F") +
1101                 " \"" + getName() + "\"");
1102         }
1103 
getFile()1104         /* package */ File getFile()
1105             throws FileNotFoundException
1106         {
1107             if(node == null) {
1108                 try {
1109                     node = File.createTempFile(TMP_FILE_PREF, TMP_FILE_SUFX);
1110                    // copy the file from archive to the cache
1111                    OutputStream nos = null;
1112                    InputStream  zis = null;
1113                     try {
1114                         ZipEntry entry = zipFile.getEntry(getName());
1115                         if(entry != null) {
1116                             // copy existing file to the cache
1117                             zis = zipFile.getInputStream(entry);
1118                             nos = new FileOutputStream(node);
1119                             FileUtil.copy(zis, nos);
1120                         }
1121                     } finally {
1122                         // close streams
1123                         if(nos != null) {
1124                             try {
1125                                 nos.close();
1126                             } catch(java.io.IOException ioe) {
1127                             }
1128                         }
1129                         if(zis != null) {
1130                             try {
1131                                 zis.close();
1132                             } catch(java.io.IOException ioe) {
1133                             }
1134                         }
1135                     }
1136                 } catch(java.lang.Exception exp) {
1137                     // delete cache file
1138                     if(node != null)
1139                         node.delete();
1140                     node = null;
1141                     throw new FileNotFoundException(
1142                         "cannot access file: " + getName());    // I18N
1143                 }
1144             }
1145             return node;
1146         }
1147 
1148     }
1149 
1150     /* -----------------------------------------------------------
1151      * ReadWrite cache entry
1152      */
1153     private class ReadWriteEntry
1154         extends Entry
1155     {
1156         private boolean modified;
1157 
1158         // 'null' is special cace of root folder
ReadWriteEntry(ZipEntry entry)1159         public ReadWriteEntry(ZipEntry entry)
1160         {
1161             super(entry);
1162         }
1163 
isReadOnly()1164         public boolean isReadOnly()
1165         {
1166             return false;
1167         }
1168 
isModified()1169         public boolean isModified()
1170         {
1171             return modified;
1172         }
1173 
rename(String name)1174         public void rename(String name)
1175             throws IOException
1176         {
1177             modified = true;
1178             super.rename(name);
1179         }
1180 
getOutputStream()1181         public OutputStream getOutputStream()
1182             throws IOException
1183         {
1184             modified = true;
1185             return (isFolder() == false)? new FileOutputStream(getFile()): null;
1186         }
1187     }
1188 }
1189