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 */
9package com.vladium.util;
10
11import java.io.BufferedInputStream;
12import java.io.File;
13import java.io.FileInputStream;
14import java.io.FileNotFoundException;
15import java.io.IOException;
16import java.util.ArrayList;
17import java.util.HashSet;
18import java.util.Set;
19import java.util.StringTokenizer;
20import java.util.jar.Attributes;
21import java.util.jar.JarFile;
22import java.util.jar.JarInputStream;
23import java.util.jar.Manifest;
24import java.util.zip.ZipEntry;
25
26import com.vladium.logging.Logger;
27import com.vladium.util.asserts.$assert;
28
29// ----------------------------------------------------------------------------
30/**
31 * @author Vlad Roubtsov, (C) 2003
32 */
33public
34interface 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// ----------------------------------------------------------------------------