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 * Retrieves 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 * Retrieves '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 within 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 * Retrieves 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 * Retrieves 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