View Javadoc
1   /**
2    *
3    * Copyright 2004 The Apache Software Foundation
4    *
5    * Licensed under the Apache License, Version 2.0 (the "License");
6    * you may not use this file except in compliance with the License.
7    * You may obtain a copy of the License at
8    *
9    * http://www.apache.org/licenses/LICENSE-2.0
10   *
11   * Unless required by applicable law or agreed to in writing, software
12   * distributed under the License is distributed on an "AS IS" BASIS,
13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14   * See the License for the specific language governing permissions and
15   * limitations under the License.
16   */
17  package org.codehaus.plexus.archiver.zip;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.File;
21  import java.io.FileOutputStream;
22  import java.io.IOException;
23  import java.io.InputStream;
24  import java.io.OutputStream;
25  import java.io.SequenceInputStream;
26  import java.nio.ByteBuffer;
27  import java.nio.charset.Charset;
28  import java.util.Hashtable;
29  import java.util.Stack;
30  import java.util.concurrent.ExecutionException;
31  import java.util.zip.CRC32;
32  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
33  import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
34  import org.apache.commons.compress.archivers.zip.ZipEncoding;
35  import org.apache.commons.compress.archivers.zip.ZipEncodingHelper;
36  import org.apache.commons.compress.parallel.InputStreamSupplier;
37  import org.apache.commons.compress.utils.Charsets;
38  import org.codehaus.plexus.archiver.AbstractArchiver;
39  import org.codehaus.plexus.archiver.ArchiveEntry;
40  import org.codehaus.plexus.archiver.Archiver;
41  import org.codehaus.plexus.archiver.ArchiverException;
42  import org.codehaus.plexus.archiver.ResourceIterator;
43  import org.codehaus.plexus.archiver.UnixStat;
44  import org.codehaus.plexus.archiver.exceptions.EmptyArchiveException;
45  import org.codehaus.plexus.archiver.util.ResourceUtils;
46  import org.codehaus.plexus.components.io.functions.SymlinkDestinationSupplier;
47  import org.codehaus.plexus.components.io.resources.PlexusIoResource;
48  import org.codehaus.plexus.util.FileUtils;
49  import org.codehaus.plexus.util.IOUtil;
50  import static org.codehaus.plexus.archiver.util.Streams.bufferedOutputStream;
51  import static org.codehaus.plexus.archiver.util.Streams.fileOutputStream;
52  
53  @SuppressWarnings(
54  {
55      "NullableProblems", "UnusedDeclaration"
56  } )
57  public abstract class AbstractZipArchiver
58      extends AbstractArchiver
59  {
60  
61      private String comment;
62  
63      /**
64       * Encoding to use for filenames, defaults to the platform's
65       * default encoding.
66       */
67      private String encoding = "UTF8";
68  
69      private boolean doCompress = true;
70  
71      private boolean recompressAddedZips = true;
72  
73      private boolean doUpdate = false;
74  
75      // shadow of the above if the value is altered in execute
76      private boolean savedDoUpdate = false;
77  
78      protected String archiveType = "zip";
79  
80      private boolean doFilesonly = false;
81  
82      protected final Hashtable<String, String> entries = new Hashtable<String, String>();
83  
84      protected final AddedDirs addedDirs = new AddedDirs();
85  
86      private static final long EMPTY_CRC = new CRC32().getValue();
87  
88      protected boolean doubleFilePass = false;
89  
90      protected boolean skipWriting = false;
91  
92      /**
93       * @deprecated Use {@link Archiver#setDuplicateBehavior(String)} instead.
94       */
95      protected final String duplicate = Archiver.DUPLICATES_SKIP;
96  
97      /**
98       * true when we are adding new files into the Zip file, as opposed
99       * to adding back the unchanged files
100      */
101     protected boolean addingNewFiles = false;
102 
103     /**
104      * Whether the file modification times will be rounded up to the
105      * next even number of seconds.
106      * <p/>
107      * <p>
108      * Zip archives store file modification times with a
109      * granularity of two seconds, so the times will either be rounded
110      * up or down. If you round down, the archive will always seem
111      * out-of-date when you rerun the task, so the default is to round
112      * up. Rounding up may lead to a different type of problems like
113      * JSPs inside a web archive that seem to be slightly more recent
114      * than precompiled pages, rendering precompilation useless.</p>
115      *
116      * <p/>
117      * plexus-archiver chooses to round up.
118      * <p/>
119      * Java versions up to java7 round timestamp down, which means we add a heuristic value (which is slightly
120      * questionable)
121      * Java versions from 8 and up round timestamp up.
122      * s
123      */
124     private static final boolean isJava7OrLower = getJavaVersion() <= 7;
125 
126     // Renamed version of original file, if it exists
127     private File renamedFile = null;
128 
129     private File zipFile;
130 
131     private boolean success;
132 
133     private ConcurrentJarCreator zOut;
134 
135     protected ZipArchiveOutputStream zipArchiveOutputStream;
136 
137     private static int getJavaVersion()
138     {
139         String javaSpecVersion = System.getProperty( "java.specification.version" );
140         if ( javaSpecVersion.contains( "." ) )
141         {//before jdk 9
142             return Integer.parseInt( javaSpecVersion.split( "\\." )[1] );
143         }
144         else
145         {
146             return Integer.parseInt( javaSpecVersion );
147         }
148     }
149 
150     public String getComment()
151     {
152         return comment;
153     }
154 
155     public void setComment( String comment )
156     {
157         this.comment = comment;
158     }
159 
160     public String getEncoding()
161     {
162         return encoding;
163     }
164 
165     public void setEncoding( String encoding )
166     {
167         this.encoding = encoding;
168     }
169 
170     public void setCompress( boolean compress )
171     {
172         this.doCompress = compress;
173     }
174 
175     public boolean isCompress()
176     {
177         return doCompress;
178     }
179 
180     public boolean isRecompressAddedZips()
181     {
182         return recompressAddedZips;
183     }
184 
185     public void setRecompressAddedZips( boolean recompressAddedZips )
186     {
187         this.recompressAddedZips = recompressAddedZips;
188     }
189 
190     public void setUpdateMode( boolean update )
191     {
192         this.doUpdate = update;
193         savedDoUpdate = doUpdate;
194     }
195 
196     public boolean isInUpdateMode()
197     {
198         return doUpdate;
199     }
200 
201     /**
202      * If true, emulate Sun's jar utility by not adding parent directories;
203      * optional, defaults to false.
204      *
205      * @param f true to emilate sun jar utility
206      */
207     public void setFilesonly( boolean f )
208     {
209         doFilesonly = f;
210     }
211 
212     public boolean isFilesonly()
213     {
214         return doFilesonly;
215     }
216 
217     @Override
218     protected void execute()
219         throws ArchiverException, IOException
220     {
221         if ( !checkForced() )
222         {
223             return;
224         }
225 
226         if ( doubleFilePass )
227         {
228             skipWriting = true;
229             createArchiveMain();
230             skipWriting = false;
231             createArchiveMain();
232         }
233         else
234         {
235             createArchiveMain();
236         }
237 
238         finalizeZipOutputStream( zOut );
239     }
240 
241     protected void finalizeZipOutputStream( ConcurrentJarCreator zOut )
242         throws IOException, ArchiverException
243     {
244     }
245 
246     private void createArchiveMain()
247         throws ArchiverException, IOException
248     {
249         //noinspection deprecation
250         if ( !Archiver.DUPLICATES_SKIP.equals( duplicate ) )
251         {
252             //noinspection deprecation
253             setDuplicateBehavior( duplicate );
254         }
255 
256         ResourceIterator iter = getResources();
257         if ( !iter.hasNext() && !hasVirtualFiles() )
258         {
259             throw new EmptyArchiveException( "archive cannot be empty" );
260         }
261 
262         zipFile = getDestFile();
263 
264         if ( zipFile == null )
265         {
266             throw new ArchiverException( "You must set the destination " + archiveType + "file." );
267         }
268 
269         if ( zipFile.exists() && !zipFile.isFile() )
270         {
271             throw new ArchiverException( zipFile + " isn't a file." );
272         }
273 
274         if ( zipFile.exists() && !zipFile.canWrite() )
275         {
276             throw new ArchiverException( zipFile + " is read-only." );
277         }
278 
279         // Whether or not an actual update is required -
280         // we don't need to update if the original file doesn't exist
281         addingNewFiles = true;
282 
283         if ( doUpdate && !zipFile.exists() )
284         {
285             doUpdate = false;
286             getLogger().debug( "ignoring update attribute as " + archiveType + " doesn't exist." );
287         }
288 
289         success = false;
290 
291         if ( doUpdate )
292         {
293             renamedFile = FileUtils.createTempFile( "zip", ".tmp", zipFile.getParentFile() );
294             renamedFile.deleteOnExit();
295 
296             try
297             {
298                 FileUtils.rename( zipFile, renamedFile );
299             }
300             catch ( SecurityException e )
301             {
302                 getLogger().debug( e.toString() );
303                 throw new ArchiverException(
304                     "Not allowed to rename old file (" + zipFile.getAbsolutePath() + ") to temporary file", e );
305             }
306             catch ( IOException e )
307             {
308                 getLogger().debug( e.toString() );
309                 throw new ArchiverException(
310                     "Unable to rename old file (" + zipFile.getAbsolutePath() + ") to temporary file", e );
311             }
312         }
313 
314         String action = doUpdate ? "Updating " : "Building ";
315 
316         getLogger().info( action + archiveType + ": " + zipFile.getAbsolutePath() );
317 
318         if ( !skipWriting )
319         {
320             zipArchiveOutputStream =
321                 new ZipArchiveOutputStream( bufferedOutputStream( fileOutputStream( zipFile, "zip" ) ) );
322             zipArchiveOutputStream.setEncoding( encoding );
323             zipArchiveOutputStream.setCreateUnicodeExtraFields( this.getUnicodeExtraFieldPolicy() );
324             zipArchiveOutputStream.setMethod(
325                 doCompress ? ZipArchiveOutputStream.DEFLATED : ZipArchiveOutputStream.STORED );
326 
327             zOut = new ConcurrentJarCreator( recompressAddedZips, Runtime.getRuntime().availableProcessors() );
328         }
329         initZipOutputStream( zOut );
330 
331         // Add the new files to the archive.
332         addResources( iter, zOut );
333 
334         // If we've been successful on an update, delete the
335         // temporary file
336         if ( doUpdate )
337         {
338             if ( !renamedFile.delete() )
339             {
340                 getLogger().warn( "Warning: unable to delete temporary file " + renamedFile.getName() );
341             }
342         }
343         success = true;
344     }
345 
346     /**
347      * Gets the {@code UnicodeExtraFieldPolicy} to apply.
348      *
349      * @return {@link ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NEVER}, if the effective encoding is
350      * UTF-8; {@link ZipArchiveOutputStream.UnicodeExtraFieldPolicy.ALWAYS}, if the effective encoding is not
351      * UTF-8.
352      *
353      * @see #getEncoding()
354      */
355     private ZipArchiveOutputStream.UnicodeExtraFieldPolicy getUnicodeExtraFieldPolicy()
356     {
357         // Copied from ZipEncodingHelper.isUTF8()
358         String effectiveEncoding = this.getEncoding();
359 
360         if ( effectiveEncoding == null )
361         {
362             effectiveEncoding = Charset.defaultCharset().name();
363         }
364 
365         boolean utf8 = Charsets.UTF_8.name().equalsIgnoreCase( effectiveEncoding );
366 
367         if ( !utf8 )
368         {
369             for ( String alias : Charsets.UTF_8.aliases() )
370             {
371                 if ( alias.equalsIgnoreCase( effectiveEncoding ) )
372                 {
373                     utf8 = true;
374                     break;
375                 }
376             }
377         }
378 
379         // Using ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NOT_ENCODEABLE as a fallback makes no sense here.
380         // If the encoding is UTF-8 and a name is not encodeable using UTF-8, the Info-ZIP Unicode Path extra field
381         // is not encodeable as well. If the effective encoding is not UTF-8, we always add the extra field. If it is
382         // UTF-8, we never add the extra field.
383         return utf8
384                    ? ZipArchiveOutputStream.UnicodeExtraFieldPolicy.NEVER
385                    : ZipArchiveOutputStream.UnicodeExtraFieldPolicy.ALWAYS;
386 
387     }
388 
389     /**
390      * Add the given resources.
391      *
392      * @param resources the resources to add
393      * @param zOut the stream to write to
394      */
395     @SuppressWarnings(
396     {
397         "JavaDoc"
398     } )
399     protected final void addResources( ResourceIterator resources, ConcurrentJarCreator zOut )
400         throws IOException, ArchiverException
401     {
402         while ( resources.hasNext() )
403         {
404             ArchiveEntry entry = resources.next();
405             String name = entry.getName();
406             name = name.replace( File.separatorChar, '/' );
407 
408             if ( "".equals( name ) )
409             {
410                 continue;
411             }
412 
413             if ( entry.getResource().isDirectory() && !name.endsWith( "/" ) )
414             {
415                 name = name + "/";
416             }
417 
418             addParentDirs( entry, null, name, zOut );
419 
420             if ( entry.getResource().isFile() )
421             {
422                 zipFile( entry, zOut, name );
423             }
424             else
425             {
426                 zipDir( entry.getResource(), zOut, name, entry.getMode(), encoding );
427             }
428         }
429     }
430 
431     /**
432      * Ensure all parent dirs of a given entry have been added.
433      * <p/>
434      * This method is computed in terms of the potentially remapped entry (that may be disconnected from the file system)
435      * we do not *relly* know the entry's connection to the file system so establishing the attributes of the parents can
436      * be impossible and is not really supported.
437      */
438     @SuppressWarnings(
439     {
440         "JavaDoc"
441     } )
442     private void addParentDirs( ArchiveEntry archiveEntry, File baseDir, String entry, ConcurrentJarCreator zOut )
443         throws IOException
444     {
445         if ( !doFilesonly && getIncludeEmptyDirs() )
446         {
447             Stack<String> directories = addedDirs.asStringStack( entry );
448 
449             while ( !directories.isEmpty() )
450             {
451                 String dir = directories.pop();
452                 File f;
453                 if ( baseDir != null )
454                 {
455                     f = new File( baseDir, dir );
456                 }
457                 else
458                 {
459                     f = new File( dir );
460                 }
461                 // the
462                 // At this point we could do something like read the atr
463                 final PlexusIoResource res = new AnonymousResource( f );
464                 zipDir( res, zOut, dir, archiveEntry.getDefaultDirMode(), encoding );
465             }
466         }
467     }
468 
469     /**
470      * Adds a new entry to the archive, takes care of duplicates as well.
471      *
472      * @param in the stream to read data for the entry from.
473      * @param zOut the stream to write to.
474      * @param vPath the name this entry shall have in the archive.
475      * @param lastModified last modification time for the entry.
476      * @param fromArchive the original archive we are copying this
477      * @param symlinkDestination
478      * @param addInParallel Indicates if the entry should be add in parallel.
479      * If set to {@code false} it is added synchronously.
480      * If the entry is symbolic link this parameter is ignored.
481      */
482     @SuppressWarnings(
483     {
484         "JavaDoc"
485     } )
486     protected void zipFile( InputStreamSupplier in, ConcurrentJarCreator zOut, String vPath,
487                             long lastModified,
488                             File fromArchive, int mode, String symlinkDestination, boolean addInParallel )
489         throws IOException, ArchiverException
490     {
491         getLogger().debug( "adding entry " + vPath );
492 
493         entries.put( vPath, vPath );
494 
495         if ( !skipWriting )
496         {
497             ZipArchiveEntry ze = new ZipArchiveEntry( vPath );
498             setTime( ze, lastModified );
499 
500             ze.setMethod( doCompress ? ZipArchiveEntry.DEFLATED : ZipArchiveEntry.STORED );
501             ze.setUnixMode( UnixStat.FILE_FLAG | mode );
502 
503             InputStream payload;
504             if ( ze.isUnixSymlink() )
505             {
506                 final byte[] bytes = encodeArchiveEntry( symlinkDestination, getEncoding() );
507                 payload = new ByteArrayInputStream( bytes );
508                 zOut.addArchiveEntry( ze, createInputStreamSupplier( payload ), true );
509             }
510             else
511             {
512                 zOut.addArchiveEntry( ze, in, addInParallel );
513             }
514         }
515     }
516 
517     /**
518      * Method that gets called when adding from java.io.File instances.
519      * <p/>
520      * <p>
521      * This implementation delegates to the six-arg version.</p>
522      *
523      * @param entry the file to add to the archive
524      * @param zOut the stream to write to
525      * @param vPath the name this entry shall have in the archive
526      */
527     @SuppressWarnings(
528     {
529         "JavaDoc"
530     } )
531     protected void zipFile( final ArchiveEntry entry, ConcurrentJarCreator zOut, String vPath )
532         throws IOException, ArchiverException
533     {
534         final PlexusIoResource resource = entry.getResource();
535         if ( ResourceUtils.isSame( resource, getDestFile() ) )
536         {
537             throw new ArchiverException( "A zip file cannot include itself" );
538         }
539 
540         final boolean b = entry.getResource() instanceof SymlinkDestinationSupplier;
541         String symlinkTarget = b ? ( (SymlinkDestinationSupplier) entry.getResource() ).getSymlinkDestination() : null;
542         InputStreamSupplier in = new InputStreamSupplier()
543         {
544 
545             @Override
546             public InputStream get()
547             {
548                 try
549                 {
550                     return entry.getInputStream();
551                 }
552                 catch ( IOException e )
553                 {
554                     throw new RuntimeException( e );
555                 }
556             }
557 
558         };
559         try
560         {
561             zipFile( in, zOut, vPath, resource.getLastModified(), null, entry.getMode(), symlinkTarget,
562                      !entry.shouldAddSynchronously() );
563         }
564         catch ( IOException e )
565         {
566             throw new ArchiverException( "IOException when zipping r" + entry.getName() + ": " + e.getMessage(), e );
567         }
568     }
569 
570     private void setTime( java.util.zip.ZipEntry zipEntry, long lastModified )
571     {
572         zipEntry.setTime( lastModified + ( isJava7OrLower ? 1999 : 0 ) );
573 
574         /*   Consider adding extended file stamp support.....
575 
576          X5455_ExtendedTimestamp ts =  new X5455_ExtendedTimestamp();
577          ts.setModifyJavaTime(new Date(lastModified));
578          if (zipEntry.getExtra() != null){
579          // Uh-oh. What do we do now.
580          throw new IllegalStateException("DIdnt expect to see xtradata here ?");
581 
582          }  else {
583          zipEntry.setExtra(ts.getLocalFileDataData());
584          }
585          */
586     }
587 
588     protected void zipDir( PlexusIoResource dir, ConcurrentJarCreator zOut, String vPath, int mode,
589                            String encodingToUse )
590         throws IOException
591     {
592         if ( addedDirs.update( vPath ) )
593         {
594             return;
595         }
596 
597         getLogger().debug( "adding directory " + vPath );
598 
599         if ( !skipWriting )
600         {
601             final boolean isSymlink = dir instanceof SymlinkDestinationSupplier;
602 
603             if ( isSymlink && vPath.endsWith( File.separator ) )
604             {
605                 vPath = vPath.substring( 0, vPath.length() - 1 );
606             }
607 
608             ZipArchiveEntry ze = new ZipArchiveEntry( vPath );
609 
610             /*
611              * ZipOutputStream.putNextEntry expects the ZipEntry to
612              * know its size and the CRC sum before you start writing
613              * the data when using STORED mode - unless it is seekable.
614              *
615              * This forces us to process the data twice.
616              */
617             if ( isSymlink )
618             {
619                 mode = UnixStat.LINK_FLAG | mode;
620             }
621 
622             if ( dir != null && dir.isExisting() )
623             {
624                 setTime( ze, dir.getLastModified() );
625             }
626             else
627             {
628                 // ZIPs store time with a granularity of 2 seconds, round up
629                 setTime( ze, System.currentTimeMillis() );
630             }
631             if ( !isSymlink )
632             {
633                 ze.setSize( 0 );
634                 ze.setMethod( ZipArchiveEntry.STORED );
635                 // This is faintly ridiculous:
636                 ze.setCrc( EMPTY_CRC );
637             }
638             ze.setUnixMode( mode );
639 
640             if ( !isSymlink )
641             {
642                 zOut.addArchiveEntry( ze, createInputStreamSupplier( new ByteArrayInputStream( "".getBytes() ) ), true );
643             }
644             else
645             {
646                 String symlinkDestination = ( (SymlinkDestinationSupplier) dir ).getSymlinkDestination();
647                 final byte[] bytes = encodeArchiveEntry( symlinkDestination, encodingToUse );
648                 ze.setMethod( ZipArchiveEntry.DEFLATED );
649                 zOut.addArchiveEntry( ze, createInputStreamSupplier( new ByteArrayInputStream( bytes ) ), true );
650             }
651         }
652     }
653 
654     private byte[] encodeArchiveEntry( String payload, String encoding )
655         throws IOException
656     {
657         ZipEncoding enc = ZipEncodingHelper.getZipEncoding( encoding );
658         ByteBuffer encodedPayloadByteBuffer = enc.encode( payload );
659         byte[] encodedPayloadBytes = new byte[encodedPayloadByteBuffer.limit()];
660         encodedPayloadByteBuffer.get( encodedPayloadBytes );
661 
662         return encodedPayloadBytes;
663     }
664 
665     protected InputStreamSupplier createInputStreamSupplier( final InputStream inputStream )
666     {
667         return new InputStreamSupplier()
668         {
669 
670             @Override
671             public InputStream get()
672             {
673                 return inputStream;
674             }
675 
676         };
677     }
678 
679     /**
680      * Create an empty zip file
681      *
682      * @param zipFile The file
683      *
684      * @return true for historic reasons
685      */
686     @SuppressWarnings(
687     {
688         "JavaDoc"
689     } )
690     protected boolean createEmptyZip( File zipFile )
691         throws ArchiverException
692     {
693         // In this case using java.util.zip will not work
694         // because it does not permit a zero-entry archive.
695         // Must create it manually.
696         getLogger().info( "Note: creating empty " + archiveType + " archive " + zipFile );
697         OutputStream os = null;
698         try
699         {
700             os = new FileOutputStream( zipFile );
701             // Cf. PKZIP specification.
702             byte[] empty = new byte[ 22 ];
703             empty[0] = 80; // P
704             empty[1] = 75; // K
705             empty[2] = 5;
706             empty[3] = 6;
707             // remainder zeros
708             os.write( empty );
709             os.close();
710             os = null;
711         }
712         catch ( IOException ioe )
713         {
714             throw new ArchiverException( "Could not create empty ZIP archive " + "(" + ioe.getMessage() + ")", ioe );
715         }
716         finally
717         {
718             IOUtil.close( os );
719         }
720         return true;
721     }
722 
723     /**
724      * Do any clean up necessary to allow this instance to be used again.
725      * <p/>
726      * <p>
727      * When we get here, the Zip file has been closed and all we
728      * need to do is to reset some globals.</p>
729      * <p/>
730      * <p>
731      * This method will only reset globals that have been changed
732      * during execute(), it will not alter the attributes or nested
733      * child elements. If you want to reset the instance so that you
734      * can later zip a completely different set of files, you must use
735      * the reset method.</p>
736      *
737      * @see #reset
738      */
739     @Override
740     protected void cleanUp()
741         throws IOException
742     {
743         super.cleanUp();
744         addedDirs.clear();
745         entries.clear();
746         addingNewFiles = false;
747         doUpdate = savedDoUpdate;
748         success = false;
749         zOut = null;
750         renamedFile = null;
751         zipFile = null;
752     }
753 
754     /**
755      * Makes this instance reset all attributes to their default
756      * values and forget all children.
757      *
758      * @see #cleanUp
759      */
760     public void reset()
761     {
762         setDestFile( null );
763 //        duplicate = "add";
764         archiveType = "zip";
765         doCompress = true;
766         doUpdate = false;
767         doFilesonly = false;
768         encoding = null;
769     }
770 
771     /**
772      * method for subclasses to override
773      *
774      * @param zOut The output stream
775      */
776     protected void initZipOutputStream( ConcurrentJarCreator zOut )
777         throws ArchiverException, IOException
778     {
779     }
780 
781     /**
782      * method for subclasses to override
783      */
784     @Override
785     public boolean isSupportingForced()
786     {
787         return true;
788     }
789 
790     @Override
791     protected boolean revert( StringBuffer messageBuffer )
792     {
793         int initLength = messageBuffer.length();
794 
795         // delete a bogus ZIP file (but only if it's not the original one)
796         if ( ( !doUpdate || renamedFile != null ) && !zipFile.delete() )
797         {
798             messageBuffer.append( " (and the archive is probably corrupt but I could not delete it)" );
799         }
800 
801         if ( doUpdate && renamedFile != null )
802         {
803             try
804             {
805                 FileUtils.rename( renamedFile, zipFile );
806             }
807             catch ( IOException e )
808             {
809                 messageBuffer.append( " (and I couldn't rename the temporary file " );
810                 messageBuffer.append( renamedFile.getName() );
811                 messageBuffer.append( " back)" );
812             }
813         }
814 
815         return messageBuffer.length() == initLength;
816     }
817 
818     @Override
819     protected void close()
820         throws IOException
821     {
822         // Close the output stream.
823         try
824         {
825             if ( zipArchiveOutputStream != null )
826             {
827                 if ( zOut != null )
828                 {
829                     zOut.writeTo( zipArchiveOutputStream );
830                 }
831                 zipArchiveOutputStream.close();
832             }
833         }
834         catch ( IOException ex )
835         {
836             // If we're in this finally clause because of an
837             // exception, we don't really care if there's an
838             // exception when closing the stream. E.g. if it
839             // throws "ZIP file must have at least one entry",
840             // because an exception happened before we added
841             // any files, then we must swallow this
842             // exception. Otherwise, the error that's reported
843             // will be the close() error, which is not the
844             // real cause of the problem.
845             if ( success )
846             {
847                 throw ex;
848             }
849 
850         }
851         catch ( InterruptedException e )
852         {
853             IOException ex = new IOException( "InterruptedException exception" );
854             ex.initCause( e.getCause() );
855             throw ex;
856         }
857         catch ( ExecutionException e )
858         {
859             IOException ex = new IOException( "Execution exception" );
860             ex.initCause( e.getCause() );
861             throw ex;
862         }
863     }
864 
865     @Override
866     protected String getArchiveType()
867     {
868         return archiveType;
869     }
870 
871 }