KickJava   Java API By Example, From Geeks To Geeks.

Java > Open Source Codes > de > schlichtherle > io > UpdatingArchiveController


1 /*
2  * UpdatingArchiveController.java
3  *
4  * Created on 28. M�rz 2006, 17:40
5  */

6 /*
7  * Copyright 2004-2006 Schlichtherle IT Services
8  *
9  * Licensed under the Apache License, Version 2.0 (the "License");
10  * you may not use this file except in compliance with the License.
11  * You may obtain a copy of the License at
12  *
13  * http://www.apache.org/licenses/LICENSE-2.0
14  *
15  * Unless required by applicable law or agreed to in writing, software
16  * distributed under the License is distributed on an "AS IS" BASIS,
17  * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
18  * See the License for the specific language governing permissions and
19  * limitations under the License.
20  */

21
22 package de.schlichtherle.io;
23
24 import de.schlichtherle.io.archive.spi.*;
25 import de.schlichtherle.io.rof.*;
26
27 import java.io.*;
28 import java.util.*;
29 import java.util.logging.*;
30
31 /**
32  * An archive controller which's update strategy for updated archive entries
33  * is to do a full update of the target archive file
34  * (as opposed to appending the updated archive entries to the target archive
35  * file).
36  * <p>
37  * <b>TODO:</b> This class requires some more refactorings:
38  * There's a lot code in here which is not specific to the update strategy,
39  * in particular {@link #mount} and its dependencies.
40  * All this should be moved to the super class instead.
41  * This is going to happen as soon as another update strategy is added,
42  * which is planned for TrueZIP 7.
43  *
44  * @author Christian Schlichtherle
45  * @version @version@
46  * @since TrueZIP 6.0 (refactored from the former <code>ZipController</code>)
47  */

48 // TODO: Refactor this to the state pattern and introduce strategy pattern
49
// in order to support different update strategies.
50
final class UpdatingArchiveController extends GeneralArchiveController {
51
52     //
53
// Static fields.
54
//
55

56     private static final String JavaDoc CLASS_NAME
57             = "de/schlichtherle/io/UpdatingArchiveController".replace('/', '.'); // support code obfuscation!
58
private static final Logger logger = Logger.getLogger(CLASS_NAME, CLASS_NAME);
59
60     /** Prefix for temporary files created by this class. */
61     static final String JavaDoc TEMP_FILE_PREFIX = "tzp-ctrl";
62
63     /**
64      * Suffix for temporary files created by this class
65      * - should <em>not</em> be <code>null</code> for enhanced unit tests.
66      */

67     static final String JavaDoc TEMP_FILE_SUFFIX = ".tmp";
68
69     //
70
// Instance fields.
71
//
72

73     /**
74      * The actual archive file as a plain <code>java.io.File</code> object
75      * which serves as the input file for the virtual file system managed
76      * by this {@link ArchiveController} object.
77      * Note that this will be set to a tempory file if the archive file is
78      * enclosed within another archive file.
79      */

80     private java.io.File JavaDoc inFile;
81
82     /**
83      * An {@link InputArchive} object used to mount the virtual file system
84      * and read the entries from the archive file.
85      */

86     private InputArchive inArchive;
87
88     /**
89      * Plain <code>java.io.File</code> object used for temporary output.
90      * Maybe identical to <code>inFile</code>.
91      */

92     private java.io.File JavaDoc outFile;
93
94     /**
95      * The (possibly temporary) {@link OutputArchive} we are writing newly
96      * created or modified entries to.
97      */

98     private OutputArchive outArchive;
99
100     /**
101      * Whether or not nesting this archive file to its enclosing
102      * archive file has been deferred.
103      */

104     private boolean needsReassembly;
105
106     //
107
// Constructors.
108
//
109

110     UpdatingArchiveController(
111             java.io.File JavaDoc target,
112             ArchiveController enclController,
113             String JavaDoc enclEntryName,
114             ArchiveDriver driver) {
115         super(target, enclController, enclEntryName, driver);
116     }
117
118     //
119
// Methods.
120
//
121

122     protected void mount(final boolean autoCreate)
123     throws IOException {
124         assert writeLock().isLocked();
125         assert inArchive == null;
126         assert outFile == null;
127         assert outArchive == null;
128         assert getFileSystem() == null;
129
130         // Do the logging part and leave the work to mount0.
131
logger.log(Level.FINER, "mount.entering", // NOI18N
132
new Object JavaDoc[] {
133                     getPath(),
134                     Boolean.valueOf(autoCreate),
135         });
136         try {
137             mount0(autoCreate);
138         } catch (IOException failure) {
139             assert writeLock().isLocked();
140             assert inArchive == null;
141             assert outFile == null;
142             assert outArchive == null;
143             assert getFileSystem() == null;
144
145             // Log at FINER level. This is mostly because of false positives.
146
logger.log(Level.FINER, "mount.throwing", failure); // NOI18N
147
throw failure;
148         }
149         logger.log(Level.FINER, "mount.exiting"); // NOI18N
150

151         assert writeLock().isLocked();
152         assert autoCreate || inArchive != null;
153         assert autoCreate || outFile == null;
154         assert autoCreate || outArchive == null;
155         assert getFileSystem() != null;
156     }
157
158     private void mount0(final boolean autoCreate)
159     throws IOException {
160         // We need to mount the virtual file system from the input file.
161
// and so far we have not successfully opened the input file.
162
if (usesNativeTargetFile()) {
163             // The target file of this controller is NOT enclosed
164
// in another archive file.
165
// Test modification time BEFORE opening the input file!
166
if (inFile == null)
167                 inFile = getTarget();
168             final long time = inFile.lastModified();
169             if (time != 0) {
170                 // The archive file exists.
171
// Thoroughly test read-only status BEFORE opening
172
// the device file!
173
final boolean isReadOnly = !File.isWritableOrCreatable(inFile);
174                 try {
175                     initInArchive(inFile);
176                 } catch (IOException failure) {
177                     // Wrap cause so that a matching catch block can assume
178
// that it can access the target in the native file system.
179
throw new FalsePositiveNativeException(failure);
180                 }
181                 setFileSystem(new ArchiveFileSystem(
182                         this, inArchive, time, isReadOnly));
183             } else if (!autoCreate) {
184                 // The archive file does not exist and we may not create it
185
// automatically.
186
throw new ArchiveNotFoundException("may not create");
187             } else {
188                 // The archive file does NOT exist, but we may create
189
// it automatically.
190
// Setup output first to implement fail-fast behavior.
191
// This may fail e.g. if the target file is a RAES
192
// encrypted ZIP file and the user cancels password
193
// prompting.
194
ensureOutArchive(); // required!
195
setFileSystem(new ArchiveFileSystem(this));
196             }
197         } else {
198             // The target file of this controller IS (or appears to be)
199
// enclosed in another archive file.
200
if (inFile == null) {
201                 unwrap(getEnclController(), getEnclEntryName(), autoCreate);
202             } else {
203                 // The enclosed archive file has already been updated and the
204
// file previously used for output has been left over to be
205
// reused as our input in order to skip the lengthy process
206
// of searching for the right enclosing archive controller
207
// to extract the entry which is our target.
208
try {
209                     initInArchive(inFile);
210                 } catch (IOException failure) {
211                     // This is very unlikely unless someone has tampered with
212
// the temporary file or this controller is managing an
213
// RAES encrypted ZIP file and the client application has
214
// inadvertently called KeyManager.resetKeyProviders() or
215
// similar and the subsequent repetitious prompting for
216
// the key has unfortunately been cancelled by the user.
217
// Now the next problem is that we cannot always generate
218
// a false positive exception with the correct enclosing
219
// controller because we haven't searched for it.
220
// Anyway, this is so unlikely that we simply throw a
221
// false positive exception and cross fingers that the
222
// controller and entry name information will not be used.
223
// When assertions are enabled, we prefer to treat this as
224
// a bug.
225
assert false : "We should've never got here! Read the source code comments for the full details.";
226                     throw new FalsePositiveFileEntryException(
227                             getEnclController(), // probably not correct!
228
getEnclEntryName(), // dito
229
failure);
230                 }
231                 // Note that the archive file system must be read-write
232
// because we are reusing a file which has been previously
233
// used to output modifications to it!
234
// Similarly, the last modification time of the left over
235
// output file has been set to the last modification time of
236
// its virtual root directory.
237
// Nice trick, isn't it?!
238
setFileSystem(new ArchiveFileSystem(
239                         this, inArchive, inFile.lastModified(), false));
240             }
241         }
242     }
243
244     private void unwrap(
245             final ArchiveController controller,
246             final String JavaDoc entryName,
247             final boolean autoCreate)
248     throws IOException {
249         assert controller != null;
250         //assert !controller.readLock().isLocked();
251
//assert !controller.writeLock().isLocked();
252
assert entryName != null;
253         assert File.EMPTY != entryName;
254         assert inFile == null;
255
256         try {
257             // We want to allow as much concurrency as possible, so we will
258
// write lock the controller only if we need to update it first
259
// or the controller's target shall be automatically created.
260
final ReentrantLock lock = autoCreate
261                     ? controller.writeLock()
262                     : controller.readLock();
263             controller.readLock().lock();
264             if (controller.hasNewData(entryName) || autoCreate) {
265                 controller.readLock().unlock();
266                 controller.runWriteLocked(new IORunnable() {
267                     public void run() throws IOException {
268                         // Update controller if the entry already has new data.
269
// This needs to be done first before we can access the
270
// file system since controller.getInputStream(entryName)
271
// would do the same and controller.update() would
272
// invalidate the file system reference.
273
if (controller.hasNewData(entryName))
274                             controller.update();
275
276                         // Keep a lock for the actual unwrapping.
277
// If this is an ordinary mounting procedure where the
278
// file system shall not be created automatically, then
279
// we MUST NOT hold a write lock while unwrapping and
280
// mounting the file system.
281
// This is to prevent dead locks when using RAES
282
// encrypted ZIP files with JFileChooser where the user
283
// may be prompted for a password by the EDT while one
284
// of JFileChooser's background file loading threads is
285
// holding a read lock for the same controller and
286
// waiting for the EDT to be accessible in order to
287
// prompt the user for the same controller's target file,
288
// too.
289
lock.lock(); // keep lock upon return
290
}
291                 });
292             }
293             try {
294                 unwrapFromLockedController(controller, entryName, autoCreate);
295             } finally {
296                 lock.unlock();
297             }
298         } catch (FalsePositiveDirectoryEntryException failure) {
299             // We could have catched this exception in the inner try-catch
300
// block where we access the controller's file system as well,
301
// but then we would still hold the lock on controller, which
302
// is not necessary while accessing the file system of the
303
// enclosing controller.
304
if (failure.getTarget() == controller)
305                 throw failure; // just created - pass on
306

307             unwrap( controller.getEnclController(),
308                     controller.enclEntryName(entryName),
309                     autoCreate);
310         }
311     }
312
313     private void unwrapFromLockedController(
314             final ArchiveController controller,
315             final String JavaDoc entryName,
316             final boolean autoCreate)
317     throws IOException {
318         assert controller != null;
319         assert controller.readLock().isLocked() || controller.writeLock().isLocked();
320         assert entryName != null;
321         assert File.EMPTY != entryName;
322         assert inFile == null;
323
324         final ArchiveFileSystem controllerFileSystem;
325         try {
326             controllerFileSystem = controller.getFileSystem(
327                     autoCreate && File.isLenient());
328         } catch (FalsePositiveNativeException failure) {
329             // Unwrap cause so that we don't catch recursively here and
330
// disable any other matching catch blocks for failure.
331
throw (IOException) failure.getCause();
332         }
333         if (controllerFileSystem.isFile(entryName)) {
334             // This archive file DOES exist in the enclosing archive.
335
// The input file is only temporarily used for the
336
// archive file entry.
337
final java.io.File JavaDoc tmp = File.createTempFile(
338                     TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
339             // We do properly delete our temps, so this is not required.
340
// In addition, this would be dangerous as the deletion
341
// could happen before our shutdown hook has a chance to
342
// process this controller!!!
343
//tmp.deleteOnExit();
344
try {
345                 // Now extract the entry to the temporary file.
346
File.cp(controller.getInputStream(entryName),
347                         new java.io.FileOutputStream JavaDoc(tmp));
348                 // Don't keep tmp if this fails: our caller
349
// couldn't reproduce the proper exception on a
350
// second try!
351
try {
352                     initInArchive(tmp);
353                 } catch (IOException failure) {
354                     throw new FalsePositiveFileEntryException(
355                             controller, entryName, failure);
356                 }
357                 setFileSystem(new ArchiveFileSystem(this, inArchive,
358                         controllerFileSystem.lastModified(entryName),
359                         controllerFileSystem.isReadOnly()));
360                 inFile = tmp; // init on success only!
361
} catch (Throwable JavaDoc failure) {
362                 // failure could be a NoClassDefFoundError if
363
// target is an RAES encrypted ZIP file and
364
// Bouncycastle's Lightweight Crypto API is not
365
// in the classpath.
366
// We are just catching all kinds of Throwables
367
// to make sure that we always delete the newly
368
// created temp file.
369
// Finally, we pass on the catched exception.
370
if (!tmp.delete()) {
371                     // This should normally never happen...
372
final IOException ioe = new IOException(
373                             tmp.getPath()
374                             + " (couldn't delete corrupted input file)");
375                     ioe.initCause(failure);
376                     throw ioe;
377                 }
378                 if (failure instanceof IOException)
379                     throw (IOException) failure;
380                 else if (failure instanceof RuntimeException JavaDoc)
381                     throw (RuntimeException JavaDoc) failure;
382                 else
383                     throw (Error JavaDoc) failure; // must be Error, throws ClassCastException otherwise!
384
}
385         } else if (controllerFileSystem.isDirectory(entryName)) {
386             throw new FalsePositiveDirectoryEntryException(
387                     controller, entryName);
388         } else if (!autoCreate) {
389             // The entry does NOT exist in the enclosing archive
390
// file and we may not create it automatically.
391
throw new ArchiveNotFoundException("may not create");
392         } else {
393             assert autoCreate;
394             assert controller.writeLock().isLocked();
395
396             // The entry does NOT exist in the enclosing archive
397
// file, but we may create it automatically.
398
// TODO: Why do we need to pass File.isLenient() instead
399
// of just true? Document this!
400
final ArchiveFileSystem.Delta delta
401                     = controllerFileSystem.beginCreateAndLink(
402                         entryName, File.isLenient());
403
404             // This may fail if e.g. the target file is an RAES
405
// encrypted ZIP file and the user cancels password
406
// prompting.
407
ensureOutArchive();
408
409             // Now try to create the entry in the enclosing controller.
410
try {
411                 delta.commit();
412             } catch (IOException failure) {
413                 // The delta on the *enclosing* controller failed.
414
// Hence, we need to revert our state changes.
415
try {
416                     try {
417                         outArchive.close();
418                     } finally {
419                         outArchive = null;
420                     }
421                 } finally {
422                     boolean deleted = outFile.delete();
423                     assert deleted;
424                     outFile = null;
425                 }
426
427                 throw failure;
428             }
429
430             setFileSystem(new ArchiveFileSystem(this));
431         }
432     }
433
434     /**
435      * Initializes <code>inArchive</code> with a newly created
436      * {@link InputArchive} for reading <code>inFile</code>.
437      *
438      * @throws IOException On any I/O related issue with <code>inFile</code>.
439      */

440     private void initInArchive(final java.io.File JavaDoc inFile)
441     throws IOException {
442         assert writeLock().isLocked();
443         assert inArchive == null;
444
445         logger.log(Level.FINEST, "initInArchive.entering", inFile); // NOI18N
446
try {
447             final ReadOnlyFile rof;
448             if (usesNativeTargetFile())
449                 rof = new CountingReadOnlyFile(inFile);
450             else
451                 rof = new SimpleReadOnlyFile(inFile);
452             try {
453                 inArchive = getDriver().createInputArchive(this, rof);
454             } catch (Throwable JavaDoc failure) {
455                 // failure could be a NoClassDefFoundError if target is an RAES
456
// encrypted ZIP file and Bouncycastle's Lightweight
457
// Crypto API is not in the classpath.
458
// We are just catching all kinds of Throwables to make sure
459
// that we close the read only file.
460
// Finally, we will pass on the catched exception.
461
rof.close();
462                 if (failure instanceof IOException)
463                     throw (IOException) failure;
464                 else if (failure instanceof RuntimeException JavaDoc)
465                     throw (RuntimeException JavaDoc) failure;
466                 else if (failure instanceof Error JavaDoc)
467                     throw (Error JavaDoc) failure;
468                 else
469                     throw new AssertionError JavaDoc(failure); // cannot happen!
470
}
471             new InputArchiveMetaData(this, inArchive);
472         } catch (IOException failure) {
473             assert inArchive == null;
474             logger.log(Level.FINEST, "initInArchive.throwing", failure); // NOI18N
475
throw failure;
476         }
477         logger.log(Level.FINEST, "initInArchive.exiting", // NOI18N
478
new Integer JavaDoc(inArchive.getNumArchiveEntries()));
479
480         assert inArchive != null;
481     }
482
483     protected InputStream getInputStreamImpl(
484             final ArchiveEntry entry,
485             final ArchiveEntry dstEntry)
486     throws IOException {
487         assert readLock().isLocked() || writeLock().isLocked();
488         assert !hasNewData(entry.getName());
489
490         if (entry.isDirectory())
491             throw new ArchiveEntryNotFoundException(
492                     entry.getName(), "cannot read directory entry");
493
494         final InputStream in = inArchive.getMetaData().getInputStream(entry, dstEntry);
495         if (in == null) {
496             // This entry is actually a newly created archive file which is now
497
// accessed by another file which's ArchiveDetector doesn't
498
// recognize it as a directory.
499
throw new ArchiveEntryNotFoundException(
500                     entry.getName(), "illegal access to archive file");
501         }
502         return in;
503     }
504     
505     protected OutputStream getOutputStreamImpl(
506             final ArchiveEntry entry,
507             final ArchiveEntry srcEntry)
508     throws IOException {
509         assert writeLock().isLocked();
510         assert !hasNewData(entry.getName());
511
512         ensureOutArchive();
513         final OutputStream out = outArchive.getMetaData().getOutputStream(entry, srcEntry);
514         if (out == null) {
515             // Although not allowed by the specification of its contract,
516
// the archive driver has returned a null value.
517
throw new ArchiveEntryNotFoundException(
518                     entry.getName(), "archive driver bug: illegal null value returned");
519         }
520         return out;
521     }
522
523     protected void touch() throws IOException {
524         assert writeLock().isLocked();
525         ensureOutArchive();
526         super.touch();
527     }
528
529     private void ensureOutArchive()
530     throws IOException {
531         assert writeLock().isLocked();
532
533         if (outArchive != null)
534             return;
535
536         java.io.File JavaDoc tmp = outFile;
537         if (tmp == null) {
538             if (usesNativeTargetFile() && !getTarget().isFile()) {
539                 tmp = getTarget();
540             } else {
541                 // Use a new temporary file as the output archive file.
542
tmp = File.createTempFile(TEMP_FILE_PREFIX, TEMP_FILE_SUFFIX);
543                 // We do properly delete our temps, so this is not required.
544
// In addition, this would be dangerous as the deletion
545
// could happen before our shutdown hook has a chance to
546
// process this controller!!!
547
//tmp.deleteOnExit();
548
}
549         }
550
551         try {
552             initOutArchive(tmp);
553         } catch (TransientIOException failure) {
554             // Currently we do not have any use for this wrapper exception
555
// when creating output archives, so we unwrap the transient
556
// cause here.
557
throw failure.getTransientCause();
558         }
559         outFile = tmp; // init outFile on success only!
560
}
561
562     /**
563      * Initializes <code>outArchive</code> with a newly created
564      * {@link OutputArchive} for writing <code>outFile</code>.
565      * This method will delete <code>outFile</code> if it has successfully
566      * opened it for overwriting, but failed to write the archive file header.
567      *
568      * @throws IOException On any I/O related issue with <code>outFile</code>.
569      */

570     private void initOutArchive(final java.io.File JavaDoc outFile)
571     throws IOException {
572         assert writeLock().isLocked();
573         assert outArchive == null;
574
575         logger.log(Level.FINEST, "initOutArchive.entering", outFile); // NOI18N
576
try {
577             OutputStream out = new java.io.FileOutputStream JavaDoc(outFile);
578             // If we are actually writing to the target file,
579
// we want to remember the byte count in the total byte count.
580
if (outFile == getTarget())
581                 out = new CountingOutputStream(out);
582             try {
583                 outArchive = getDriver().createOutputArchive(this, out, inArchive);
584             } catch (Throwable JavaDoc failure) {
585                 // failure could be a NoClassDefFoundError if target is an RAES
586
// encrypted archive file and Bouncycastle's Lightweight
587
// Crypto API is not in the classpath.
588
// We are just catching all kinds of Throwables to make sure
589
// that we delete the newly created temp file.
590
// Finally, we will pass on the catched exception.
591
out.close();
592                 if (!outFile.delete()) {
593                     // This could happen in situations where the file system
594
// allows us to open the file for overwriting, then
595
// overwriting failed (e.g. because of a cancelled password
596
// for an RAES encrypted ZIP file) and finally the file
597
// system also denied deleting the corrupted file.
598
// Shit happens!
599
final IOException ioe = new IOException(outFile.getPath()
600                             + " (couldn't delete corrupted output file)");
601                     ioe.initCause(failure);
602                     throw ioe;
603                 }
604                 if (failure instanceof IOException)
605                     throw (IOException) failure;
606                 else if (failure instanceof RuntimeException JavaDoc)
607                     throw (RuntimeException JavaDoc) failure;
608                 else if (failure instanceof Error JavaDoc)
609                     throw (Error JavaDoc) failure;
610                 else
611                     throw new AssertionError JavaDoc(failure); // cannot happen!
612
}
613             new OutputArchiveMetaData(this, outArchive);
614         } catch (IOException failure) {
615             assert outArchive == null;
616             logger.log(Level.FINEST, "initOutArchive.throwing", failure); // NOI18N
617
throw failure;
618         }
619         logger.log(Level.FINEST, "initOutArchive.exiting"); // NOI18N
620

621         assert outArchive != null;
622     }
623
624     protected boolean hasNewData(String JavaDoc entryName) {
625         assert readLock().isLocked() || writeLock().isLocked();
626         return outArchive != null && outArchive.getArchiveEntry(entryName) != null;
627     }
628
629     protected void update(
630             final ArchiveException exceptionChain,
631             final boolean waitInputStreams,
632             final boolean closeInputStreams,
633             final boolean waitOutputStreams,
634             final boolean closeOutputStreams,
635             final boolean umount,
636             final boolean reassemble)
637     throws ArchiveException {
638         assert closeInputStreams || !closeOutputStreams; // closeOutputStreams => closeInputStreams
639
assert !umount || reassemble; // umount => reassemble
640
assert writeLock().isLocked();
641         assert inArchive == null || inFile != null; // input archive => input file
642
assert !isTouched() || outArchive != null; // file system touched => output archive
643
assert outArchive == null || outFile != null; // output archive => output file
644

645         // Do the logging part and leave the work to update0.
646
logger.log(Level.FINER, "update.entering", // NOI18N
647
new Object JavaDoc[] {
648                     getPath(),
649                     exceptionChain,
650                     Boolean.valueOf(waitInputStreams),
651                     Boolean.valueOf(closeInputStreams),
652                     Boolean.valueOf(waitOutputStreams),
653                     Boolean.valueOf(closeOutputStreams),
654                     Boolean.valueOf(umount),
655                     Boolean.valueOf(reassemble),
656         });
657         try {
658             update0(exceptionChain,
659                     waitInputStreams, closeInputStreams,
660                     waitOutputStreams, closeOutputStreams,
661                     umount, reassemble);
662         } catch (ArchiveException failure) {
663             logger.log(Level.FINER, "update.throwing", failure); // NOI18N
664
throw failure;
665         }
666         logger.log(Level.FINER, "update.exiting"); // NOI18N
667
}
668
669     private void update0(
670             final ArchiveException exceptionChain,
671             final boolean waitInputStreams,
672             final boolean closeInputStreams,
673             final boolean waitOutputStreams,
674             final boolean closeOutputStreams,
675             final boolean umount,
676             final boolean reassemble)
677     throws ArchiveException {
678         ArchiveException newExceptionChain = exceptionChain;
679
680         // Check output streams first, because closeInputStreams may be
681
// true and closeOutputStreams may be false in which case we
682
// don't even need to check open input streams if there are
683
// some open output streams.
684
if (outArchive != null) {
685             final OutputArchiveMetaData outMetaData = outArchive.getMetaData();
686             final int outStreams = outMetaData.waitAllOutputStreamsByOtherThreads(
687                     waitOutputStreams ? 0 : 50);
688             if (outStreams > 0) {
689                 if (!closeOutputStreams)
690                     throw new ArchiveOutputBusyException(
691                             newExceptionChain, getPath(), outStreams);
692                 newExceptionChain = new ArchiveOutputBusyWarningException(
693                         newExceptionChain, getPath(), outStreams);
694             }
695         }
696         if (inArchive != null) {
697             final InputArchiveMetaData inMetaData = inArchive.getMetaData();
698             final int inStreams = inMetaData.waitAllInputStreamsByOtherThreads(
699                     waitInputStreams ? 0 : 50);
700             if (inStreams > 0) {
701                 if (!closeInputStreams)
702                     throw new ArchiveInputBusyException(
703                             newExceptionChain, getPath(), inStreams);
704                 newExceptionChain = new ArchiveInputBusyWarningException(
705                         newExceptionChain, getPath(), inStreams);
706             }
707         }
708
709         try {
710             if (isTouched()) {
711                 needsReassembly = true;
712                 try {
713                     newExceptionChain = updateOutArchive(newExceptionChain);
714                     assert getFileSystem() == null;
715                     assert inArchive == null;
716                 } finally {
717                     assert outArchive == null;
718                 }
719                 try {
720                     if (reassemble) {
721                         newExceptionChain = reassembleTargetFile(newExceptionChain);
722                         needsReassembly = false;
723                     }
724                 } finally {
725                     shutdownStep3(umount && !needsReassembly);
726                 }
727             } else if (reassemble && needsReassembly) {
728                 // Nesting this archive file to its enclosing archive file
729
// has been deferred until now.
730
assert outFile == null; // isTouched() otherwise!
731
assert inFile != null; // !needsReassembly otherwise!
732
// Beware: inArchive or fileSystem may be initialized!
733
shutdownStep2(newExceptionChain);
734                 outFile = inFile;
735                 inFile = null;
736                 try {
737                     newExceptionChain = reassembleTargetFile(newExceptionChain);
738                     needsReassembly = false;
739                 } finally {
740                     shutdownStep3(umount && !needsReassembly);
741                 }
742             } else if (umount) {
743                 assert reassemble;
744                 assert !needsReassembly;
745                 shutdownStep2(newExceptionChain);
746                 shutdownStep3(true);
747             } else {
748                 // This may happen if File.update() or File.umount() has
749
// been called and no modifications have been applied to
750
// this ArchiveController since its creation or last update.
751
assert outArchive == null;
752             }
753         } catch (IOException failure) {
754             throw new ArchiveException(newExceptionChain, failure);
755         } finally {
756             setScheduled(needsReassembly);
757         }
758
759         if (newExceptionChain != exceptionChain)
760             throw newExceptionChain;
761     }
762
763     protected final int waitAllInputStreamsByOtherThreads(long timeout) {
764         return inArchive != null
765                 ? inArchive.getMetaData().waitAllInputStreamsByOtherThreads(timeout)
766                 : 0;
767     }
768
769     protected final int waitAllOutputStreamsByOtherThreads(long timeout) {
770         return outArchive != null
771                 ? outArchive.getMetaData().waitAllOutputStreamsByOtherThreads(timeout)
772                 : 0;
773     }
774
775     /**
776      * Updates all nodes in the virtual file system to the (temporary) output
777      * archive file.
778      * <p>
779      * <b>This method is intended to be called by <code>update()</code> only!</b>
780      *
781      * @param exceptionChain the head of a chain of exceptions created so far.
782      * @return If any warning exception condition occurs throughout the course
783      * of this method, an {@link ArchiveWarningException} is created
784      * (but not thrown), prepended to <code>exceptionChain</code> and
785      * finally returned.
786      * If multiple warning exception conditions occur, the prepended
787      * exceptions are ordered by appearance so that the <i>last</i>
788      * exception created is the head of the returned exception chain.
789      * @see ArchiveController#update(ArchiveException, boolean, boolean, boolean, boolean, boolean, boolean)
790      * @throws ArchiveException If any exception condition occurs throughout
791      * the course of this method, an {@link ArchiveException}
792      * is created, prepended to <code>exceptionChain</code> and finally
793      * thrown unless it's an {@link ArchiveWarningException}.
794      */

795     private ArchiveException updateOutArchive(
796             ArchiveException exceptionChain)
797     throws ArchiveException {
798         assert writeLock().isLocked();
799         assert isTouched();
800         assert outArchive != null;
801         assert getFileSystem() != null;
802         assert checkNoDeletedEntriesWithNewData(exceptionChain) == exceptionChain;
803
804         final long rootTime;
805         try {
806             try {
807                 try {
808                     exceptionChain = shutdownStep1(exceptionChain);
809
810                     ArchiveWarningException inputEntryCorrupted = null;
811                     ArchiveWarningException outputEntryCorrupted = null;
812
813                     // Entries are always written in reverse order so that
814
// their containing directories will be written last.
815
// This is in order to support dumb ZIP utilities which are
816
// not sorting the entries before unzipping them, in which
817
// case they would process the directories last and thus
818
// hopefully apply their timestamps.
819
// Note: This doesn't help with WinZIP. WinZIP seems to
820
// ignore directory entries completely.
821
final Enumeration e = getFileSystem().getReversedEntries();
822                     while (e.hasMoreElements()) {
823                         final ArchiveEntry entry = (ArchiveEntry) e.nextElement();
824                         final String JavaDoc name = entry.getName();
825                         if (hasNewData(name))
826                             continue; // we have already written this entry
827
if (entry.isDirectory()) {
828                             if (name.equals(ArchiveFileSystem.ROOT))
829                                 continue; // never write the root directory
830
if (entry.getTime() < 0)
831                                 continue; // never write ghost directories
832
// 'entry' will never be used again, so it is safe
833
// to hand over this entry from the InputArchive
834
// to the OutputArchive.
835
outArchive.storeDirectory(entry);
836                         } else if (inArchive != null && inArchive.getArchiveEntry(name) != null) {
837                             assert entry == inArchive.getArchiveEntry(name);
838                             InputStream in;
839                             try {
840                                 in = inArchive.getInputStream(entry, entry);
841                             } catch (IOException failure) {
842                                 if (inputEntryCorrupted == null) {
843                                     exceptionChain = inputEntryCorrupted
844                                             = new ArchiveWarningException(
845                                                 exceptionChain,
846                                                 getPath()
847                                                     + " (skipped one or more corrupted archive entries from the input)",
848                                                 failure);
849                                 }
850                                 continue;
851                             }
852                             try {
853                                 // 'entry' will never be used again, so it is
854
// safe to hand over this entry from the
855
// InputArchive to the OutputArchive.
856
final OutputStream out = outArchive
857                                         .getOutputStream(entry, entry);
858                                 try {
859                                     File.cat(in, out);
860                                 } catch (InputIOException failure) {
861                                     if (outputEntryCorrupted == null) {
862                                         exceptionChain = outputEntryCorrupted
863                                                 = new ArchiveWarningException(
864                                                     exceptionChain,
865                                                     getPath()
866                                                         + " (one or more archive entries in the output may be corrupted)",
867                                                     failure);
868                                     }
869                                 } finally {
870                                     out.close();
871                                 }
872                             } finally {
873                                 in.close();
874                             }
875                         } else {
876                             // This may happen if the entry is an archive
877
// which has been newly created and not yet been
878
// reassembled into this archive.
879
// Write an empty entry now as a marker in order to
880
// recreate the entry when the file system gets
881
// reloaded from the archive.
882
outArchive.getOutputStream(entry, null).close();
883                         }
884                     } // while (e.hasMoreElements())
885
} finally {
886                     // We MUST do cleanup here because (1) any entries in the
887
// filesystem which were successfully written (this is the
888
// normal case) have been modified by the OutputArchive
889
// and thus cannot get used anymore to access the input;
890
// and (2) if there has been any IOException on the
891
// output archive there is no way to recover from it.
892
rootTime = getFileSystem().lastModified(ArchiveFileSystem.ROOT);
893                     shutdownStep2(exceptionChain);
894                 }
895             } catch (IOException failure) {
896                 // The output file is corrupted! We must remove it now to
897
// prevent it from being reused as the input file.
898
// We do this even if the output file is the target file, i.e.
899
// the archive file has just been created, because it
900
// doesn't make any sense to keep a corrupted archive file:
901
// There is no way to recover it and it could spoil any
902
// attempts to redo the file operations, because TrueZIP would
903
// normaly correctly identify it as a false positive archive
904
// file and would not allow to treat it like a directory again.
905
boolean deleted = outFile.delete();
906                 outFile = null;
907                 assert deleted;
908                 throw failure;
909             }
910         } catch (ArchiveException failure) {
911             throw failure;
912         } catch (IOException failure) {
913             throw new ArchiveException(
914                     exceptionChain,
915                     getPath()
916                         + " (could not update archive file - all changes are lost)",
917                     failure);
918         }
919
920         // Set the last modification time of the output archive file
921
// to the last modification time of the root directory
922
// in the virtual file system, effectively preserving it.
923
if (!outFile.setLastModified(rootTime)) {
924             exceptionChain = new ArchiveWarningException(
925                     exceptionChain,
926                     getPath()
927                         + " (couldn't preserve last modification time)");
928         }
929
930         return exceptionChain;
931     }
932
933     private ArchiveException checkNoDeletedEntriesWithNewData(
934             ArchiveException exceptionChain) {
935         assert isTouched();
936         assert getFileSystem() != null;
937
938         // Check if we have written out any entries that have been
939
// deleted from the master directory meanwhile and prepare
940
// to throw a warning exception.
941
final Enumeration e = outArchive.getArchiveEntries();
942         while (e.hasMoreElements()) {
943             final String JavaDoc name = ((ArchiveEntry) e.nextElement()).getName();
944             if (getFileSystem().get(name) == null) {
945                 // The entry has been written out already, but also
946
// has been deleted from the master directory meanwhile.
947
// Create a warning exception, but do not yet throw it.
948
exceptionChain = new ArchiveWarningException(
949                         exceptionChain,
950                         getPath()
951                             + " (couldn't remove archive entry: "
952                             + name + ")");
953             }
954         }
955
956         return exceptionChain;
957     }
958
959     /**
960      * Uses the updated output archive file to reassemble the
961      * target archive file, which may be an entry in an enclosing
962      * archive file.
963      * <p>
964      * <b>This method is intended to be called by <code>update()</code> only!</b>
965      *
966      * @param exceptionChain the head of a chain of exceptions created so far.
967      * @return If any warning condition occurs throughout the course of this
968      * method, a <code>ArchiveWarningException</code> is created (but not
969      * thrown), prepended to <code>exceptionChain</code> and finally
970      * returned.
971      * If multiple warning conditions occur,
972      * the prepended exceptions are ordered by appearance so that the
973      * <i>last</i> exception created is the head of the returned
974      * exception chain.
975      * @return If any warning exception condition occurs throughout the course
976      * of this method, an {@link ArchiveWarningException} is created
977      * (but not thrown), prepended to <code>exceptionChain</code> and
978      * finally returned.
979      * If multiple warning exception conditions occur, the prepended
980      * exceptions are ordered by appearance so that the <i>last</i>
981      * exception created is the head of the returned exception chain.
982      * @throws ArchiveException If any exception condition occurs throughout
983      * the course of this method, an {@link ArchiveException}
984      * is created, prepended to <code>exceptionChain</code> and finally
985      * thrown unless it's an {@link ArchiveWarningException}.
986      */

987     private ArchiveException reassembleTargetFile(
988             ArchiveException exceptionChain)
989     throws ArchiveException {
990         assert writeLock().isLocked();
991
992         if (usesNativeTargetFile()) {
993             // The archive file managed by this object is NOT enclosed in
994
// another archive file.
995
if (outFile != getTarget()) {
996                 // The archive file existed before and we have written
997
// to a temporary output file.
998
// Now copy the temporary output file to the target file,
999
// preserving the last modification time and counting the
1000
// output.
1001
try {
1002                    final OutputStream out = new CountingOutputStream(
1003                            new java.io.FileOutputStream JavaDoc(getTarget()));
1004                    final InputStream in;
1005                    try {
1006                        in = new java.io.FileInputStream JavaDoc(outFile);
1007                    } catch (IOException failure) {
1008                        out.close();
1009                        throw failure;
1010                    }
1011                    File.cp(in , out); // always closes in and out
1012
} catch (IOException cause) {
1013                    throw new ArchiveException(
1014                            exceptionChain,
1015                            getPath()
1016                                + " (could not reassemble archive file - all changes are lost)",
1017                            cause);
1018                }
1019
1020                // Set the last modification time of the target archive file
1021
// to the last modification time of the output archive file,
1022
// which has been set to the last modification time of the root
1023
// directory during updateOutArchive(...).
1024
final long time = outFile.lastModified();
1025                if (time != 0 && !getTarget().setLastModified(time)) {
1026                    exceptionChain = new ArchiveWarningException(
1027                            exceptionChain,
1028                            getPath()
1029                                + " (couldn't preserve last modification time)");
1030                }
1031            }
1032        } else {
1033            // The archive file managed by this archive controller IS
1034
// enclosed in another archive file.
1035
try {
1036                wrap(getEnclController(), getEnclEntryName());
1037            } catch (IOException cause) {
1038                throw new ArchiveException(
1039                        exceptionChain,
1040                        getEnclController().getPath() + "/" + getEnclEntryName()
1041                            + " (could not update archive entry - all changes are lost)",
1042                        cause);
1043            }
1044        }
1045
1046        return exceptionChain;
1047    }
1048
1049    private void wrap(
1050            final ArchiveController controller,
1051            final String JavaDoc entryName)
1052    throws IOException {
1053        assert writeLock().isLocked();
1054        assert controller != null;
1055        //assert !controller.readLock().isLocked();
1056
//assert !controller.writeLock().isLocked();
1057
assert entryName != null;
1058        assert File.EMPTY != entryName;
1059
1060        controller.runWriteLocked(new IORunnable() {
1061            public void run() throws IOException {
1062                wrapToWriteLockedController(controller, entryName);
1063            }
1064        });
1065    }
1066
1067    private void wrapToWriteLockedController(
1068            final ArchiveController controller,
1069            final String JavaDoc entryName)
1070    throws IOException {
1071        assert controller != null;
1072        assert controller.writeLock().isLocked();
1073        assert entryName != null;
1074        assert File.EMPTY != entryName;
1075
1076        // Write the updated output archive file as an entry
1077
// to its enclosing archive file, preserving the
1078
// last modification time of the root directory as the last
1079
// modification time of the entry.
1080
final InputStream in = new java.io.FileInputStream JavaDoc(outFile);
1081        try {
1082            // We know that the enclosing controller's entry is not a false
1083
// positive, so we may safely pass in null as the destination
1084
// de.schlichtherle.io.File.
1085
cp( outFile, in,
1086                null /*new File(controller.target)*/, controller, entryName,
1087                true);
1088        } finally {
1089            in.close();
1090        }
1091    }
1092
1093    /**
1094     * Resets the archive controller to its initial state - all changes to the
1095     * archive file which have not yet been updated get lost!
1096     * <p>
1097     * Thereafter, the archive controller will behave as if it has just been
1098     * created and any subsequent operations on its entries will remount
1099     * the virtual file system from the archive file again.
1100     */

1101    protected void reset() throws IOException {
1102        assert writeLock().isLocked();
1103
1104        ArchiveException exceptionChain = shutdownStep1(null);
1105        shutdownStep2(exceptionChain);
1106        shutdownStep3(true);
1107        setScheduled(false);
1108
1109        if (exceptionChain != null)
1110            throw exceptionChain;
1111    }
1112
1113    protected void finalize()
1114    throws Throwable JavaDoc {
1115        logger.log(Level.FINEST, "finalize.entering", getPath()); // NOI18N
1116
// Note: If fileSystem or inArchive are not null, then the controller
1117
// has been used to perform read-only operations only.
1118
// If outArchive is null, the controller has been used to perform
1119
// write operations, but however, but all file system transactions
1120
// must have failed.
1121
// Otherwise, the fileSystem would have been marked as touched and
1122
// we should never be made elegible for finalization!
1123
// Tactical note: Assertions don't work in a finalizer, so we use
1124
// logging.
1125
if (isTouched())
1126            logger.log(Level.SEVERE, "finalize.hasUpdatedEntries", getPath());
1127        shutdownStep1(null);
1128        shutdownStep2(null);
1129        shutdownStep3(true);
1130        super.finalize();
1131    }
1132
1133    /**
1134     * Closes and disconnects all entry streams of the output and input
1135     * archive.
1136     */

1137    private ArchiveException shutdownStep1(ArchiveException exceptionChain) {
1138        if (outArchive != null)
1139            exceptionChain = outArchive.getMetaData().closeAllStreams(
1140                    exceptionChain);
1141        if (inArchive != null)
1142            exceptionChain = inArchive.getMetaData().closeAllStreams(
1143                    exceptionChain);
1144
1145        return exceptionChain;
1146    }
1147
1148    /**
1149     * Discards the file system and closes the output and input archive.
1150     */

1151    private void shutdownStep2(ArchiveException exceptionChain)
1152    throws IOException {
1153        final ArchiveException oldExceptionChain = exceptionChain;
1154
1155        super.reset(); // discard file system
1156

1157        // The output archive must be closed BEFORE the input archive is
1158
// closed. This is because the input archive has been presented
1159
// to output archive as the "source" when it was created and may
1160
// be using the input archive when its closing to retrieve some
1161
// meta data information.
1162
// E.g. Zip32OutputArchive copies the postamble from the
1163
// Zip32InputArchive when it closes.
1164
if (outArchive != null) {
1165            try {
1166                outArchive.close();
1167            } catch (IOException failure) {
1168                exceptionChain = new ArchiveException(exceptionChain, failure);
1169            } finally {
1170                outArchive = null;
1171            }
1172        }
1173
1174        if (inArchive != null) {
1175            try {
1176                inArchive.close();
1177            } catch (IOException failure) {
1178                exceptionChain = new ArchiveException(exceptionChain, failure);
1179            } finally {
1180                inArchive = null;
1181            }
1182        }
1183
1184        if (exceptionChain != oldExceptionChain)
1185            throw exceptionChain;
1186    }
1187
1188    /**
1189     * Cleans up temporary files.
1190     *
1191     * @param deleteOutFile If this parameter is <code>true</code>,
1192     * this method also deletes the temporary output file unless it's
1193     * the target archive file (i.e. unless the archive file has been
1194     * newly created).
1195     */

1196    private void shutdownStep3(final boolean deleteOutFile) {
1197        if (inFile != null) {
1198            if (inFile != getTarget()) {
1199                boolean deleted = inFile.delete();
1200                assert deleted;
1201            }
1202            inFile = null;
1203        }
1204
1205        if (outFile != null) {
1206            if (deleteOutFile) {
1207                if (outFile != getTarget()) {
1208                    boolean deleted = outFile.delete();
1209                    assert deleted;
1210                }
1211            } else {
1212                //assert outFile != target; // may have been newly created
1213
assert outFile.isFile();
1214                inFile = outFile;
1215            }
1216            outFile = null;
1217        }
1218
1219        if (deleteOutFile)
1220            needsReassembly = false;
1221    }
1222}
1223
Popular Tags