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                 filesetManifest( fromArchive, is.get() );
488             }
489         }
490         else if ( INDEX_NAME.equalsIgnoreCase( vPath ) && index )
491         {
492             getLogger().warn( "Warning: selected " + archiveType + " files include a META-INF/INDEX.LIST which will"
493                                   + " be replaced by a newly generated one." );
494         }
495         else
496         {
497             if ( index && ( !vPath.contains( "/" ) ) )
498             {
499                 rootEntries.addElement( vPath );
500             }
501             super.zipFile( is, zOut, vPath, lastModified, fromArchive, mode, symlinkDestination, addInParallel );
502         }
503     }
504 
505     private void filesetManifest( File file, InputStream is )
506         throws ArchiverException
507     {
508         if ( ( manifestFile != null ) && manifestFile.equals( file ) )
509         {
510             // If this is the same name specified in 'manifest', this
511             // is the manifest to use
512             getLogger().debug( "Found manifest " + file );
513             if ( is != null )
514             {
515                 manifest = getManifest( is );
516             }
517             else
518             {
519                 manifest = getManifest( file );
520             }
521         }
522         else if ( ( filesetManifestConfig != null ) && filesetManifestConfig != FilesetManifestConfig.skip )
523         {
524             // we add this to our group of fileset manifests
525             getLogger().debug( "Found manifest to merge in file " + file );
526 
527             Manifest newManifest;
528             if ( is != null )
529             {
530                 newManifest = getManifest( is );
531             }
532             else
533             {
534                 newManifest = getManifest( file );
535             }
536 
537             if ( filesetManifest == null )
538             {
539                 filesetManifest = newManifest;
540             }
541             else
542             {
543                 JdkManifestFactory.merge( filesetManifest, newManifest, false );
544             }
545         }
546     }
547 
548     @Override
549     protected boolean createEmptyZip( File zipFile )
550         throws ArchiverException
551     {
552         if ( !createEmpty )
553         {
554             return true;
555         }
556 
557         try
558         {
559             getLogger().debug( "Building MANIFEST-only jar: " + getDestFile().getAbsolutePath() );
560             zipArchiveOutputStream =
561                 new ZipArchiveOutputStream( bufferedOutputStream( fileOutputStream( getDestFile(), "jar" ) ) );
562 
563             zipArchiveOutputStream.setEncoding( getEncoding() );
564             if ( isCompress() )
565             {
566                 zipArchiveOutputStream.setMethod( ZipArchiveOutputStream.DEFLATED );
567             }
568             else
569             {
570                 zipArchiveOutputStream.setMethod( ZipArchiveOutputStream.STORED );
571             }
572             ConcurrentJarCreator ps =
573                 new ConcurrentJarCreator( isRecompressAddedZips(), Runtime.getRuntime().availableProcessors() );
574             initZipOutputStream( ps );
575             finalizeZipOutputStream( ps );
576         }
577         catch ( IOException ioe )
578         {
579             throw new ArchiverException( "Could not create almost empty JAR archive (" + ioe.getMessage() + ")", ioe );
580         }
581         finally
582         {
583             // Close the output stream.
584             //IOUtil.close( zOut );
585             createEmpty = false;
586         }
587         return true;
588     }
589 
590     /**
591      * Make sure we don't think we already have a MANIFEST next time this task
592      * gets executed.
593      *
594      * @see ZipArchiver#cleanUp
595      */
596     @Override
597     protected void cleanUp()
598         throws IOException
599     {
600         super.cleanUp();
601 
602         // we want to save this info if we are going to make another pass
603         if ( !doubleFilePass || !skipWriting )
604         {
605             manifest = null;
606             configuredManifest = savedConfiguredManifest;
607             filesetManifest = null;
608             originalManifest = null;
609         }
610         rootEntries.removeAllElements();
611     }
612 
613     /**
614      * reset to default values.
615      *
616      * @see ZipArchiver#reset
617      */
618     @Override
619     public void reset()
620     {
621         super.reset();
622         configuredManifest = null;
623         filesetManifestConfig = null;
624         mergeManifestsMain = false;
625         manifestFile = null;
626         index = false;
627     }
628 
629     public enum FilesetManifestConfig
630     {
631 
632         skip,
633         merge,
634         mergewithoutmain
635 
636     }
637 
638     /**
639      * Writes the directory entries from the first and the filenames
640      * from the second list to the given writer, one entry per line.
641      *
642      * @param dirs The directories
643      * @param files The files
644      * @param writer The printwriter ;)
645      */
646     protected final void writeIndexLikeList( List<String> dirs, List<String> files, PrintWriter writer )
647     {
648         // JarIndex is sorting the directories by ascending order.
649         // it has no value but cosmetic since it will be read into a
650         // hashtable by the classloader, but we'll do so anyway.
651         Collections.sort( dirs );
652         Collections.sort( files );
653         for ( String dir : dirs )
654         {
655             // try to be smart, not to be fooled by a weird directory name
656             dir = dir.replace( '\\', '/' );
657             if ( dir.startsWith( "./" ) )
658             {
659                 dir = dir.substring( 2 );
660             }
661             while ( dir.startsWith( "/" ) )
662             {
663                 dir = dir.substring( 1 );
664             }
665             int pos = dir.lastIndexOf( '/' );
666             if ( pos != -1 )
667             {
668                 dir = dir.substring( 0, pos );
669             }
670 
671             // name newline
672             writer.println( dir );
673         }
674 
675         for ( String file : files )
676         {
677             writer.println( file );
678         }
679     }
680 
681     /**
682      * try to guess the name of the given file.
683      * <p/>
684      * <p>
685      * If this jar has a classpath attribute in its manifest, we
686      * can assume that it will only require an index of jars listed
687      * there. try to find which classpath entry is most likely the
688      * one the given file name points to.</p>
689      * <p/>
690      * <p>
691      * In the absence of a classpath attribute, assume the other
692      * files will be placed inside the same directory as this jar and
693      * use their basename.</p>
694      * <p/>
695      * <p>
696      * if there is a classpath and the given file doesn't match any
697      * of its entries, return null.</p>
698      *
699      * @param fileName .
700      * @param classpath .
701      *
702      * @return The guessed name
703      */
704     protected static String findJarName( String fileName, String[] classpath )
705     {
706         if ( classpath == null )
707         {
708             return new File( fileName ).getName();
709         }
710         fileName = fileName.replace( File.separatorChar, '/' );
711         SortedMap<String, String> matches = new TreeMap<String, String>( new Comparator<String>()
712         {
713 
714             // longest match comes first
715             @Override
716             public int compare( String o1, String o2 )
717             {
718                 if ( ( o1 != null ) && ( o2 != null ) )
719                 {
720                     return o2.length() - o1.length();
721                 }
722                 return 0;
723             }
724 
725         } );
726 
727         for ( String aClasspath : classpath )
728         {
729             if ( fileName.endsWith( aClasspath ) )
730             {
731                 matches.put( aClasspath, aClasspath );
732             }
733             else
734             {
735                 int slash = aClasspath.indexOf( "/" );
736                 String candidate = aClasspath;
737                 while ( slash > -1 )
738                 {
739                     candidate = candidate.substring( slash + 1 );
740                     if ( fileName.endsWith( candidate ) )
741                     {
742                         matches.put( candidate, aClasspath );
743                         break;
744                     }
745                     slash = candidate.indexOf( "/" );
746                 }
747             }
748         }
749 
750         return matches.size() == 0 ? null : matches.get( matches.firstKey() );
751     }
752 
753     /**
754      * Grab lists of all root-level files and all directories
755      * contained in the given archive.
756      *
757      * @param file .
758      * @param files .
759      * @param dirs .
760      *
761      * @throws java.io.IOException .
762      */
763     protected static void grabFilesAndDirs( String file, List<String> dirs, List<String> files )
764         throws IOException
765     {
766         File zipFile = new File( file );
767         if ( !zipFile.exists() )
768         {
769             Logger logger = new ConsoleLogger( Logger.LEVEL_INFO, "console" );
770             logger.error( "JarArchive skipping non-existing file: " + zipFile.getAbsolutePath() );
771         }
772         else if ( zipFile.isDirectory() )
773         {
774             Logger logger = new ConsoleLogger( Logger.LEVEL_INFO, "console" );
775             logger.info( "JarArchiver skipping indexJar " + zipFile + " because it is not a jar" );
776         }
777         else
778         {
779             org.apache.commons.compress.archivers.zip.ZipFile zf = null;
780             try
781             {
782                 zf = new org.apache.commons.compress.archivers.zip.ZipFile( file, "utf-8" );
783                 Enumeration<ZipArchiveEntry> entries = zf.getEntries();
784                 HashSet<String> dirSet = new HashSet<String>();
785                 while ( entries.hasMoreElements() )
786                 {
787                     ZipArchiveEntry ze = entries.nextElement();
788                     String name = ze.getName();
789                     // avoid index for manifest-only jars.
790                     if ( !name.equals( META_INF_NAME ) && !name.equals( META_INF_NAME + '/' ) && !name.equals(
791                         INDEX_NAME ) && !name.equals( MANIFEST_NAME ) )
792                     {
793                         if ( ze.isDirectory() )
794                         {
795                             dirSet.add( name );
796                         }
797                         else if ( !name.contains( "/" ) )
798                         {
799                             files.add( name );
800                         }
801                         else
802                         {
803                             // a file, not in the root
804                             // since the jar may be one without directory
805                             // entries, add the parent dir of this file as
806                             // well.
807                             dirSet.add( name.substring( 0, name.lastIndexOf( "/" ) + 1 ) );
808                         }
809                     }
810                 }
811                 dirs.addAll( dirSet );
812             }
813             finally
814             {
815                 if ( zf != null )
816                 {
817                     zf.close();
818                 }
819             }
820         }
821     }
822 
823 }