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.jar;
18  
19  import java.io.ByteArrayInputStream;
20  import java.io.ByteArrayOutputStream;
21  import java.io.File;
22  import java.io.FileInputStream;
23  import java.io.IOException;
24  import java.io.InputStream;
25  import java.io.OutputStreamWriter;
26  import java.io.PrintWriter;
27  import java.util.ArrayList;
28  import java.util.Collections;
29  import java.util.Comparator;
30  import java.util.Enumeration;
31  import java.util.HashSet;
32  import java.util.List;
33  import java.util.Set;
34  import java.util.SortedMap;
35  import java.util.StringTokenizer;
36  import java.util.TreeMap;
37  import java.util.Vector;
38  import org.apache.commons.compress.archivers.zip.ZipArchiveEntry;
39  import org.apache.commons.compress.archivers.zip.ZipArchiveOutputStream;
40  import org.apache.commons.compress.parallel.InputStreamSupplier;
41  import org.codehaus.plexus.archiver.ArchiverException;
42  import org.codehaus.plexus.archiver.zip.ConcurrentJarCreator;
43  import org.codehaus.plexus.archiver.zip.ZipArchiver;
44  import org.codehaus.plexus.logging.Logger;
45  import org.codehaus.plexus.logging.console.ConsoleLogger;
46  import org.codehaus.plexus.util.IOUtil;
47  import static org.codehaus.plexus.archiver.util.Streams.bufferedOutputStream;
48  import static org.codehaus.plexus.archiver.util.Streams.fileOutputStream;
49  
50  /**
51   * Base class for tasks that build archives in JAR file format.
52   */
53  @SuppressWarnings(
54  {
55      "NullableProblems"
56  } )
57  public class JarArchiver
58      extends ZipArchiver
59  {
60  
61      /**
62       * the name of the meta-inf dir
63       */
64      private static final String META_INF_NAME = "META-INF";
65  
66      /**
67       * The index file name.
68       */
69      private static final String INDEX_NAME = "META-INF/INDEX.LIST";
70  
71      /**
72       * The manifest file name.
73       */
74      private static final String MANIFEST_NAME = "META-INF/MANIFEST.MF";
75  
76      /**
77       * merged manifests added through addConfiguredManifest
78       */
79      private Manifest configuredManifest;
80  
81      /**
82       * shadow of the above if upToDate check alters the value
83       */
84      private Manifest savedConfiguredManifest;
85  
86      /**
87       * merged manifests added through filesets
88       */
89      private Manifest filesetManifest;
90  
91      /**
92       * Manifest of original archive, will be set to null if not in
93       * update mode.
94       */
95      private Manifest originalManifest;
96  
97      /**
98       * whether to merge fileset manifests;
99       * value is true if filesetmanifest is 'merge' or 'mergewithoutmain'
100      */
101     private FilesetManifestConfig filesetManifestConfig;
102 
103     /**
104      * whether to merge the main section of fileset manifests;
105      * value is true if filesetmanifest is 'merge'
106      */
107     private boolean mergeManifestsMain = true;
108 
109     /**
110      * the manifest specified by the 'manifest' attribute *
111      */
112     private Manifest manifest;
113 
114     /**
115      * The file found from the 'manifest' attribute. This can be
116      * either the location of a manifest, or the name of a jar added
117      * through a fileset. If its the name of an added jar, the
118      * manifest is looked for in META-INF/MANIFEST.MF
119      */
120     private File manifestFile;
121 
122     /**
123      * jar index is JDK 1.3+ only
124      */
125     private boolean index = false;
126 
127     /**
128      * whether to really create the archive in createEmptyZip, will
129      * get set in getResourcesToAdd.
130      */
131     private boolean createEmpty = false;
132 
133     /**
134      * Stores all files that are in the root of the archive (i.e. that
135      * have a name that doesn't contain a slash) so they can get
136      * listed in the index.
137      * <p/>
138      * Will not be filled unless the user has asked for an index.
139      */
140     private Vector<String> rootEntries;
141 
142     /**
143      * Path containing jars that shall be indexed in addition to this archive.
144      */
145     private ArrayList<String> indexJars;
146 
147     /**
148      * constructor
149      */
150     public JarArchiver()
151     {
152         super();
153         archiveType = "jar";
154         setEncoding( "UTF8" );
155         rootEntries = new Vector<String>();
156     }
157 
158     /**
159      * Set whether or not to create an index list for classes.
160      * This may speed up classloading in some cases.
161      *
162      * @param flag true to create an index
163      */
164     public void setIndex( boolean flag )
165     {
166         index = flag;
167     }
168 
169     @SuppressWarnings(
170     {
171         "JavaDoc", "UnusedDeclaration"
172     } )
173     @Deprecated // Useless method. Manifests should be UTF-8 by convention. Calling this setter does nothing
174     public void setManifestEncoding( String manifestEncoding )
175     {
176     }
177 
178     /**
179      * Allows the manifest for the archive file to be provided inline
180      * in the build file rather than in an external file.
181      *
182      * @param newManifest The new manifest
183      *
184      * @throws ManifestException .
185      */
186     public void addConfiguredManifest( Manifest newManifest )
187         throws ManifestException
188     {
189         if ( configuredManifest == null )
190         {
191             configuredManifest = newManifest;
192         }
193         else
194         {
195             JdkManifestFactory.merge( configuredManifest, newManifest, false );
196         }
197         savedConfiguredManifest = configuredManifest;
198     }
199 
200     /**
201      * The manifest file to use. This can be either the location of a manifest, or the name of a jar added through a
202      * fileset. If its the name of an added jar, the task expects the manifest to be in the jar at META-INF/MANIFEST.MF.
203      *
204      * @param manifestFile the manifest file to use.
205      *
206      * @throws org.codehaus.plexus.archiver.ArchiverException
207      * .
208      */
209     @SuppressWarnings(
210     {
211         "UnusedDeclaration"
212     } )
213     public void setManifest( File manifestFile )
214         throws ArchiverException
215     {
216         if ( !manifestFile.exists() )
217         {
218             throw new ArchiverException( "Manifest file: " + manifestFile + " does not exist." );
219         }
220 
221         this.manifestFile = manifestFile;
222     }
223 
224     private Manifest getManifest( File manifestFile )
225         throws ArchiverException
226     {
227         InputStream in = null;
228         try
229         {
230             in = new FileInputStream( manifestFile );
231             final Manifest mf = getManifest( in );
232             in.close();
233             in = null;
234             return mf;
235         }
236         catch ( IOException e )
237         {
238             throw new ArchiverException( "Unable to read manifest file: " + manifestFile + " (" + e.getMessage() + ")",
239                                          e );
240         }
241         finally
242         {
243             IOUtil.close( in );
244         }
245     }
246 
247     private Manifest getManifest( InputStream is )
248         throws ArchiverException
249     {
250         try
251         {
252             return new Manifest( is );
253         }
254         catch ( IOException e )
255         {
256             throw new ArchiverException( "Unable to read manifest file" + " (" + e.getMessage() + ")", e );
257         }
258     }
259 
260     /**
261      * Behavior when a Manifest is found in a zipfileset or zipgroupfileset file.
262      * Valid values are "skip", "merge", and "mergewithoutmain".
263      * "merge" will merge all of manifests together, and merge this into any
264      * other specified manifests.
265      * "mergewithoutmain" merges everything but the Main section of the manifests.
266      * Default value is "skip".
267      * <p/>
268      * Note: if this attribute's value is not "skip", the created jar will not
269      * be readable by using java.util.jar.JarInputStream
270      *
271      * @param config setting for found manifest behavior.
272      */
273     @SuppressWarnings(
274     {
275         "UnusedDeclaration"
276     } )
277     public void setFilesetmanifest( FilesetManifestConfig config )
278     {
279         filesetManifestConfig = config;
280         mergeManifestsMain = FilesetManifestConfig.merge == config;
281 
282         if ( ( filesetManifestConfig != null ) && filesetManifestConfig != FilesetManifestConfig.skip )
283         {
284 
285             doubleFilePass = true;
286         }
287     }
288 
289     /**
290      * @param indexJar The indexjar
291      */
292     public void addConfiguredIndexJars( File indexJar )
293     {
294         if ( indexJars == null )
295         {
296             indexJars = new ArrayList<String>();
297         }
298         indexJars.add( indexJar.getAbsolutePath() );
299     }
300 
301     @Override
302     protected void initZipOutputStream( ConcurrentJarCreator zOut )
303         throws ArchiverException, IOException
304     {
305         if ( !skipWriting )
306         {
307             Manifest jarManifest = createManifest();
308             writeManifest( zOut, jarManifest );
309         }
310     }
311 
312     @Override
313     protected boolean hasVirtualFiles()
314     {
315         getLogger().debug( "\n\n\nChecking for jar manifest virtual files...\n\n\n" );
316         System.out.flush();
317 
318         return ( configuredManifest != null ) || ( manifest != null ) || ( manifestFile != null )
319                    || super.hasVirtualFiles();
320     }
321 
322     private Manifest createManifest()
323         throws ArchiverException
324     {
325         Manifest finalManifest = Manifest.getDefaultManifest();
326 
327         if ( ( manifest == null ) && ( manifestFile != null ) )
328         {
329             // if we haven't got the manifest yet, attempt to
330             // get it now and have manifest be the final merge
331             manifest = getManifest( manifestFile );
332         }
333 
334         /*
335          * Precedence: manifestFile wins over inline manifest,
336          * over manifests read from the filesets over the original
337          * manifest.
338          *
339          * merge with null argument is a no-op
340          */
341         if ( isInUpdateMode() )
342         {
343             JdkManifestFactory.merge( finalManifest, originalManifest, false );
344         }
345         JdkManifestFactory.merge( finalManifest, filesetManifest, false );
346         JdkManifestFactory.merge( finalManifest, configuredManifest, false );
347         JdkManifestFactory.merge( finalManifest, manifest, !mergeManifestsMain );
348 
349         return finalManifest;
350     }
351 
352     private void writeManifest( ConcurrentJarCreator zOut, Manifest manifest )
353         throws IOException, ArchiverException
354     {
355         for ( Enumeration e = manifest.getWarnings(); e.hasMoreElements(); )
356         {
357             getLogger().warn( "Manifest warning: " + e.nextElement() );
358         }
359 
360         zipDir( null, zOut, "META-INF/", DEFAULT_DIR_MODE, getEncoding() );
361         // time to write the manifest
362         ByteArrayOutputStream baos = new ByteArrayOutputStream();
363         manifest.write( baos );
364 
365         ByteArrayInputStream bais = new ByteArrayInputStream( baos.toByteArray() );
366         super.zipFile( createInputStreamSupplier( bais ), zOut, MANIFEST_NAME, System.currentTimeMillis(), null,
367                        DEFAULT_FILE_MODE, null, false );
368         super.initZipOutputStream( zOut );
369     }
370 
371     @Override
372     protected void finalizeZipOutputStream( ConcurrentJarCreator zOut )
373         throws IOException, ArchiverException
374     {
375         if ( index )
376         {
377             createIndexList( zOut );
378         }
379     }
380 
381     /**
382      * Create the index list to speed up classloading.
383      * This is a JDK 1.3+ specific feature and is enabled by default. See
384      * <a href="http://java.sun.com/j2se/1.3/docs/guide/jar/jar.html#JAR%20Index">
385      * the JAR index specification</a> for more details.
386      *
387      * @param zOut the zip stream representing the jar being built.
388      *
389      * @throws IOException thrown if there is an error while creating the
390      * index and adding it to the zip stream.
391      * @throws org.codehaus.plexus.archiver.ArchiverException
392      * .
393      */
394     private void createIndexList( ConcurrentJarCreator zOut )
395         throws IOException, ArchiverException
396     {
397         ByteArrayOutputStream baos = new ByteArrayOutputStream();
398         // encoding must be UTF8 as specified in the specs.
399         PrintWriter writer = new PrintWriter( new OutputStreamWriter( baos, "UTF8" ) );
400 
401         // version-info blankline
402         writer.println( "JarIndex-Version: 1.0" );
403         writer.println();
404 
405         // header newline
406         writer.println( getDestFile().getName() );
407 
408         // filter out META-INF if it doesn't contain anything other than the index and manifest.
409         // this is what sun.misc.JarIndex does, guess we ought to be consistent.
410         Set<String> filteredDirs = addedDirs.allAddedDirs();
411         // our added dirs always have a trailing slash
412         if ( filteredDirs.contains( META_INF_NAME + '/' ) )
413         {
414             boolean add = false;
415             for ( String entry : entries.keySet() )
416             {
417                 if ( entry.startsWith( META_INF_NAME + '/' ) && !entry.equals( INDEX_NAME )
418                          && !entry.equals( MANIFEST_NAME ) )
419                 {
420                     add = true;
421                     break;
422                 }
423             }
424             if ( !add )
425             {
426                 filteredDirs.remove( META_INF_NAME + '/' );
427             }
428         }
429         writeIndexLikeList( new ArrayList<String>( filteredDirs ), rootEntries, writer );
430         writer.println();
431 
432         if ( indexJars != null )
433         {
434             java.util.jar.Manifest mf = createManifest();
435             String classpath = mf.getMainAttributes().getValue( ManifestConstants.ATTRIBUTE_CLASSPATH );
436             String[] cpEntries = null;
437             if ( classpath != null )
438             {
439                 StringTokenizer tok = new StringTokenizer( classpath, " " );
440                 cpEntries = new String[ tok.countTokens() ];
441                 int c = 0;
442                 while ( tok.hasMoreTokens() )
443                 {
444                     cpEntries[c++] = tok.nextToken();
445                 }
446             }
447 
448             for ( String indexJar : indexJars )
449             {
450                 String name = findJarName( indexJar, cpEntries );
451                 if ( name != null )
452                 {
453                     ArrayList<String> dirs = new ArrayList<String>();
454                     ArrayList<String> files = new ArrayList<String>();
455                     grabFilesAndDirs( indexJar, dirs, files );
456                     if ( dirs.size() + files.size() > 0 )
457                     {
458                         writer.println( name );
459                         writeIndexLikeList( dirs, files, writer );
460                         writer.println();
461                     }
462                 }
463             }
464         }
465 
466         writer.flush();
467 
468         ByteArrayInputStream bais = new ByteArrayInputStream( baos.toByteArray() );
469 
470         super.zipFile( createInputStreamSupplier( bais ), zOut, INDEX_NAME, System.currentTimeMillis(), null,
471                        DEFAULT_FILE_MODE, null, true );
472     }
473 
474     /**
475      * Overridden from Zip class to deal with manifests and index lists.
476      */
477     @Override
478     protected void zipFile( InputStreamSupplier is, ConcurrentJarCreator zOut, String vPath,
479                             long lastModified, File fromArchive,
480                             int mode, String symlinkDestination, boolean addInParallel )
481         throws IOException, ArchiverException
482     {
483         if ( MANIFEST_NAME.equalsIgnoreCase( vPath ) )
484         {
485             if ( !doubleFilePass || skipWriting )
486             {
487                 try ( InputStream manifestInputStream = is.get() )
488                 {
489                     filesetManifest( fromArchive, manifestInputStream );
490                 }
491             }
492         }
493         else if ( INDEX_NAME.equalsIgnoreCase( vPath ) && index )
494         {
495             getLogger().warn( "Warning: selected " + archiveType + " files include a META-INF/INDEX.LIST which will"
496                                   + " be replaced by a newly generated one." );
497         }
498         else
499         {
500             if ( index && ( !vPath.contains( "/" ) ) )
501             {
502                 rootEntries.addElement( vPath );
503             }
504             super.zipFile( is, zOut, vPath, lastModified, fromArchive, mode, symlinkDestination, addInParallel );
505         }
506     }
507 
508     private void filesetManifest( File file, InputStream is )
509         throws ArchiverException
510     {
511         if ( ( manifestFile != null ) && manifestFile.equals( file ) )
512         {
513             // If this is the same name specified in 'manifest', this
514             // is the manifest to use
515             getLogger().debug( "Found manifest " + file );
516             if ( is != null )
517             {
518                 manifest = getManifest( is );
519             }
520             else
521             {
522                 manifest = getManifest( file );
523             }
524         }
525         else if ( ( filesetManifestConfig != null ) && filesetManifestConfig != FilesetManifestConfig.skip )
526         {
527             // we add this to our group of fileset manifests
528             getLogger().debug( "Found manifest to merge in file " + file );
529 
530             Manifest newManifest;
531             if ( is != null )
532             {
533                 newManifest = getManifest( is );
534             }
535             else
536             {
537                 newManifest = getManifest( file );
538             }
539 
540             if ( filesetManifest == null )
541             {
542                 filesetManifest = newManifest;
543             }
544             else
545             {
546                 JdkManifestFactory.merge( filesetManifest, newManifest, false );
547             }
548         }
549     }
550 
551     @Override
552     protected boolean createEmptyZip( File zipFile )
553         throws ArchiverException
554     {
555         if ( !createEmpty )
556         {
557             return true;
558         }
559 
560         try
561         {
562             getLogger().debug( "Building MANIFEST-only jar: " + getDestFile().getAbsolutePath() );
563             zipArchiveOutputStream =
564                 new ZipArchiveOutputStream( bufferedOutputStream( fileOutputStream( getDestFile(), "jar" ) ) );
565 
566             zipArchiveOutputStream.setEncoding( getEncoding() );
567             if ( isCompress() )
568             {
569                 zipArchiveOutputStream.setMethod( ZipArchiveOutputStream.DEFLATED );
570             }
571             else
572             {
573                 zipArchiveOutputStream.setMethod( ZipArchiveOutputStream.STORED );
574             }
575             ConcurrentJarCreator ps =
576                 new ConcurrentJarCreator( isRecompressAddedZips(), Runtime.getRuntime().availableProcessors() );
577             initZipOutputStream( ps );
578             finalizeZipOutputStream( ps );
579         }
580         catch ( IOException ioe )
581         {
582             throw new ArchiverException( "Could not create almost empty JAR archive (" + ioe.getMessage() + ")", ioe );
583         }
584         finally
585         {
586             // Close the output stream.
587             //IOUtil.close( zOut );
588             createEmpty = false;
589         }
590         return true;
591     }
592 
593     /**
594      * Make sure we don't think we already have a MANIFEST next time this task
595      * gets executed.
596      *
597      * @see ZipArchiver#cleanUp
598      */
599     @Override
600     protected void cleanUp()
601         throws IOException
602     {
603         super.cleanUp();
604 
605         // we want to save this info if we are going to make another pass
606         if ( !doubleFilePass || !skipWriting )
607         {
608             manifest = null;
609             configuredManifest = savedConfiguredManifest;
610             filesetManifest = null;
611             originalManifest = null;
612         }
613         rootEntries.removeAllElements();
614     }
615 
616     /**
617      * reset to default values.
618      *
619      * @see ZipArchiver#reset
620      */
621     @Override
622     public void reset()
623     {
624         super.reset();
625         configuredManifest = null;
626         filesetManifestConfig = null;
627         mergeManifestsMain = false;
628         manifestFile = null;
629         index = false;
630     }
631 
632     public enum FilesetManifestConfig
633     {
634 
635         skip,
636         merge,
637         mergewithoutmain
638 
639     }
640 
641     /**
642      * Writes the directory entries from the first and the filenames
643      * from the second list to the given writer, one entry per line.
644      *
645      * @param dirs The directories
646      * @param files The files
647      * @param writer The printwriter ;)
648      */
649     protected final void writeIndexLikeList( List<String> dirs, List<String> files, PrintWriter writer )
650     {
651         // JarIndex is sorting the directories by ascending order.
652         // it has no value but cosmetic since it will be read into a
653         // hashtable by the classloader, but we'll do so anyway.
654         Collections.sort( dirs );
655         Collections.sort( files );
656         for ( String dir : dirs )
657         {
658             // try to be smart, not to be fooled by a weird directory name
659             dir = dir.replace( '\\', '/' );
660             if ( dir.startsWith( "./" ) )
661             {
662                 dir = dir.substring( 2 );
663             }
664             while ( dir.startsWith( "/" ) )
665             {
666                 dir = dir.substring( 1 );
667             }
668             int pos = dir.lastIndexOf( '/' );
669             if ( pos != -1 )
670             {
671                 dir = dir.substring( 0, pos );
672             }
673 
674             // name newline
675             writer.println( dir );
676         }
677 
678         for ( String file : files )
679         {
680             writer.println( file );
681         }
682     }
683 
684     /**
685      * try to guess the name of the given file.
686      * <p/>
687      * <p>
688      * If this jar has a classpath attribute in its manifest, we
689      * can assume that it will only require an index of jars listed
690      * there. try to find which classpath entry is most likely the
691      * one the given file name points to.</p>
692      * <p/>
693      * <p>
694      * In the absence of a classpath attribute, assume the other
695      * files will be placed inside the same directory as this jar and
696      * use their basename.</p>
697      * <p/>
698      * <p>
699      * if there is a classpath and the given file doesn't match any
700      * of its entries, return null.</p>
701      *
702      * @param fileName .
703      * @param classpath .
704      *
705      * @return The guessed name
706      */
707     protected static String findJarName( String fileName, String[] classpath )
708     {
709         if ( classpath == null )
710         {
711             return new File( fileName ).getName();
712         }
713         fileName = fileName.replace( File.separatorChar, '/' );
714         SortedMap<String, String> matches = new TreeMap<String, String>( new Comparator<String>()
715         {
716 
717             // longest match comes first
718             @Override
719             public int compare( String o1, String o2 )
720             {
721                 if ( ( o1 != null ) && ( o2 != null ) )
722                 {
723                     return o2.length() - o1.length();
724                 }
725                 return 0;
726             }
727 
728         } );
729 
730         for ( String aClasspath : classpath )
731         {
732             if ( fileName.endsWith( aClasspath ) )
733             {
734                 matches.put( aClasspath, aClasspath );
735             }
736             else
737             {
738                 int slash = aClasspath.indexOf( "/" );
739                 String candidate = aClasspath;
740                 while ( slash > -1 )
741                 {
742                     candidate = candidate.substring( slash + 1 );
743                     if ( fileName.endsWith( candidate ) )
744                     {
745                         matches.put( candidate, aClasspath );
746                         break;
747                     }
748                     slash = candidate.indexOf( "/" );
749                 }
750             }
751         }
752 
753         return matches.size() == 0 ? null : matches.get( matches.firstKey() );
754     }
755 
756     /**
757      * Grab lists of all root-level files and all directories
758      * contained in the given archive.
759      *
760      * @param file .
761      * @param files .
762      * @param dirs .
763      *
764      * @throws java.io.IOException .
765      */
766     protected static void grabFilesAndDirs( String file, List<String> dirs, List<String> files )
767         throws IOException
768     {
769         File zipFile = new File( file );
770         if ( !zipFile.exists() )
771         {
772             Logger logger = new ConsoleLogger( Logger.LEVEL_INFO, "console" );
773             logger.error( "JarArchive skipping non-existing file: " + zipFile.getAbsolutePath() );
774         }
775         else if ( zipFile.isDirectory() )
776         {
777             Logger logger = new ConsoleLogger( Logger.LEVEL_INFO, "console" );
778             logger.info( "JarArchiver skipping indexJar " + zipFile + " because it is not a jar" );
779         }
780         else
781         {
782             org.apache.commons.compress.archivers.zip.ZipFile zf = null;
783             try
784             {
785                 zf = new org.apache.commons.compress.archivers.zip.ZipFile( file, "utf-8" );
786                 Enumeration<ZipArchiveEntry> entries = zf.getEntries();
787                 HashSet<String> dirSet = new HashSet<String>();
788                 while ( entries.hasMoreElements() )
789                 {
790                     ZipArchiveEntry ze = entries.nextElement();
791                     String name = ze.getName();
792                     // avoid index for manifest-only jars.
793                     if ( !name.equals( META_INF_NAME ) && !name.equals( META_INF_NAME + '/' ) && !name.equals(
794                         INDEX_NAME ) && !name.equals( MANIFEST_NAME ) )
795                     {
796                         if ( ze.isDirectory() )
797                         {
798                             dirSet.add( name );
799                         }
800                         else if ( !name.contains( "/" ) )
801                         {
802                             files.add( name );
803                         }
804                         else
805                         {
806                             // a file, not in the root
807                             // since the jar may be one without directory
808                             // entries, add the parent dir of this file as
809                             // well.
810                             dirSet.add( name.substring( 0, name.lastIndexOf( "/" ) + 1 ) );
811                         }
812                     }
813                 }
814                 dirs.addAll( dirSet );
815             }
816             finally
817             {
818                 if ( zf != null )
819                 {
820                     zf.close();
821                 }
822             }
823         }
824     }
825 
826 }