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</p>
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     /**
323      * Creates the manifest to be added to the JAR archive.
324      * Sub-classes may choose to override this method
325      * in order to inspect or modify the JAR manifest file.
326      *
327      * @return the manifest for the JAR archive.
328      *
329      * @throws ArchiverException
330      */
331     protected Manifest createManifest()
332         throws ArchiverException
333     {
334         Manifest finalManifest = Manifest.getDefaultManifest();
335 
336         if ( ( manifest == null ) && ( manifestFile != null ) )
337         {
338             // if we haven't got the manifest yet, attempt to
339             // get it now and have manifest be the final merge
340             manifest = getManifest( manifestFile );
341         }
342 
343         /*
344          * Precedence: manifestFile wins over inline manifest,
345          * over manifests read from the filesets over the original
346          * manifest.
347          *
348          * merge with null argument is a no-op
349          */
350         if ( isInUpdateMode() )
351         {
352             JdkManifestFactory.merge( finalManifest, originalManifest, false );
353         }
354         JdkManifestFactory.merge( finalManifest, filesetManifest, false );
355         JdkManifestFactory.merge( finalManifest, configuredManifest, false );
356         JdkManifestFactory.merge( finalManifest, manifest, !mergeManifestsMain );
357 
358         return finalManifest;
359     }
360 
361     private void writeManifest( ConcurrentJarCreator zOut, Manifest manifest )
362         throws IOException, ArchiverException
363     {
364         for ( Enumeration e = manifest.getWarnings(); e.hasMoreElements(); )
365         {
366             getLogger().warn( "Manifest warning: " + e.nextElement() );
367         }
368 
369         zipDir( null, zOut, "META-INF/", DEFAULT_DIR_MODE, getEncoding() );
370         // time to write the manifest
371         ByteArrayOutputStream baos = new ByteArrayOutputStream();
372         manifest.write( baos );
373 
374         ByteArrayInputStream bais = new ByteArrayInputStream( baos.toByteArray() );
375         super.zipFile( createInputStreamSupplier( bais ), zOut, MANIFEST_NAME, System.currentTimeMillis(), null,
376                        DEFAULT_FILE_MODE, null, false );
377         super.initZipOutputStream( zOut );
378     }
379 
380     @Override
381     protected void finalizeZipOutputStream( ConcurrentJarCreator zOut )
382         throws IOException, ArchiverException
383     {
384         if ( index )
385         {
386             createIndexList( zOut );
387         }
388     }
389 
390     /**
391      * Create the index list to speed up classloading.
392      * This is a JDK 1.3+ specific feature and is enabled by default. See
393      * <a href="http://java.sun.com/j2se/1.3/docs/guide/jar/jar.html#JAR%20Index">
394      * the JAR index specification</a> for more details.
395      *
396      * @param zOut the zip stream representing the jar being built.
397      *
398      * @throws IOException thrown if there is an error while creating the
399      * index and adding it to the zip stream.
400      * @throws org.codehaus.plexus.archiver.ArchiverException
401      * .
402      */
403     private void createIndexList( ConcurrentJarCreator zOut )
404         throws IOException, ArchiverException
405     {
406         ByteArrayOutputStream baos = new ByteArrayOutputStream();
407         // encoding must be UTF8 as specified in the specs.
408         PrintWriter writer = new PrintWriter( new OutputStreamWriter( baos, "UTF8" ) );
409 
410         // version-info blankline
411         writer.println( "JarIndex-Version: 1.0" );
412         writer.println();
413 
414         // header newline
415         writer.println( getDestFile().getName() );
416 
417         // filter out META-INF if it doesn't contain anything other than the index and manifest.
418         // this is what sun.misc.JarIndex does, guess we ought to be consistent.
419         Set<String> filteredDirs = addedDirs.allAddedDirs();
420         // our added dirs always have a trailing slash
421         if ( filteredDirs.contains( META_INF_NAME + '/' ) )
422         {
423             boolean add = false;
424             for ( String entry : entries.keySet() )
425             {
426                 if ( entry.startsWith( META_INF_NAME + '/' ) && !entry.equals( INDEX_NAME )
427                          && !entry.equals( MANIFEST_NAME ) )
428                 {
429                     add = true;
430                     break;
431                 }
432             }
433             if ( !add )
434             {
435                 filteredDirs.remove( META_INF_NAME + '/' );
436             }
437         }
438         writeIndexLikeList( new ArrayList<String>( filteredDirs ), rootEntries, writer );
439         writer.println();
440 
441         if ( indexJars != null )
442         {
443             java.util.jar.Manifest mf = createManifest();
444             String classpath = mf.getMainAttributes().getValue( ManifestConstants.ATTRIBUTE_CLASSPATH );
445             String[] cpEntries = null;
446             if ( classpath != null )
447             {
448                 StringTokenizer tok = new StringTokenizer( classpath, " " );
449                 cpEntries = new String[ tok.countTokens() ];
450                 int c = 0;
451                 while ( tok.hasMoreTokens() )
452                 {
453                     cpEntries[c++] = tok.nextToken();
454                 }
455             }
456 
457             for ( String indexJar : indexJars )
458             {
459                 String name = findJarName( indexJar, cpEntries );
460                 if ( name != null )
461                 {
462                     ArrayList<String> dirs = new ArrayList<String>();
463                     ArrayList<String> files = new ArrayList<String>();
464                     grabFilesAndDirs( indexJar, dirs, files );
465                     if ( dirs.size() + files.size() > 0 )
466                     {
467                         writer.println( name );
468                         writeIndexLikeList( dirs, files, writer );
469                         writer.println();
470                     }
471                 }
472             }
473         }
474 
475         writer.flush();
476 
477         ByteArrayInputStream bais = new ByteArrayInputStream( baos.toByteArray() );
478 
479         super.zipFile( createInputStreamSupplier( bais ), zOut, INDEX_NAME, System.currentTimeMillis(), null,
480                        DEFAULT_FILE_MODE, null, true );
481     }
482 
483     /**
484      * Overridden from Zip class to deal with manifests and index lists.
485      */
486     @Override
487     protected void zipFile( InputStreamSupplier is, ConcurrentJarCreator zOut, String vPath,
488                             long lastModified, File fromArchive,
489                             int mode, String symlinkDestination, boolean addInParallel )
490         throws IOException, ArchiverException
491     {
492         if ( MANIFEST_NAME.equalsIgnoreCase( vPath ) )
493         {
494             if ( !doubleFilePass || skipWriting )
495             {
496                 try ( InputStream manifestInputStream = is.get() )
497                 {
498                     filesetManifest( fromArchive, manifestInputStream );
499                 }
500             }
501         }
502         else if ( INDEX_NAME.equalsIgnoreCase( vPath ) && index )
503         {
504             getLogger().warn( "Warning: selected " + archiveType + " files include a META-INF/INDEX.LIST which will"
505                                   + " be replaced by a newly generated one." );
506         }
507         else
508         {
509             if ( index && ( !vPath.contains( "/" ) ) )
510             {
511                 rootEntries.addElement( vPath );
512             }
513             super.zipFile( is, zOut, vPath, lastModified, fromArchive, mode, symlinkDestination, addInParallel );
514         }
515     }
516 
517     private void filesetManifest( File file, InputStream is )
518         throws ArchiverException
519     {
520         if ( ( manifestFile != null ) && manifestFile.equals( file ) )
521         {
522             // If this is the same name specified in 'manifest', this
523             // is the manifest to use
524             getLogger().debug( "Found manifest " + file );
525             if ( is != null )
526             {
527                 manifest = getManifest( is );
528             }
529             else
530             {
531                 manifest = getManifest( file );
532             }
533         }
534         else if ( ( filesetManifestConfig != null ) && filesetManifestConfig != FilesetManifestConfig.skip )
535         {
536             // we add this to our group of fileset manifests
537             getLogger().debug( "Found manifest to merge in file " + file );
538 
539             Manifest newManifest;
540             if ( is != null )
541             {
542                 newManifest = getManifest( is );
543             }
544             else
545             {
546                 newManifest = getManifest( file );
547             }
548 
549             if ( filesetManifest == null )
550             {
551                 filesetManifest = newManifest;
552             }
553             else
554             {
555                 JdkManifestFactory.merge( filesetManifest, newManifest, false );
556             }
557         }
558     }
559 
560     @Override
561     protected boolean createEmptyZip( File zipFile )
562         throws ArchiverException
563     {
564         if ( !createEmpty )
565         {
566             return true;
567         }
568 
569         try
570         {
571             getLogger().debug( "Building MANIFEST-only jar: " + getDestFile().getAbsolutePath() );
572             zipArchiveOutputStream =
573                 new ZipArchiveOutputStream( bufferedOutputStream( fileOutputStream( getDestFile(), "jar" ) ) );
574 
575             zipArchiveOutputStream.setEncoding( getEncoding() );
576             if ( isCompress() )
577             {
578                 zipArchiveOutputStream.setMethod( ZipArchiveOutputStream.DEFLATED );
579             }
580             else
581             {
582                 zipArchiveOutputStream.setMethod( ZipArchiveOutputStream.STORED );
583             }
584             ConcurrentJarCreator ps =
585                 new ConcurrentJarCreator( isRecompressAddedZips(), Runtime.getRuntime().availableProcessors() );
586             initZipOutputStream( ps );
587             finalizeZipOutputStream( ps );
588         }
589         catch ( IOException ioe )
590         {
591             throw new ArchiverException( "Could not create almost empty JAR archive (" + ioe.getMessage() + ")", ioe );
592         }
593         finally
594         {
595             // Close the output stream.
596             //IOUtil.close( zOut );
597             createEmpty = false;
598         }
599         return true;
600     }
601 
602     /**
603      * Make sure we don't think we already have a MANIFEST next time this task
604      * gets executed.
605      *
606      * @see ZipArchiver#cleanUp
607      */
608     @Override
609     protected void cleanUp()
610         throws IOException
611     {
612         super.cleanUp();
613 
614         // we want to save this info if we are going to make another pass
615         if ( !doubleFilePass || !skipWriting )
616         {
617             manifest = null;
618             configuredManifest = savedConfiguredManifest;
619             filesetManifest = null;
620             originalManifest = null;
621         }
622         rootEntries.removeAllElements();
623     }
624 
625     /**
626      * reset to default values.
627      *
628      * @see ZipArchiver#reset
629      */
630     @Override
631     public void reset()
632     {
633         super.reset();
634         configuredManifest = null;
635         filesetManifestConfig = null;
636         mergeManifestsMain = false;
637         manifestFile = null;
638         index = false;
639     }
640 
641     public enum FilesetManifestConfig
642     {
643 
644         skip,
645         merge,
646         mergewithoutmain
647 
648     }
649 
650     /**
651      * Writes the directory entries from the first and the filenames
652      * from the second list to the given writer, one entry per line.
653      *
654      * @param dirs The directories
655      * @param files The files
656      * @param writer The printwriter ;)
657      */
658     protected final void writeIndexLikeList( List<String> dirs, List<String> files, PrintWriter writer )
659     {
660         // JarIndex is sorting the directories by ascending order.
661         // it has no value but cosmetic since it will be read into a
662         // hashtable by the classloader, but we'll do so anyway.
663         Collections.sort( dirs );
664         Collections.sort( files );
665         for ( String dir : dirs )
666         {
667             // try to be smart, not to be fooled by a weird directory name
668             dir = dir.replace( '\\', '/' );
669             if ( dir.startsWith( "./" ) )
670             {
671                 dir = dir.substring( 2 );
672             }
673             while ( dir.startsWith( "/" ) )
674             {
675                 dir = dir.substring( 1 );
676             }
677             int pos = dir.lastIndexOf( '/' );
678             if ( pos != -1 )
679             {
680                 dir = dir.substring( 0, pos );
681             }
682 
683             // name newline
684             writer.println( dir );
685         }
686 
687         for ( String file : files )
688         {
689             writer.println( file );
690         }
691     }
692 
693     /**
694      * try to guess the name of the given file.
695      * <p>
696      * If this jar has a classpath attribute in its manifest, we
697      * can assume that it will only require an index of jars listed
698      * there. try to find which classpath entry is most likely the
699      * one the given file name points to.</p>
700      * <p>
701      * In the absence of a classpath attribute, assume the other
702      * files will be placed inside the same directory as this jar and
703      * use their basename.</p>
704      * <p>
705      * if there is a classpath and the given file doesn't match any
706      * of its entries, return null.</p>
707      *
708      * @param fileName .
709      * @param classpath .
710      *
711      * @return The guessed name
712      */
713     protected static String findJarName( String fileName, String[] classpath )
714     {
715         if ( classpath == null )
716         {
717             return new File( fileName ).getName();
718         }
719         fileName = fileName.replace( File.separatorChar, '/' );
720         SortedMap<String, String> matches = new TreeMap<String, String>( new Comparator<String>()
721         {
722 
723             // longest match comes first
724             @Override
725             public int compare( String o1, String o2 )
726             {
727                 if ( ( o1 != null ) && ( o2 != null ) )
728                 {
729                     return o2.length() - o1.length();
730                 }
731                 return 0;
732             }
733 
734         } );
735 
736         for ( String aClasspath : classpath )
737         {
738             if ( fileName.endsWith( aClasspath ) )
739             {
740                 matches.put( aClasspath, aClasspath );
741             }
742             else
743             {
744                 int slash = aClasspath.indexOf( "/" );
745                 String candidate = aClasspath;
746                 while ( slash > -1 )
747                 {
748                     candidate = candidate.substring( slash + 1 );
749                     if ( fileName.endsWith( candidate ) )
750                     {
751                         matches.put( candidate, aClasspath );
752                         break;
753                     }
754                     slash = candidate.indexOf( "/" );
755                 }
756             }
757         }
758 
759         return matches.size() == 0 ? null : matches.get( matches.firstKey() );
760     }
761 
762     /**
763      * Grab lists of all root-level files and all directories
764      * contained in the given archive.
765      *
766      * @param file .
767      * @param files .
768      * @param dirs .
769      *
770      * @throws java.io.IOException .
771      */
772     protected static void grabFilesAndDirs( String file, List<String> dirs, List<String> files )
773         throws IOException
774     {
775         File zipFile = new File( file );
776         if ( !zipFile.exists() )
777         {
778             Logger logger = new ConsoleLogger( Logger.LEVEL_INFO, "console" );
779             logger.error( "JarArchive skipping non-existing file: " + zipFile.getAbsolutePath() );
780         }
781         else if ( zipFile.isDirectory() )
782         {
783             Logger logger = new ConsoleLogger( Logger.LEVEL_INFO, "console" );
784             logger.info( "JarArchiver skipping indexJar " + zipFile + " because it is not a jar" );
785         }
786         else
787         {
788             org.apache.commons.compress.archivers.zip.ZipFile zf = null;
789             try
790             {
791                 zf = new org.apache.commons.compress.archivers.zip.ZipFile( file, "utf-8" );
792                 Enumeration<ZipArchiveEntry> entries = zf.getEntries();
793                 HashSet<String> dirSet = new HashSet<String>();
794                 while ( entries.hasMoreElements() )
795                 {
796                     ZipArchiveEntry ze = entries.nextElement();
797                     String name = ze.getName();
798                     // avoid index for manifest-only jars.
799                     if ( !name.equals( META_INF_NAME ) && !name.equals( META_INF_NAME + '/' ) && !name.equals(
800                         INDEX_NAME ) && !name.equals( MANIFEST_NAME ) )
801                     {
802                         if ( ze.isDirectory() )
803                         {
804                             dirSet.add( name );
805                         }
806                         else if ( !name.contains( "/" ) )
807                         {
808                             files.add( name );
809                         }
810                         else
811                         {
812                             // a file, not in the root
813                             // since the jar may be one without directory
814                             // entries, add the parent dir of this file as
815                             // well.
816                             dirSet.add( name.substring( 0, name.lastIndexOf( "/" ) + 1 ) );
817                         }
818                     }
819                 }
820                 dirs.addAll( dirSet );
821             }
822             finally
823             {
824                 if ( zf != null )
825                 {
826                     zf.close();
827                 }
828             }
829         }
830     }
831 
832 }