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 { handleDirStart(File pathDir, File dir)42 void handleDirStart (File pathDir, File dir); // not generated for path dirs themselves handleFile(File pathDir, File file)43 void handleFile (File pathDir, File file); handleDirEnd(File pathDir, File dir)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 */ handleArchiveStart(File parentDir, File archive, Manifest manifest)50 void handleArchiveStart (File parentDir, File archive, Manifest manifest); 51 handleArchiveEntry(JarInputStream in, ZipEntry entry)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 */ handleArchiveEnd(File parentDir, File archive)58 void handleArchiveEnd (File parentDir, File archive); 59 60 } // end of nested interface 61 62 enumerate()63 void enumerate () throws IOException; 64 65 66 public static abstract class Factory 67 { create(final File [] path, final boolean canonical, final IPathHandler handler)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 { enumerate()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 PathEnumerator(final File [] path, final boolean canonical, final IPathHandler handler)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 enumeratePathDir(final String dir)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 enumeratePathArchive(final String archive)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 readManifestViaJarFile(final File archive)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 // ----------------------------------------------------------------------------