| /* Copyright (C) 2003 Vladimir Roubtsov. All rights reserved. |
| * |
| * This program and the accompanying materials are made available under |
| * the terms of the Common Public License v1.0 which accompanies this distribution, |
| * and is available at http://www.eclipse.org/legal/cpl-v10.html |
| * |
| * $Id: IPathEnumerator.java,v 1.1.1.1.2.1 2004/07/16 23:32:04 vlad_r Exp $ |
| */ |
| package com.vladium.util; |
| |
| import java.io.BufferedInputStream; |
| import java.io.File; |
| import java.io.FileInputStream; |
| import java.io.FileNotFoundException; |
| import java.io.IOException; |
| import java.util.ArrayList; |
| import java.util.HashSet; |
| import java.util.Set; |
| import java.util.StringTokenizer; |
| import java.util.jar.Attributes; |
| import java.util.jar.JarFile; |
| import java.util.jar.JarInputStream; |
| import java.util.jar.Manifest; |
| import java.util.zip.ZipEntry; |
| |
| import com.vladium.logging.Logger; |
| import com.vladium.util.asserts.$assert; |
| |
| // ---------------------------------------------------------------------------- |
| /** |
| * @author Vlad Roubtsov, (C) 2003 |
| */ |
| public |
| interface IPathEnumerator |
| { |
| // public: ................................................................ |
| |
| // TODO: archives inside archives? (.war ?) |
| |
| public static interface IPathHandler |
| { |
| void handleDirStart (File pathDir, File dir); // not generated for path dirs themselves |
| void handleFile (File pathDir, File file); |
| void handleDirEnd (File pathDir, File dir); |
| |
| /** |
| * Called just after the enumerator's zip input stream for this archive |
| * is opened and the manifest entry is read. |
| */ |
| void handleArchiveStart (File parentDir, File archive, Manifest manifest); |
| |
| void handleArchiveEntry (JarInputStream in, ZipEntry entry); |
| |
| /** |
| * Called after the enumerator's zip input stream for this archive |
| * has been closed. |
| */ |
| void handleArchiveEnd (File parentDir, File archive); |
| |
| } // end of nested interface |
| |
| |
| void enumerate () throws IOException; |
| |
| |
| public static abstract class Factory |
| { |
| public static IPathEnumerator create (final File [] path, final boolean canonical, final IPathHandler handler) |
| { |
| return new PathEnumerator (path, canonical, handler); |
| } |
| |
| private static final class PathEnumerator implements IPathEnumerator |
| { |
| public void enumerate () throws IOException |
| { |
| final IPathHandler handler = m_handler; |
| |
| for (m_pathIndex = 0; m_pathIndex < m_path.size (); ++ m_pathIndex) // important not to cache m_path.size() |
| { |
| final File f = (File) m_path.get (m_pathIndex); |
| |
| if (! f.exists ()) |
| { |
| if (IGNORE_INVALID_ENTRIES) |
| continue; |
| else |
| throw new IllegalArgumentException ("path entry does not exist: [" + f + "]"); |
| } |
| |
| |
| if (f.isDirectory ()) |
| { |
| if (m_verbose) m_log.verbose ("processing dir path entry [" + f.getAbsolutePath () + "] ..."); |
| |
| m_currentPathDir = f; |
| enumeratePathDir (null); |
| } |
| else |
| { |
| final String name = f.getName (); |
| final String lcName = name.toLowerCase (); |
| |
| if (lcName.endsWith (".zip") || lcName.endsWith (".jar")) |
| { |
| if (m_verbose) m_log.verbose ("processing archive path entry [" + f.getAbsolutePath () + "] ..."); |
| |
| final File parent = f.getParentFile (); // could be null |
| final File archive = new File (name); |
| m_currentPathDir = parent; |
| |
| // move to enumeratePathArchive(): handler.handleArchiveStart (parent, archive); |
| enumeratePathArchive (name); |
| handler.handleArchiveEnd (parent, archive); // note: it is important that this is called after the zip stream has been closed |
| } |
| else if (! IGNORE_INVALID_ENTRIES) |
| { |
| throw new IllegalArgumentException ("path entry is not a directory or an archive: [" + f + "]"); |
| } |
| } |
| } |
| } |
| |
| PathEnumerator (final File [] path, final boolean canonical, final IPathHandler handler) |
| { |
| m_path = new ArrayList (path.length); |
| for (int p = 0; p < path.length; ++ p) m_path.add (path [p]); |
| |
| m_canonical = canonical; |
| |
| if (handler == null) throw new IllegalArgumentException ("null input: handler"); |
| m_handler = handler; |
| |
| m_processManifest = true; // TODO |
| |
| if (m_processManifest) |
| { |
| m_pathSet = new HashSet (path.length); |
| for (int p = 0; p < path.length; ++ p) |
| { |
| m_pathSet.add (path [p].getPath ()); // set of [possibly canonical] paths |
| } |
| } |
| else |
| { |
| m_pathSet = null; |
| } |
| |
| m_log = Logger.getLogger (); // each path enumerator caches its logger at creation time |
| m_verbose = m_log.atVERBOSE (); |
| m_trace1 = m_log.atTRACE1 (); |
| } |
| |
| |
| private void enumeratePathDir (final String dir) |
| throws IOException |
| { |
| final boolean trace1 = m_trace1; |
| |
| final File currentPathDir = m_currentPathDir; |
| final File fullDir = dir != null ? new File (currentPathDir, dir) : currentPathDir; |
| |
| final String [] children = fullDir.list (); |
| final IPathHandler handler = m_handler; |
| |
| for (int c = 0, cLimit = children.length; c < cLimit; ++ c) |
| { |
| final String childName = children [c]; |
| |
| final File child = dir != null ? new File (dir, childName) : new File (childName); |
| final File fullChild = new File (fullDir, childName); |
| |
| if (fullChild.isDirectory ()) |
| { |
| handler.handleDirStart (currentPathDir, child); |
| if (trace1) m_log.trace1 ("enumeratePathDir", "recursing into [" + child.getName () + "] ..."); |
| enumeratePathDir (child.getPath ()); |
| handler.handleDirEnd (currentPathDir, child); |
| } |
| else |
| { |
| // final String lcName = childName.toLowerCase (); |
| // |
| // if (lcName.endsWith (".zip") || lcName.endsWith (".jar")) |
| // { |
| // handler.handleArchiveStart (currentPathDir, child); |
| // enumeratePathArchive (child.getPath ()); |
| // handler.handleArchiveEnd (currentPathDir, child); |
| // } |
| // else |
| { |
| if (trace1) m_log.trace1 ("enumeratePathDir", "processing file [" + child.getName () + "] ..."); |
| handler.handleFile (currentPathDir, child); |
| } |
| } |
| } |
| } |
| |
| private void enumeratePathArchive (final String archive) |
| throws IOException |
| { |
| final boolean trace1 = m_trace1; |
| |
| final File fullArchive = new File (m_currentPathDir, archive); |
| |
| JarInputStream in = null; |
| try |
| { |
| // note: Sun's JarFile uses native code and has been known to |
| // crash the JVM in some builds; however, it uses random file |
| // access and can find "bad" manifests that are not the first |
| // entries in their archives (which JarInputStream can't do); |
| // [bugs: 4263225, 4696354, 4338238] |
| // |
| // there is really no good solution here but as a compromise |
| // I try to read the manifest again via a JarFile if the stream |
| // returns null for it: |
| |
| in = new JarInputStream (new BufferedInputStream (new FileInputStream (fullArchive), 32 * 1024)); |
| |
| final IPathHandler handler = m_handler; |
| |
| Manifest manifest = in.getManifest (); // can be null |
| if (manifest == null) manifest = readManifestViaJarFile (fullArchive); // can be null |
| |
| handler.handleArchiveStart (m_currentPathDir, new File (archive), manifest); |
| |
| // note: this loop does not skip over the manifest-related |
| // entries [the handler needs to be smart about that] |
| for (ZipEntry entry; (entry = in.getNextEntry ()) != null; ) |
| { |
| // TODO: handle nested archives |
| |
| if (trace1) m_log.trace1 ("enumeratePathArchive", "processing archive entry [" + entry.getName () + "] ..."); |
| handler.handleArchiveEntry (in, entry); |
| in.closeEntry (); |
| } |
| |
| |
| // TODO: this needs major testing |
| if (m_processManifest) |
| { |
| // note: JarInputStream only reads the manifest if it the |
| // first jar entry |
| if (manifest == null) manifest = in.getManifest (); |
| if (manifest != null) |
| { |
| final Attributes attributes = manifest.getMainAttributes (); |
| if (attributes != null) |
| { |
| // note: Sun's documentation says that multiple Class-Path: |
| // entries are merged sequentially (http://java.sun.com/products/jdk/1.2/docs/guide/extensions/spec.html) |
| // however, their own code does not implement this |
| final String jarClassPath = attributes.getValue (Attributes.Name.CLASS_PATH); |
| if (jarClassPath != null) |
| { |
| final StringTokenizer tokenizer = new StringTokenizer (jarClassPath); |
| for (int p = 1; tokenizer.hasMoreTokens (); ) |
| { |
| final String relPath = tokenizer.nextToken (); |
| |
| final File archiveParent = fullArchive.getParentFile (); |
| final File path = archiveParent != null ? new File (archiveParent, relPath) : new File (relPath); |
| |
| final String fullPath = m_canonical ? Files.canonicalizePathname (path.getPath ()) : path.getPath (); |
| |
| if (m_pathSet.add (fullPath)) |
| { |
| if (m_verbose) m_log.verbose (" added manifest Class-Path entry [" + path + "]"); |
| m_path.add (m_pathIndex + (p ++), path); // insert after the current m_path entry |
| } |
| } |
| } |
| } |
| } |
| } |
| } |
| catch (FileNotFoundException fnfe) // ignore: this should not happen |
| { |
| if ($assert.ENABLED) throw fnfe; |
| } |
| finally |
| { |
| if (in != null) try { in.close (); } catch (Exception ignore) {} |
| } |
| } |
| |
| |
| // see comments at the start of enumeratePathArchive() |
| |
| private static Manifest readManifestViaJarFile (final File archive) |
| { |
| Manifest result = null; |
| |
| JarFile jarfile = null; |
| try |
| { |
| jarfile = new JarFile (archive, false); // 3-arg constructor is not in J2SE 1.2 |
| result = jarfile.getManifest (); |
| } |
| catch (IOException ignore) |
| { |
| } |
| finally |
| { |
| if (jarfile != null) try { jarfile.close (); } catch (IOException ignore) {} |
| } |
| |
| return result; |
| } |
| |
| |
| private final ArrayList /* File */ m_path; |
| private final boolean m_canonical; |
| private final Set /* String */ m_pathSet; |
| private final IPathHandler m_handler; |
| private final boolean m_processManifest; |
| |
| private final Logger m_log; |
| private boolean m_verbose, m_trace1; |
| |
| private int m_pathIndex; |
| private File m_currentPathDir; |
| |
| // if 'true', non-existent or non-archive or non-directory path entries |
| // will be silently ignored: |
| private static final boolean IGNORE_INVALID_ENTRIES = true; // this is consistent with the normal JVM behavior |
| |
| } // end of nested class |
| |
| } // end of nested class |
| |
| } // end of interface |
| // ---------------------------------------------------------------------------- |