Home | History | Annotate | Download | only in util
      1 /* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved.
      2  *
      3  * This program and the accompanying materials are made available under
      4  * the terms of the Common Public License v1.0 which accompanies this distribution,
      5  * and is available at http://www.eclipse.org/legal/cpl-v10.html
      6  *
      7  * $Id: IPathEnumerator.java,v 1.1.1.1.2.1 2004/07/16 23:32:04 vlad_r Exp $
      8  */
      9 package com.vladium.util;
     10 
     11 import java.io.BufferedInputStream;
     12 import java.io.File;
     13 import java.io.FileInputStream;
     14 import java.io.FileNotFoundException;
     15 import java.io.IOException;
     16 import java.util.ArrayList;
     17 import java.util.HashSet;
     18 import java.util.Set;
     19 import java.util.StringTokenizer;
     20 import java.util.jar.Attributes;
     21 import java.util.jar.JarFile;
     22 import java.util.jar.JarInputStream;
     23 import java.util.jar.Manifest;
     24 import java.util.zip.ZipEntry;
     25 
     26 import com.vladium.logging.Logger;
     27 import com.vladium.util.asserts.$assert;
     28 
     29 // ----------------------------------------------------------------------------
     30 /**
     31  * @author Vlad Roubtsov, (C) 2003
     32  */
     33 public
     34 interface IPathEnumerator
     35 {
     36     // public: ................................................................
     37 
     38     // TODO: archives inside archives? (.war ?)
     39 
     40     public static interface IPathHandler
     41     {
     42         void handleDirStart (File pathDir, File dir); // not generated for path dirs themselves
     43         void handleFile (File pathDir, File file);
     44         void handleDirEnd (File pathDir, File dir);
     45 
     46         /**
     47          * Called just after the enumerator's zip input stream for this archive
     48          * is opened and the manifest entry is read.
     49          */
     50         void handleArchiveStart (File parentDir, File archive, Manifest manifest);
     51 
     52         void handleArchiveEntry (JarInputStream in, ZipEntry entry);
     53 
     54         /**
     55          * Called after the enumerator's zip input stream for this archive
     56          * has been closed.
     57          */
     58         void handleArchiveEnd (File parentDir, File archive);
     59 
     60     } // end of nested interface
     61 
     62 
     63     void enumerate () throws IOException;
     64 
     65 
     66     public static abstract class Factory
     67     {
     68         public static IPathEnumerator create (final File [] path, final boolean canonical, final IPathHandler handler)
     69         {
     70             return new PathEnumerator (path, canonical, handler);
     71         }
     72 
     73         private static final class PathEnumerator implements IPathEnumerator
     74         {
     75             public void enumerate () throws IOException
     76             {
     77                 final IPathHandler handler = m_handler;
     78 
     79                 for (m_pathIndex = 0; m_pathIndex < m_path.size (); ++ m_pathIndex) // important not to cache m_path.size()
     80                 {
     81                     final File f = (File) m_path.get (m_pathIndex);
     82 
     83                     if (! f.exists ())
     84                     {
     85                         if (IGNORE_INVALID_ENTRIES)
     86                             continue;
     87                         else
     88                             throw new IllegalArgumentException ("path entry does not exist: [" + f + "]");
     89                     }
     90 
     91 
     92                     if (f.isDirectory ())
     93                     {
     94                         if (m_verbose) m_log.verbose ("processing dir path entry [" + f.getAbsolutePath () + "] ...");
     95 
     96                         m_currentPathDir = f;
     97                         enumeratePathDir (null);
     98                     }
     99                     else
    100                     {
    101                         final String name = f.getName ();
    102                         final String lcName = name.toLowerCase ();
    103 
    104                         if (lcName.endsWith (".zip") || lcName.endsWith (".jar"))
    105                         {
    106                             if (m_verbose) m_log.verbose ("processing archive path entry [" + f.getAbsolutePath () + "] ...");
    107 
    108                             final File parent = f.getParentFile (); // could be null
    109                             final File archive = new File (name);
    110                             m_currentPathDir = parent;
    111 
    112                             // move to enumeratePathArchive(): handler.handleArchiveStart (parent, archive);
    113                             enumeratePathArchive (name);
    114                             handler.handleArchiveEnd (parent, archive); // note: it is important that this is called after the zip stream has been closed
    115                         }
    116                         else if (! IGNORE_INVALID_ENTRIES)
    117                         {
    118                             throw new IllegalArgumentException ("path entry is not a directory or an archive: [" + f + "]");
    119                         }
    120                     }
    121                 }
    122             }
    123 
    124             PathEnumerator (final File [] path, final boolean canonical, final IPathHandler handler)
    125             {
    126                 m_path = new ArrayList (path.length);
    127                 for (int p = 0; p < path.length; ++ p) m_path.add (path [p]);
    128 
    129                 m_canonical = canonical;
    130 
    131                 if (handler == null) throw new IllegalArgumentException ("null input: handler");
    132                 m_handler = handler;
    133 
    134                 m_processManifest = true; // TODO
    135 
    136                 if (m_processManifest)
    137                 {
    138                     m_pathSet = new HashSet (path.length);
    139                     for (int p = 0; p < path.length; ++ p)
    140                     {
    141                         m_pathSet.add (path [p].getPath ()); // set of [possibly canonical] paths
    142                     }
    143                 }
    144                 else
    145                 {
    146                     m_pathSet = null;
    147                 }
    148 
    149                 m_log = Logger.getLogger (); // each path enumerator caches its logger at creation time
    150                 m_verbose = m_log.atVERBOSE ();
    151                 m_trace1 = m_log.atTRACE1 ();
    152             }
    153 
    154 
    155             private void enumeratePathDir (final String dir)
    156                 throws IOException
    157             {
    158                 final boolean trace1 = m_trace1;
    159 
    160                 final File currentPathDir = m_currentPathDir;
    161                 final File fullDir = dir != null ? new File (currentPathDir, dir) : currentPathDir;
    162 
    163                 final String [] children = fullDir.list ();
    164                 final IPathHandler handler = m_handler;
    165 
    166                 for (int c = 0, cLimit = children.length; c < cLimit; ++ c)
    167                 {
    168                     final String childName = children [c];
    169 
    170                     final File child = dir != null ? new File (dir, childName) : new File (childName);
    171                     final File fullChild = new File (fullDir, childName);
    172 
    173                     if (fullChild.isDirectory ())
    174                     {
    175                         handler.handleDirStart (currentPathDir, child);
    176                         if (trace1) m_log.trace1 ("enumeratePathDir", "recursing into [" + child.getName () + "] ...");
    177                         enumeratePathDir (child.getPath ());
    178                         handler.handleDirEnd (currentPathDir, child);
    179                     }
    180                     else
    181                     {
    182 //                        final String lcName = childName.toLowerCase ();
    183 //
    184 //                        if (lcName.endsWith (".zip") || lcName.endsWith (".jar"))
    185 //                        {
    186 //                            handler.handleArchiveStart (currentPathDir, child);
    187 //                            enumeratePathArchive (child.getPath ());
    188 //                            handler.handleArchiveEnd (currentPathDir, child);
    189 //                        }
    190 //                        else
    191                         {
    192                             if (trace1) m_log.trace1 ("enumeratePathDir", "processing file [" + child.getName () + "] ...");
    193                             handler.handleFile (currentPathDir, child);
    194                         }
    195                     }
    196                 }
    197             }
    198 
    199             private void enumeratePathArchive (final String archive)
    200                 throws IOException
    201             {
    202                 final boolean trace1 = m_trace1;
    203 
    204                 final File fullArchive = new File (m_currentPathDir, archive);
    205 
    206                 JarInputStream in = null;
    207                 try
    208                 {
    209                     // note: Sun's JarFile uses native code and has been known to
    210                     // crash the JVM in some builds; however, it uses random file
    211                     // access and can find "bad" manifests that are not the first
    212                     // entries in their archives (which JarInputStream can't do);
    213                     // [bugs: 4263225, 4696354, 4338238]
    214                     //
    215                     // there is really no good solution here but as a compromise
    216                     // I try to read the manifest again via a JarFile if the stream
    217                     // returns null for it:
    218 
    219                     in = new JarInputStream (new BufferedInputStream (new FileInputStream (fullArchive), 32 * 1024));
    220 
    221                     final IPathHandler handler = m_handler;
    222 
    223                     Manifest manifest = in.getManifest (); // can be null
    224                     if (manifest == null) manifest = readManifestViaJarFile (fullArchive); // can be null
    225 
    226                     handler.handleArchiveStart (m_currentPathDir, new File (archive), manifest);
    227 
    228                     // note: this loop does not skip over the manifest-related
    229                     // entries [the handler needs to be smart about that]
    230                     for (ZipEntry entry; (entry = in.getNextEntry ()) != null; )
    231                     {
    232                         // TODO: handle nested archives
    233 
    234                         if (trace1) m_log.trace1 ("enumeratePathArchive", "processing archive entry [" + entry.getName () + "] ...");
    235                         handler.handleArchiveEntry (in, entry);
    236                         in.closeEntry ();
    237                     }
    238 
    239 
    240                     // TODO: this needs major testing
    241                     if (m_processManifest)
    242                     {
    243                         // note: JarInputStream only reads the manifest if it the
    244                         // first jar entry
    245                         if (manifest == null) manifest = in.getManifest ();
    246                         if (manifest != null)
    247                         {
    248                             final Attributes attributes = manifest.getMainAttributes ();
    249                             if (attributes != null)
    250                             {
    251                                 // note: Sun's documentation says that multiple Class-Path:
    252                                 // entries are merged sequentially (http://java.sun.com/products/jdk/1.2/docs/guide/extensions/spec.html)
    253                                 // however, their own code does not implement this
    254                                 final String jarClassPath = attributes.getValue (Attributes.Name.CLASS_PATH);
    255                                 if (jarClassPath != null)
    256                                 {
    257                                     final StringTokenizer tokenizer = new StringTokenizer (jarClassPath);
    258                                     for (int p = 1; tokenizer.hasMoreTokens (); )
    259                                     {
    260                                         final String relPath = tokenizer.nextToken ();
    261 
    262                                         final File archiveParent = fullArchive.getParentFile ();
    263                                         final File path = archiveParent != null ? new File (archiveParent, relPath) : new File (relPath);
    264 
    265                                         final String fullPath = m_canonical ? Files.canonicalizePathname (path.getPath ()) : path.getPath ();
    266 
    267                                         if (m_pathSet.add (fullPath))
    268                                         {
    269                                             if (m_verbose) m_log.verbose ("  added manifest Class-Path entry [" + path + "]");
    270                                             m_path.add (m_pathIndex + (p ++), path); // insert after the current m_path entry
    271                                         }
    272                                     }
    273                                 }
    274                             }
    275                         }
    276                     }
    277                 }
    278                 catch (FileNotFoundException fnfe) // ignore: this should not happen
    279                 {
    280                     if ($assert.ENABLED) throw fnfe;
    281                 }
    282                 finally
    283                 {
    284                     if (in != null) try { in.close (); } catch (Exception ignore) {}
    285                 }
    286             }
    287 
    288 
    289             // see comments at the start of enumeratePathArchive()
    290 
    291             private static Manifest readManifestViaJarFile (final File archive)
    292             {
    293                 Manifest result = null;
    294 
    295                 JarFile jarfile = null;
    296                 try
    297                 {
    298                     jarfile = new JarFile (archive, false); // 3-arg constructor is not in J2SE 1.2
    299                     result = jarfile.getManifest ();
    300                 }
    301                 catch (IOException ignore)
    302                 {
    303                 }
    304                 finally
    305                 {
    306                     if (jarfile != null) try { jarfile.close (); } catch (IOException ignore) {}
    307                 }
    308 
    309                 return result;
    310             }
    311 
    312 
    313             private final ArrayList /* File */ m_path;
    314             private final boolean m_canonical;
    315             private final Set /* String */ m_pathSet;
    316             private final IPathHandler m_handler;
    317             private final boolean m_processManifest;
    318 
    319             private final Logger m_log;
    320             private boolean m_verbose, m_trace1;
    321 
    322             private int m_pathIndex;
    323             private File m_currentPathDir;
    324 
    325             // if 'true', non-existent or non-archive or non-directory path entries
    326             // will be silently ignored:
    327             private static final boolean IGNORE_INVALID_ENTRIES = true; // this is consistent with the normal JVM behavior
    328 
    329         } // end of nested class
    330 
    331     } // end of nested class
    332 
    333 } // end of interface
    334 // ----------------------------------------------------------------------------