View Javadoc
1   package com.randomnoun.common;
2   
3   /* (c) 2013 randomnoun. All Rights Reserved. This work is licensed under a
4    * BSD Simplified License. (http://www.randomnoun.com/bsd-simplified.html)
5    */
6   
7   import java.io.BufferedReader;
8   import java.io.EOFException;
9   import java.io.File;
10  import java.io.FileInputStream;
11  import java.io.FileNotFoundException;
12  import java.io.FileOutputStream;
13  import java.io.IOException;
14  import java.io.InputStream;
15  import java.io.InputStreamReader;
16  import java.io.LineNumberReader;
17  import java.security.MessageDigest;
18  import java.security.NoSuchAlgorithmException;
19  import java.text.SimpleDateFormat;
20  import java.util.ArrayList;
21  import java.util.Date;
22  import java.util.List;
23  import java.util.regex.Pattern;
24  import java.util.zip.ZipEntry;
25  import java.util.zip.ZipException;
26  import java.util.zip.ZipInputStream;
27  
28  import org.apache.log4j.Logger;
29  
30  /** Find a resource recursively through all JARs, EARs, WARs, etc from
31   * the current directory down.
32   * 
33   * <p>Command-line usage</p>
34   *
35   * <p>The following command-line arguments are recognised 
36   *
37  <table>
38  <caption>Usage</caption>
39  <tr><th> -h -?     <td>displays this helptext
40  <tr><th> -f        <td>follow symlinks
41  <tr><th> -a        <td>show all resources found (i.e. do not use searchTerm)
42  <tr><th>
43  <tr><th colspan="2">Search criteria:
44  <tr><th> -i        <td>case-insensitive match
45  <tr><th> -sc       <td>if present, searchTerm matches within filename (default)
46  <tr><th> -ss       <td>if present, searchTerm matches start of filename
47  <tr><th> -se       <td>if present, searchTerm matches exact filename
48  <tr><th> -sr       <td>if present, searchTerm matches filename as a regular expression
49  <tr><th> -mf n     <td>max filesystem folder depth (0 = do not descend into subfolders)
50  <tr><th> -ma n     <td>max archive depth (0 = do not descend into archives)
51  <tr><th> -x        <td>if present, will attempt to recover if errors occur reading archives
52                         (errors sent to stderr)
53  <tr><th>
54  <tr><th colspan="2">Action when resource found:
55  <tr><th> -v        <td>verbose; display filenames with file sizes and timestamps
56  <tr><th> -vv       <td>display MD5/SHA1 hashes of resources (NB: modifies display order)
57  <tr><th> -d n      <td>dump/display the contents of the n'th resource found
58  <tr><th> -d all    <td>dump the name and contents of all resources found
59  <tr><th> -d names  <td>dump just the names of all resources found (default)
60  <tr><th> -d n1,n2...<td>dump the name and contents of the n1'th, n2'nd etc... resources found
61  <tr><th> -dm n|all <td>as per -d, but performs manifest unmangling on resource (fixes linewraps)
62  <tr><th> -dj n|all <td>as per -d, but performs class decompiling (requires jad to be in PATH)
63  <tr><th> -c text   <td>search for text in contents of resource (uses UTF-8 encoding)
64  <tr><th> -ci text  <td>case-insensitive search for text in contents of resources
65  </table>
66  
67   * 
68   * <p><b>TODO</b> split CLI functionality into separate class
69   * <p><b>TODO</b> pass enough information to the callback classes to display somewhat sane progress bar 
70   * <p><b>TODO</b> fix -dj switch + handle inner classes
71   * <p><b>TODO</b> rewrite jad to deal with annotations and other 1.5+ crap
72   * <p><b>TODO</b> -cs switches to change search behaviour within content
73   * 
74   * @author knoxg
75   * 
76   */
77  public class ResourceFinder {
78  
79  	/** Logger instance for this class */
80  	Logger logger = Logger.getLogger(ResourceFinder.class);
81  
82  	/** Match type used in {@link #matches(String)}} comparisons that tests whether
83  	 * the last component of a resource name contains a specified string; e.g. 
84  	 * "abc/def.txt" will match against the searchTerm "ef" using this matchType.
85  	 */
86  	public final static int MATCHTYPE_CONTAINS = 0;
87  	
88  	/** Match type used in {@link #matches(String)}} comparisons that tests whether
89  	 * the last component of a resource name starts with a specified string; e.g. 
90  	 * "abc/def.txt" will match against the searchTerm "de" using this matchType.
91  	 */
92  	public final static int MATCHTYPE_STARTSWITH = 1;
93  	
94  	/** Match type used in {@link #matches(String)}} comparisons that tests whether
95  	 * the last component of a resource name is equal to a specified regular expression; e.g. 
96  	 * "abc/def.txt" will match against the searchTerm "def.txt" using this matchType.
97  	 */
98  	public final static int MATCHTYPE_EXACT = 2;
99  
100 	/** Match type used in {@link #matches(String)}} comparisons that tests whether
101 	 * the last component of a resource name matches a specified regular expression; e.g. 
102 	 * "abc/def.txt" will match against the searchTerm ".*e.*" using this matchType.
103 	 */
104 	public final static int MATCHTYPE_REGEX = 3;
105 
106 	/** Resource being searched for */
107 	private String searchTerm;
108 	
109 	/** If performing regex searches, the Pattern form of {@link #searchTerm} */
110 	private Pattern searchPattern;
111 	
112 	/** File or directory from which search begins. If this is null, {@link #startInputStream} must be non-null, and vice versa */
113 	private File startDirectory;
114 	
115 	/** ZipInputStream from which search begins.  If this is null, {@link #startDirectory} must be non-null, and vice versa*/ 
116 	private ZipInputStream startInputStream;
117 	
118 	/** A MATCHTYPE_* constant. */
119 	private int matchType;
120 
121 	/** If true, performs a case-insensitive match */
122 	private boolean ignoreCase = false;
123 	
124 	/** If false, will prevent recursive search from following symbolic links */ 
125 	private boolean followSymlinks = false;
126 
127 	/** If true, will invoke the ResourceFinderCallback for every file in every archive iterated over 
128 	 * (i.e. the {@link #searchTerm} will be ignored) */ 
129 	private boolean showAll = false;
130 	
131 	/** If true, will attempt to recover processing after reading an invalid ZIP entry */
132 	private boolean ignoreErrors = false;
133 	
134 	/** Maximum depth; -1 = no depth limit. See {@link #setMaxArchiveDepth(long)}. <i>(Not implemented)</i>*/
135 	private long maxArchiveDepth = -1;
136 	
137 	/** Maximum folder depth; -1 = no depth limit. See {@link #setMaxFolderDepth(long)}. */
138 	private long maxFolderDepth = -1;
139 	
140 	/** Current archive depth */
141 	private long currentArchiveDepth = -1;
142 	
143 	/** Current folder depth */
144 	private long currentFolderDepth = -1;
145 	
146 	/** Callback to be invoked on every resource that matches the search criteria */
147 	private ResourceFinderCallback callback;
148 	
149 	/** If set to true, allows the search to be aborted whilst it is in progress */
150 	private transient boolean abort = false;
151 	
152 	/** Regex to define which files will be opened via ZipInputStream. Will return true if the file ends with
153 	 * .zip, .sar, .jar, .war, .ear, .rar or .har. These are Java RARs (resource archives), not the other type
154 	 * of RAR. */
155 	private Pattern isArchivePattern = Pattern.compile(
156 		".*\\.([Zz][Ii][Pp]|" +
157 		"[SsJjWwEeRrHh][Aa][Rr])$");
158 	
159 	/** Call this method within a {@link ResourceFinderCallback} to stop looking for resources */
160 	public void abort() { this.abort = true; }
161 	
162 	/** Return the file or directory from which search begins. If this returns null, try {@link #getStartInputStream()} */
163 	public File getStartDirectory() { return startDirectory; }
164 	
165 	/** Return the ZipInputStream from which search begins.  If this returns null, try {@link #getStartDirectory()} */ 
166 	public InputStream getStartInputStream() { return startInputStream; }
167 	
168 	/** Tests a resource name against the search criteria specified in this object
169 	 * 
170 	 * @param resourceName the last component of a resource name
171 	 * 
172 	 * @return true if the resource passes the search criteria, false otherwise
173 	 */
174 	public boolean matches(String resourceName) {
175 		if (resourceName==null) { throw new NullPointerException("null string"); }
176 		if (ignoreCase) {
177 			switch(matchType) {
178 				case MATCHTYPE_EXACT: return searchTerm.equalsIgnoreCase(resourceName);
179 				case MATCHTYPE_REGEX: return searchPattern.matcher(resourceName).find();
180 				case MATCHTYPE_STARTSWITH: return resourceName.toUpperCase().startsWith(searchTerm);
181 				case MATCHTYPE_CONTAINS: return resourceName.toUpperCase().contains(searchTerm);
182 				default: throw new IllegalStateException("Illegal matchType '" + matchType + "'");
183 			}
184 		} else {
185 			switch(matchType) {
186 				case MATCHTYPE_EXACT: return searchTerm.equals(resourceName);
187 				case MATCHTYPE_REGEX: return searchPattern.matcher(resourceName).find();
188 				case MATCHTYPE_STARTSWITH: return resourceName.startsWith(searchTerm);
189 				case MATCHTYPE_CONTAINS: return resourceName.contains(searchTerm);
190 				default: throw new IllegalStateException("Illegal matchType '" + matchType + "'");
191 			}
192 		}
193 	}
194 	
195 
196 	/** An {@link java.io.InputStream} wrapper which updates an internal md5/sha1 digest
197 	 * as the stream is being read.
198 	 */
199 	public static class HashGeneratingInputStream extends InputStream {
200 		InputStream is;
201 		MessageDigest algorithm1, algorithm2;
202 		
203 		public HashGeneratingInputStream(InputStream is) {
204 			this.is = is;
205 			try {
206 				algorithm1 = MessageDigest.getInstance("MD5");
207 				algorithm2 = MessageDigest.getInstance("SHA-1");
208 			} catch (NoSuchAlgorithmException nsae) {
209 				throw (IllegalStateException) new IllegalStateException(
210 					"Invalid crypto config").initCause(nsae);
211 			}
212     		algorithm1.reset();
213     		algorithm2.reset();
214 
215 		}
216 		
217 		@Override
218 		public int read() throws IOException {
219 			int result = is.read();
220 			if (result != -1) {
221 				algorithm1.update((byte) result);
222 				algorithm2.update((byte) result);
223 			}
224 			return result;
225 		}
226 		public int available() throws IOException {
227 			return is.available();
228 		}
229 		public void close() {
230 			// ignored;
231 		}
232 		public void mark(int readlimit) {
233 			is.mark(readlimit);
234 		}
235         public boolean markSupported() {
236         	return is.markSupported();
237         }
238         public int read(byte[] b)  throws IOException {
239         	int result = is.read(b);
240         	if (result!=-1) {
241         		algorithm1.update(b, 0, result);
242         		algorithm2.update(b, 0, result);
243         	}
244         	return result;
245         }
246         public int read(byte[] b, int off, int len)  throws IOException {
247         	int result = is.read(b, off, len);
248         	if (result!=-1) {
249         		algorithm1.update(b, off, result);
250         		algorithm2.update(b, off, result);
251         	}
252 			return result;
253         }
254         public void reset() throws IOException {
255         	is.reset();
256         }
257         public long skip(long n) throws IOException {
258         	return is.skip(n);
259         }
260         /** Returns the MD5 digest of all input that has been read by this InputStream so far,
261          * in a hexadecimal String form */
262         public String getMd5() {
263     		byte messageDigest[] = algorithm1.digest();
264     		//System.err.println("md5 messageDigest is " + messageDigest.length + " bytes");
265     		StringBuffer hexString = new StringBuffer();
266     		for (int i=0;i<messageDigest.length;i++) {
267     			hexString.append(Integer.toString( ( messageDigest[i] & 0xff ) + 0x100, 16).substring( 1 ));
268     		}
269     		return hexString.toString();
270         }
271         /** Returns the SHA1 digest of all input that has been read by this InputStream so far,
272          * in a hexadecimal String form */
273 		public String getSha1() {
274     		byte messageDigest[] = algorithm2.digest();
275     		//System.err.println("sha1 messageDigest is " + messageDigest.length + " bytes");
276     		StringBuffer hexString = new StringBuffer();
277     		for (int i=0;i<messageDigest.length;i++) {
278     			hexString.append(Integer.toString( ( messageDigest[i] & 0xff ) + 0x100, 16).substring( 1 ));
279     		}
280     		return hexString.toString();
281         }
282 	}
283 	
284 
285 	public static class ResourceFinderCallbackResult {
286 		boolean abort = false;
287 		InputStream replaceInputStream = null;
288 	}
289 	
290 	/** A callback interface used when resources are found using this object
291 	 * 
292 	 */
293 	public interface ResourceFinderCallback {
294 
295 		/** This method is only invoked for archive resources, before that archive has been read or
296 		 * recursed into. Both archives and standard files will be passed to the 
297 		 * {@link #postProcess(String, long, long, InputStream, ResourceFinderCallbackResult)} method.  
298 		 * 
299 		 * @param resourceName full resource name
300 		 * @param filesize the size of the (uncompressed) resource, or -1 if this is unknown
301 		 *   (some archives do not store this information) 
302 		 * @param timestamp the timestamp of the resource 
303 		 * @param inputStream an inputStream which can be used to retrieve the contents
304 		 *   of the resource
305 		 *   
306 		 * @return a ResourceFinderCallbackResult which can be used to abort the search or 
307 		 *   modify/wrap the inputStream being searched.
308 		 * 
309 		 * @throws IOException
310 		 */
311 		public ResourceFinderCallbackResult preProcess(String resourceName, long filesize, long timestamp, 
312 			InputStream inputStream) throws IOException;
313 
314 		/** This method is invoked for each resource that matches the search criteria
315 		 * specified in the containing {@link ResourceFinder} class. This method
316 		 * is not responsible for closing the supplied inputStream.
317 		 *  
318 		 * @param resourceName full resource name
319 		 * @param filesize the size of the (uncompressed) resource, or -1 if this is unknown
320 		 *   (some archives do not store this information) 
321 		 * @param timestamp the timestamp of the resource 
322 		 * @param inputStream an inputStream which can be used to retrieve the contents
323 		 *   of the resource
324 		 * 
325 		 * @return a ResourceFinderCallbackResult which can be used to abort the search
326 		 * 
327 		 * @throws IOException if an operation on the <code>inputStream</code> fails 
328 		 */
329 		public ResourceFinderCallbackResult postProcess(String resourceName, long filesize, long timestamp, 
330 			InputStream inputStream, ResourceFinderCallbackResult preProcessResult) throws IOException;
331 		
332 	}
333 
334 	/** Class which defines a callback which sends names and resource hashes to System.out.
335 	 */
336 	public static class HashingResourceFinderCallback implements ResourceFinderCallback {
337 		
338 		int resourceIndex = 0;
339 		boolean showHashes = false;
340 		boolean ignoreErrors = false;
341 	
342 		public HashingResourceFinderCallback(boolean ignoreErrors) {
343 			this.ignoreErrors = ignoreErrors;
344 		}
345 		
346 		public void ignorableException(String message, Exception e) throws ZipException {
347 			if (ignoreErrors) {
348 				Logger.getLogger(HashingResourceFinderCallback.class).error(message, e);
349 			} else {
350 				throw (ZipException) new ZipException(message).initCause(e);
351 			}
352 		}
353 		
354 		public ResourceFinderCallbackResult preProcess(String resourceName, long filesize, long timestamp, InputStream inputStream) throws IOException {
355 			ResourceFinderCallbackResult rfcbr = new ResourceFinderCallbackResult();
356 			rfcbr.replaceInputStream = new HashGeneratingInputStream(inputStream);
357 			return rfcbr;
358 		}
359 		
360 		public ResourceFinderCallbackResult postProcess(String resourceName, long filesize, long timestamp, InputStream inputStream, ResourceFinderCallbackResult preProcessResult) throws IOException {
361 			ResourceFinderCallbackResult rfcbr = new ResourceFinderCallbackResult();
362 			try {
363 				SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
364 				String md5, sha1;
365 				if (inputStream instanceof HashGeneratingInputStream) {
366 					HashGeneratingInputStream hgis = (HashGeneratingInputStream) inputStream;
367 					// pump the rest of the bits through this stream
368 					byte[] buffer = new byte[4096];
369 					try {
370 						
371 						while (inputStream.read(buffer) != -1) { /* nothing */ }
372 					} catch (EOFException eofe) {
373 						ignorableException("Error reading zip resource '" + resourceName + "' for hash", eofe);
374 					} catch (ZipException ze) {
375 						// can trigger "java.util.zip.ZipException: invalid distance code"
376 						ignorableException("Error reading zip resource '" + resourceName + "' for hash", ze);
377 					} 
378 					
379 					md5 = hgis.getMd5();
380 					sha1 = hgis.getSha1();
381 				} else {
382 					MessageDigest algorithm1, algorithm2;
383 					try {
384 						algorithm1 = MessageDigest.getInstance("MD5");
385 						algorithm2 = MessageDigest.getInstance("SHA1");
386 					} catch (NoSuchAlgorithmException nsae) {
387 						throw (IllegalStateException) new IllegalStateException(
388 							"Invalid crypto config").initCause(nsae);
389 					}
390 		    		algorithm1.reset();
391 		    		algorithm2.reset();
392 					byte[] buffer = new byte[4096];
393 					int bytesRead;
394 					try {
395 						while ((bytesRead = inputStream.read(buffer)) != -1) {
396 							algorithm1.update(buffer, 0, bytesRead);
397 							algorithm2.update(buffer, 0, bytesRead);
398 				        }
399 					} catch (EOFException eofe) {
400 						// can trigger "java.io.EOFException: Unexpected end of ZLIB input stream" errors
401 						ignorableException("Error hashing zip resource '" + resourceName + "'", eofe);
402 					} catch (ZipException ze) {
403 						ignorableException("Error hashing zip resource '" + resourceName + "'", ze);
404 					} 
405 		    		byte messageDigest1[] = algorithm1.digest();
406 		    		byte messageDigest2[] = algorithm2.digest();
407 		    		StringBuffer hexString1 = new StringBuffer();
408 		    		StringBuffer hexString2 = new StringBuffer();
409 		    		for (int i=0; i<messageDigest1.length; i++) {
410 		    			hexString1.append(Integer.toString( ( messageDigest1[i] & 0xff ) + 0x100, 16).substring( 1 ));
411 		    		}
412 		    		for (int i=0; i<messageDigest2.length; i++) {
413 		    			hexString2.append(Integer.toString( ( messageDigest2[i] & 0xff ) + 0x100, 16).substring( 1 ));
414 		    		}
415 		    		md5 = hexString1.toString();
416 		    		sha1 = hexString2.toString();
417 				}
418 	    		System.out.println("[" + resourceIndex + "] " + resourceName + " " + (filesize==-1 ? "(unknown)" : String.valueOf(filesize)) + 
419 	    			" " + sdf.format(new Date(timestamp)) + " " + md5 + " " + sha1 );
420 			} catch (IllegalArgumentException iae) {
421 				// not sure if this is needed any more
422 				throw new IOException("IllegalArgumentException processing ZipInputStream", iae);
423 			}
424 			resourceIndex++;
425 			return rfcbr;
426 		}
427 	}
428 
429 	
430 	/** Class which defines a callback which sends names and resources to System.out.
431 	 * 
432 	 * <p>This class uses '#' as a separator between the filesystem and files contained
433 	 * within archives; e.g. test.jar#abc.txt refers to abc.txt in test.jar.
434 	 * 
435 	 * <p>For comparison, Tangosol seems to use '!', includes a leading slash and 
436 	 * includes a protocol-like identifier at the beginning
437 	 * (e.g. jar:file:test.jar!/abc.txt). If a constructor is supplied which only provides
438 	 * a ZipInputStream (i.e. no filename is available), then resources will be returned 
439 	 * starting with the '#' character.
440 	 * 
441 	 */
442 	public static class DisplayResourceFinderCallback implements ResourceFinderCallback {
443 		
444 		public final static int DUMP_NAMES = -1;
445 		public final static int DUMP_NAMES_AND_RESOURCES = -2;
446 		public final static int DUMP_RESOURCES = 0;
447 	
448 		int dumpType = 0;
449 		List<Integer> dumpResourceNumbers;
450 		int maxDumpResourceNumber = -1;
451 		int resourceIndex = 0;
452 		boolean verbose = false;
453 		boolean manifests = false;
454 		boolean decompile = false;
455 		String searchContents = null;
456 		boolean searchContentsIgnoreCase = false;
457 
458 		// with all the trimmings
459 		public DisplayResourceFinderCallback(int dumpType, List<Integer> dumpResourceNumbers, boolean verbose, boolean manifests, boolean decompile, String searchContents, boolean searchContentsIgnoreCase) {
460 			// System.out.println("2 searchContents is " + searchContents);
461 			this.dumpResourceNumbers = dumpResourceNumbers;
462 			this.dumpType = dumpType;
463 			this.verbose = verbose;
464 			this.manifests = manifests;
465 			this.decompile = decompile;
466 			this.searchContents = searchContents;
467 			this.searchContentsIgnoreCase = searchContentsIgnoreCase;
468 			if (dumpResourceNumbers != null) {
469 				for (Integer drn : dumpResourceNumbers) {
470 					maxDumpResourceNumber = Math.max(maxDumpResourceNumber, drn.intValue());
471 				}
472 			}
473 		}
474 		
475 		private ResourceFinderCallbackResult dump(String resourceName, long filesize, long timestamp, InputStream inputStream) throws IOException {
476 			ResourceFinderCallbackResult rfcr = new ResourceFinderCallbackResult(); 
477 			if ((dumpType == DUMP_NAMES_AND_RESOURCES || dumpType == DUMP_NAMES) &&
478 				(dumpResourceNumbers==null || dumpResourceNumbers.contains(new Integer(resourceIndex)))
479 			   )
480 			{
481 				if (verbose) {
482 					SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
483 					System.out.println("[" + resourceIndex + "] " + resourceName + " " + (filesize==-1 ? "(unknown)" : String.valueOf(filesize)) + " " + sdf.format(new Date(timestamp)));
484 				} else if (searchContents == null) {
485 					System.out.println("[" + resourceIndex + "] " + resourceName);
486 				} else if (searchContents != null) {
487 					int pos = -1;
488 					// @TODO this assumes we never search for strings containing newlines
489 					LineNumberReader lnr = new LineNumberReader(new InputStreamReader(inputStream));
490 					if (searchContentsIgnoreCase) {
491 						searchContents = searchContents.toLowerCase();
492 						String line = lnr.readLine();
493 						if (line!=null) {
494 							pos = line.toString().toLowerCase().indexOf(searchContents.toLowerCase());
495 							while (line!=null && pos==-1) {
496 								line = lnr.readLine();
497 								pos = line==null ? -1 : line.toString().toLowerCase().indexOf(searchContents.toLowerCase());
498 							}
499 						}
500 					} else {
501 						String line = lnr.readLine();
502 						if (line!=null) {
503 							pos = line.toString().indexOf(searchContents);
504 							while (line!=null && pos==-1) {
505 								line = lnr.readLine();
506 								pos = line==null ? -1 :  line.toString().indexOf(searchContents);
507 							}
508 						}
509 					}
510 					if (pos!=-1) {
511 						System.out.println("[" + resourceIndex + "] [line " + lnr.getLineNumber() + ", col " + pos + "] " + resourceName);
512 					}
513 				}
514 			}  
515 			if ((dumpType == DUMP_NAMES_AND_RESOURCES || dumpType == DUMP_RESOURCES) &&
516 				(dumpResourceNumbers==null || dumpResourceNumbers.contains(new Integer(resourceIndex)))
517 				) {
518 				// InputStream is = ResourceFinder.getResourceStream(resourceName);
519 				if (manifests) {
520 					BufferedReader br = new BufferedReader(new InputStreamReader(inputStream));
521 					String line = br.readLine();
522 					while (line != null) {
523 						int len = line.length();
524 						while (line.length() > 0 && line.charAt(0) == ' ') {
525 							line = line.substring(1);
526 						}
527 						System.out.print(line);
528 						if (len != 70) { System.out.println(); }
529 						line = br.readLine();
530 					}
531 				} else if (decompile) {
532 					// hopefully this is deleted when the VM exits
533 					// @TODO: we need to grab all inner classes for this class as well
534 					File tmpFile = File.createTempFile("resourceFinder", ".class");
535 					FileOutputStream fos = new FileOutputStream(tmpFile); 
536 					StreamUtil.copyStream(inputStream, fos, 1024);
537 					fos.close();
538 					try {
539 						ProcessUtil processUtil = new ProcessUtil();
540 						String result = processUtil.exec(new String[] { "jad", "-lnc", "-p", tmpFile.getCanonicalPath() });
541 						System.out.println(result);
542 					} catch (ProcessUtil.ProcessException pe) {
543 						throw (IOException) new IOException("Problem executing jad").initCause(pe);
544 					}
545 					
546 				} else {
547 					StreamUtil.copyStream(inputStream, System.out, 1024);
548 				}
549 				if (resourceIndex == maxDumpResourceNumber) {
550 					// don't bother continuing this search if we're found the last resource being searched for
551 					rfcr.abort = true;
552 				}
553 
554 				// we don't insert an additional newline when dumping the contents of just one file
555 				// so that stdout redirection still does something useful
556 				if (dumpType == DUMP_NAMES_AND_RESOURCES && 
557 					(dumpResourceNumbers==null || dumpResourceNumbers.size()>1)) {
558 					System.out.println();
559 				}
560 			}
561 			resourceIndex++;
562 			return rfcr;
563 		}
564 
565 		public ResourceFinderCallbackResult preProcess(String resourceName, long filesize, long timestamp, InputStream inputStream) throws IOException {
566 			return dump(resourceName, filesize, timestamp, inputStream);
567 		}
568 		
569 		// won't be needing this one
570 		public ResourceFinderCallbackResult postProcess(String resourceName, long filesize, long timestamp, InputStream inputStream, ResourceFinderCallbackResult preProcessResult) throws IOException {
571 			if (preProcessResult==null) { return dump(resourceName, filesize, timestamp, inputStream); }
572 			return null;
573 		}
574 		
575 		
576 	}
577 	
578 	/** Creates a new resource finder object
579 	 * 
580 	 * @param searchTerm resource being searched for
581 	 * @param matchType a MATCHTYPE_* constant denoting how the searchTerm is to be used to match against resource names
582 	 * @param ignoreCase if true, will perform a case insensitive search 
583 	 * @param startDirectory directory from which search begins
584 	 * @param callback callback to be invoked on every resource that matches the search criteria
585 	 * 
586 	 * @throws IOException if the start directory is invalid
587 	 */
588 	public ResourceFinder(String searchTerm, int matchType, boolean ignoreCase, File startDirectory, ResourceFinderCallback callback) throws IOException {
589 		init(searchTerm, matchType, ignoreCase, callback);
590 		this.startDirectory = startDirectory.getCanonicalFile(); // for symlink test
591 
592 	}
593 	
594 	/** Common code to both the File and ZipInputStream constructors */
595 	private void init(String searchTerm, int matchType, boolean ignoreCase, 
596 		ResourceFinderCallback callback) 
597 	{
598 		this.matchType = matchType;
599 		this.ignoreCase = ignoreCase;
600 		this.showAll = false;
601 		this.ignoreErrors = false;
602 		this.callback = callback;
603 
604 		// @TODO clean this up a bit
605 		if (ignoreCase) {
606 			switch (matchType) {
607 				case MATCHTYPE_EXACT: break;
608 				case MATCHTYPE_REGEX: searchPattern = Pattern.compile(searchTerm, Pattern.CASE_INSENSITIVE); break;
609 				case MATCHTYPE_STARTSWITH: searchTerm = searchTerm.toUpperCase(); break;
610 				case MATCHTYPE_CONTAINS: searchTerm = searchTerm.toUpperCase(); break;
611 				default: throw new IllegalStateException("Illegal matchType '" + matchType + "'");
612 			}
613 		} else {
614 			switch (matchType) {
615 				case MATCHTYPE_EXACT: break;
616 				case MATCHTYPE_REGEX: searchPattern = Pattern.compile(searchTerm); break;
617 				case MATCHTYPE_STARTSWITH: break;
618 				case MATCHTYPE_CONTAINS: break;
619 				default: throw new IllegalStateException("Illegal matchType '" + matchType + "'");
620 			}
621 		}
622 		this.searchTerm = searchTerm;
623 		
624 	}
625 
626 	/** Sets whether to follow symbolic links during filesystem scans. By default symlinks will not be followed.
627 	 * 
628 	 * @see #getFollowSymlinks()
629 	 * 
630 	 * @param followSymlinks true if symbolic links should be followed during filesystem scans, false otherwise 
631 	 */
632 	public void setFollowSymLinks(boolean followSymlinks) {
633 		this.followSymlinks = followSymlinks;
634 	}
635 	
636 	/** Returns whether symbolic links will be followed during filesystem scans
637 	 *
638 	 * @see #setFollowSymLinks(boolean)
639 	 *  
640 	 * @return whether symbolic links will be followed during filesystem scans
641 	 */
642 	public boolean getFollowSymlinks() {
643 		return followSymlinks;
644 	}
645 	
646 	/** Sets whether the ResourceFinderCallback should be invoked for every file in every archive iterated over 
647 	 * (i.e. to ignore the {@link #searchTerm} ). By default this flag is set to false.
648 	 * 
649 	 * @see #getShowAll()
650 	 * 
651 	 * @param showAll if true, will invoke the ResourceFinderCallback for every file in every archive iterated over 
652 	 */
653 	public void setShowAll(boolean showAll) {
654 		this.showAll = showAll;
655 	}
656 	
657 	/** Returns whether the ResourceFinderCallback will be invoked for every file in every archive iterated over
658 	 * 
659 	 * @see #setShowAll(boolean)
660 	 * 
661 	 * @return true if the ResourceFinderCallback will be invoked for every file in every archive iterated over, false otherwise
662 	 */
663 	public boolean getShowAll() {
664 		return showAll;
665 	}
666 	
667 	/** Sets whether to ignore (some) exceptions encountered whilst processing ZipInputStreams. 
668 	 * 
669 	 * <p>Only EOFExceptions, ZipExceptions, IllegalArgumentExceptions and the push-back buffer 
670 	 * IOException will be ignored if this flag is set. Ignored exceptions will still be logged.
671 	 * 
672 	 * <p>By default, this flag is set to false.
673 	 *
674 	 * @see #getIgnoreErrors()
675 	 * 
676 	 * @param ignoreErrors true to ignore exceptions as described above, false otherwise
677 	 */
678 	public void setIgnoreErrors(boolean ignoreErrors) {
679 		this.ignoreErrors = ignoreErrors;
680 	}
681 	
682 	/** Returns true if exceptions will be ignored whilst processing ZipInputStreams
683 	 * 
684 	 * @see #setIgnoreErrors(boolean)
685 	 * 
686 	 * @return true if exceptions will be ignored whilst processing ZipInputStreams
687 	 */
688 	public boolean getIgnoreErrors() {
689 		return ignoreErrors;
690 	}
691 
692 	
693 	/** Creates a new resource finder object
694 	 * 
695 	 * @param searchTerm resource being searched for
696 	 * @param matchType a MATCHTYPE_* constant denoting how the searchTerm is to be used to match against resource names
697 	 * @param ignoreCase if true, will perform a case insensitive search 
698 	 * @param startInputStream stream from which search begins
699 	 * @param callback callback to be invoked on every resource that matches the search criteria 
700 	 * 
701 	 * @throws IOException if the start directory is invalid
702 	 */
703 	public ResourceFinder(String searchTerm, int matchType, boolean ignoreCase, ZipInputStream startInputStream, ResourceFinderCallback callback) throws IOException {
704 		init(searchTerm, matchType, ignoreCase, callback);
705 		this.startInputStream = startInputStream;
706 	}	
707 	
708 	/** Searches and returns a list of resources matching the criteria defined
709 	 * in the constructor
710 	 * 
711 	 * <p><b>TODO</b> the list returned by this object probably isn't accurate.
712 	 * 
713 	 * @throws IOException
714 	 */
715 	public void find() throws IOException {
716 		this.currentArchiveDepth = -1; // yet to enumerate initial folder / stream
717 		if (startInputStream != null) {
718 			//List<String> result = new ArrayList<String>();
719 			findResourceInZip(startInputStream, "#");
720 			return;
721 			
722 		} else if (startDirectory.isFile()) {
723 			//List<String> result = new ArrayList<String>();
724 			File file = startDirectory;
725 			String name = file.getName();
726 			if (!followSymlinks && isLink(file )) {
727 				// ignore symlinks
728 				// System.out.println("shazbot");
729 			} else if (matches(name)) {
730 				FileInputStream fis = new FileInputStream(file);
731 				callback.postProcess(name, file.length(), file.lastModified(), fis, null);
732 				fis.close();
733 				// perhaps make this another switch
734 				/*
735 				if (isArchive(name)) {
736 					ZipInputStream zis = new ZipInputStream(new FileInputStream(file));
737 					result.addAll(findResourceInZip(zis,  name + "#"));
738 				}
739 				*/
740 			} else if (isArchive(name)) {
741 				if (showAll) {
742 					FileInputStream fis = new FileInputStream(file);
743 					callback.postProcess(name, file.length(), file.lastModified(), fis, null);
744 					fis.close();
745 				}
746 				ZipInputStream zis = new ZipInputStream(new FileInputStream(file));
747 				zis.close();
748 			} else {
749 				if (showAll) {
750 					FileInputStream fis = new FileInputStream(file);
751 					callback.postProcess(name, file.length(), file.lastModified(), fis, null);
752 					fis.close();
753 				}
754 			}
755 			return;
756 		} else {
757 			findResourceInFolder(startDirectory, "");
758 		}
759 	}
760 
761 	/** Returns true if the filename will be treated as an archive
762 	 * 
763 	 * @param name a filename
764 	 * 
765 	 * @return true if the file is an archive, false otherwise
766 	 */
767 	public boolean isArchive(String name) {
768 		return isArchivePattern.matcher(name).matches();
769 	}
770 
771 	/** Determines whether a file is a symbolic link. 
772 	 * (Copied from http://www.idiom.com/~zilla/Xfiles/javasymlinks.html)
773 	 *
774 	 *  @param file file to test
775 	 *  
776 	 *  @return true if the file is a symbolic link, false otherwise
777 	 */
778 	public static boolean isLink(File file) throws IOException {
779 		try {
780 	        if (!file.exists()) {
781 	    	    return true;
782 	        } else {
783 			    String cnnpath = file.getCanonicalPath();
784 			    String abspath = file.getAbsolutePath();
785 			    return !abspath.equals(cnnpath);
786 			}
787 	    } catch(IOException ex) {
788 	        //System.err.println(ex);
789 	        return true;
790 	    }
791 	}
792 
793 	/** Returns a list of resources within the supplied folder, subfolders,
794 	 * and archives contained within these folders
795 	 * 
796 	 * @param folder the folder to search from
797 	 * @param prefix a prefix which is included in any results returned by this
798 	 *   method
799 	 * 
800 	 * @throws IOException 
801 	 */
802 	public void findResourceInFolder(File folder, String prefix) throws IOException  {
803 		if (maxArchiveDepth!=-1 && currentArchiveDepth>=maxArchiveDepth) {
804 			return;
805 		}
806 		// System.err.println("findResourceInFolder(" + prefix + "):" + currentArchiveDepth);
807 		currentArchiveDepth++;
808 		
809 		File[] folderContents = folder.listFiles();
810 		if (folderContents != null) {
811 			for (File file : folderContents) {
812 	
813 				// don't think any of these are going to work if we're calculating hashes as well.
814 				// maybe it will. who knows.
815 				
816 				String name = file.getName();
817 				// System.out.println("Filetest '" + name + "' against '" + resourceName + "' (fs=" + followSymlinks + ")");
818 				if (!followSymlinks && isLink(file)) {
819 					// ignore symlinks
820 					// System.out.println("shazbot");
821 				} else if (file.isDirectory() && (maxFolderDepth==-1 || currentFolderDepth+1 <= maxFolderDepth)) {
822 					currentFolderDepth++;
823 					findResourceInFolder(file, prefix + name + "/");
824 					currentFolderDepth--;
825 					
826 				} else if (matches(name)) {
827 					FileInputStream fis = new FileInputStream(file);
828 					callback.postProcess(prefix + name, file.length(), file.lastModified(), fis, null);
829 					fis.close();
830 					// perhaps make this another switch
831 					if (isArchive(name)) {
832 						fis = new FileInputStream(file);
833 						ZipInputStream zis = new ZipInputStream(fis);
834 						findResourceInZip(zis, prefix + name + "#");
835 						fis.close();
836 					}
837 				} else if (isArchive(name)) {
838 					if (showAll) {
839 						FileInputStream fis = new FileInputStream(file);
840 						callback.postProcess(prefix + name, file.length(), file.lastModified(), fis, null);
841 						fis.close();
842 					}
843 					FileInputStream fis = new FileInputStream(file);
844 					ZipInputStream zis = new ZipInputStream(fis);
845 					findResourceInZip(zis, prefix + name + "#");
846 					fis.close();
847 				} else {
848 					if (showAll) {
849 						FileInputStream fis = new FileInputStream(file);
850 						callback.postProcess(prefix + name, file.length(), file.lastModified(), fis, null);
851 						fis.close();
852 					}
853 				}
854 				if (abort) { break; }
855 			}
856 		}
857 
858 		currentArchiveDepth--;
859 	}
860 
861 	/** If ignoreErrors is true, send a message to stderr with the
862 	 * exception message, otherwise throw an encapsulated ZipException
863 	 * 
864 	 * @param message message describing exception
865 	 * @param e cause of the exception
866 	 */
867 	public void ignorableException(String message, Exception e) throws ZipException {
868 		if (ignoreErrors) {
869 			logger.error(message, e);
870 		} else {
871 			throw (ZipException) new ZipException(message).initCause(e);
872 		}
873 	}
874 	
875 	/** Returns a list of resources within the supplied archive, 
876 	 * and archives contained within this archive
877 	 * 
878 	 * @param zipInputStream the archive to search
879 	 * @param prefix a prefix which is included in any results returned by this
880 	 *   method. By convention, this prefix should end with a '#' to separate it
881 	 *   from resources found within the resource.
882 	 * 
883 	 * @throws IOException 
884 	 */
885 
886 	public void findResourceInZip(ZipInputStream zipInputStream, String prefix) throws IOException {
887 		if (maxArchiveDepth!=-1 && currentArchiveDepth>=maxArchiveDepth) {
888 			return;
889 		}
890 		// System.err.println("findResourceInZip(" + prefix + "):" + currentArchiveDepth);
891 		currentArchiveDepth++;
892 		
893 		// System.out.println("Searching in " + prefix);
894 		ZipEntry zipEntry = null;
895 		try {
896 			zipEntry = zipInputStream.getNextEntry();
897 		} catch (EOFException oefe) {
898 			ignorableException("Error retrieving first entry in zip '" + prefix.substring(0, prefix.length()-1) + "'", oefe);
899 			currentArchiveDepth--;
900 			return;
901 		} catch (ZipException ze) {
902 			ignorableException("Error retrieving first entry in zip '" + prefix.substring(0, prefix.length()-1) + "'", ze);
903 			currentArchiveDepth--;
904 			return;
905 		} catch (IllegalArgumentException iae) {
906 			// can occur in ZipInputStream.getUTF8String
907 			ignorableException("Error retrieving first entry in zip '" + prefix.substring(0, prefix.length()-1) + "'", iae);
908 			currentArchiveDepth--;
909 			return;
910 		}
911 		while (zipEntry != null) {
912 			String name = zipEntry.getName();
913 			String shortName = name;
914 			// zipEntry.isDirectory(); // write these at the end ? skip them altogether ?
915 
916 			// on unix, it's possible to get directory entries (trailing '/'s) within ZIPs; on windows this doesn't seem to happen
917 			while (shortName.endsWith("/")) { shortName = shortName.substring(0, shortName.length() - 1); }
918 			while (shortName.endsWith("\\")) { shortName = shortName.substring(0, shortName.length() - 1); }
919 			if (shortName.indexOf('/')!=-1) { shortName = shortName.substring(shortName.lastIndexOf('/') + 1); }
920 			if (shortName.indexOf('\\')!=-1) { shortName = shortName.substring(shortName.lastIndexOf('\\') + 1); }
921 
922 			// may need to do case-sensitive match
923 			/* commenting this out temporarily
924 
925 			if (showVersions && name.equalsIgnoreCase("META-INF/MANIFEST.MF")) {
926 				// treat this as a property file. Which is wrong, because it's got insane line breaks
927 				// but good enough for retrieving version data
928 				
929 				Properties props = new Properties();
930 				props.load(zipInputStream);
931 				if (props.getProperty("Specification-Version")!=null) {
932 					// there's also an Implementation-Version, but this appears to be the same
933 					// maven2 doesn't write these entries. perhaps.
934 					// @TODO something
935 				}
936 			}
937 			*/
938 			
939 			InputStream inputStreamToProcess = zipInputStream;
940 			
941 			// might just be easier to add .reset() to ZipInputStream
942 			ResourceFinderCallbackResult rfcbResult = null;
943 			if (isArchive(name)) {
944 				try {
945 					if (matches(shortName) || showAll) {
946 						rfcbResult = callback.preProcess(prefix + name, zipEntry.getSize(), zipEntry.getTime(), inputStreamToProcess);
947 						if (rfcbResult!=null && rfcbResult.replaceInputStream!=null) {
948 							inputStreamToProcess = rfcbResult.replaceInputStream;
949 						}
950 						if (rfcbResult!=null && rfcbResult.abort) {
951 							this.abort = true; break;
952 						}
953 					}
954 					ZipInputStream zis = new ZipInputStream(inputStreamToProcess);
955 					findResourceInZip(zis, prefix + name + "#");
956 					zipInputStream.closeEntry();
957 				} catch (EOFException eofe) {
958 					// can trigger "java.io.EOFException: Unexpected end of ZLIB input stream" errors
959 					ignorableException("Error reading zip '" + prefix + name + "'", eofe);
960 				} catch (ZipException ze) {
961 					ignorableException("Error reading zip '" + prefix + name + "'", ze);
962 				}
963 			}
964 			if (matches(shortName) || showAll) {
965 				rfcbResult = callback.postProcess(prefix + name, zipEntry.getSize(), zipEntry.getTime(), inputStreamToProcess, rfcbResult);
966 				if (rfcbResult!=null && rfcbResult.abort) {
967 					this.abort = true; break;
968 				}
969 			}
970 			
971 			try {
972 				zipEntry = zipInputStream.getNextEntry();
973 			} catch (EOFException oefe) {
974 				ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", oefe);
975 				break;
976 			} catch (ZipException ze) {
977 				ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", ze);
978 				break;
979 			} catch (IllegalArgumentException iae) {
980 				// can occur in ZipInputStream.getUTF8String
981 				ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", iae);
982 				break;
983 			} catch (IOException ioe) {
984 				// may occur after dodgy CRCs:
985 				// invalid entry CRC (expected 0xab633fa2 but got 0xc30a2df7)
986 				// Exception in thread "main" java.io.IOException: Push back buffer is full
987 				if (ioe.getMessage().contains("Push back buffer")) {
988 					ignorableException("Error retrieving next entry in zip '" + prefix.substring(0, prefix.length()-1) + "'; after '"+ name + "'", ioe);
989 					break;
990 				} else {
991 					currentArchiveDepth--;
992 					throw ioe;
993 				}
994 			}
995 			if (abort) { break ; }
996 		}
997 		currentArchiveDepth--;
998 	}
999 
1000 	/** Returns a resource as an inputstream
1001 	 * 
1002 	 * @param resourceName a resource name, as defined by the class javadoc
1003 	 * 
1004 	 * @return the resource as an InputStream  
1005 	 * 
1006 	 * @throws FileNotFoundException the resource could not be found
1007 	 * @throws IOException the resource could not be read
1008 	 */
1009 	public static InputStream getResourceStream(String resourceName) throws IOException {
1010 		int pos = resourceName.indexOf("#");
1011 		if (pos == -1) {
1012 			return new FileInputStream(resourceName);
1013 		} else {
1014 			String filename = resourceName.substring(0, pos);
1015 			String component = resourceName.substring(pos + 1);
1016 			ZipInputStream zis = new ZipInputStream(new FileInputStream(filename));
1017 			return getResourceComponent(zis, component, resourceName);
1018 		}
1019 	}
1020 	
1021 	/** Private method to recursively search within an archive for a file
1022 	 * 
1023 	 * @param zipInputStream input stream to search
1024 	 * @param component resource name fragment, separated by '#' characters
1025 	 * @param fullResource full resource name (only used in exception messages)  
1026 	 * 
1027 	 * @return the input stream 
1028 	 * 
1029 	 * @throws IOException the input stream cannot be read
1030 	 */
1031 	public static InputStream getResourceComponent(ZipInputStream zipInputStream, String component, String fullResource) throws IOException {
1032 		int pos = component.indexOf("#");
1033 		String filename = component;
1034 		String subComponent = null;
1035 		if (pos != -1) {
1036 			filename = component.substring(0, pos);
1037 			subComponent = component.substring(pos + 1);
1038 		}
1039 		ZipEntry zipEntry = zipInputStream.getNextEntry();
1040 		while (zipEntry != null) {
1041 			if (zipEntry.getName().equals(filename)) {
1042 				if (subComponent == null) {
1043 					return zipInputStream;
1044 				} else {
1045 					return getResourceComponent(new ZipInputStream(zipInputStream), subComponent, fullResource); 
1046 				}
1047 			}
1048 			zipEntry = zipInputStream.getNextEntry();
1049 		}
1050 		throw new FileNotFoundException("Could not find component '" + filename + "' in '" + fullResource + "'");
1051 	}
1052 	
1053 	/** Sets the maximum number of times I'm going to recursively enter a 
1054 	 * JAR/EAR/WAR/whatever.
1055 	 * 
1056 	 * <ul>
1057 	 * <li>0 = none; i.e. will just perform a directory scan.
1058 	 * <li>1..n = will search up to n directories/archives deep 
1059 	 * <li>-1 = infinite; i.e. will not perform depth checking 
1060 	 * </ul>
1061 	 * 
1062 	 * @param maxArchiveDepth maximum depth (-1=no limit, 0=will not recursive into JARs/WARs etc..)
1063 	 */
1064 	public void setMaxArchiveDepth(long maxArchiveDepth) {
1065 		this.maxArchiveDepth = maxArchiveDepth;
1066 	}
1067 
1068 	/** Sets the maximum folder depth to descend into the filesystem structure.
1069 	 * 
1070 	 * <p>This will not limit folder depth within archives, only folder depth within the filesystem 
1071 	 * 
1072 	 * <p>This setting has no effect if using the InputStream constructor.
1073 	 * 
1074 	 * <ul>
1075 	 * <li>0 = none; i.e. will just scan within the top-level folder.
1076 	 * <li>1..n = will search up to n folders deep 
1077 	 * <li>-1 = infinite; i.e. will not perform folder depth checking 
1078 	 * </ul>
1079 	 * 
1080 	 * @param maxFolderDepth maximum depth (-1=no limit, 0=will not recurse into folders)
1081 	 */
1082 	public void setMaxFolderDepth(long maxFolderDepth) {
1083 		this.maxFolderDepth = maxFolderDepth;
1084 	}
1085 
1086 	
1087 	public static String usage() {
1088 		return 
1089 		  "Usage: \n" +
1090 		  "  java " + ResourceFinder.class.getName() + " [options] searchTerm\n" +
1091 		  "or\n" +
1092 		  "  java " + ResourceFinder.class.getName() + " [options] -a\n" +
1093 		  "where [options] are:\n" +
1094 		  " -h -?     displays this helptext\n" +
1095 		  " -f        follow symlinks\n" + 
1096 		  " -a        show all resources found (i.e. do not use searchTerm)\n" +
1097 		  "\n" +
1098 		  "Search criteria:\n" +
1099 		  " -i        case-insensitive match\n" +
1100 		  " -sc       if present, searchTerm matches within filename (default)\n" +
1101 		  " -ss       if present, searchTerm matches start of filename\n" +
1102 		  " -se       if present, searchTerm matches exact filename\n" +
1103 		  " -sr       if present, searchTerm matches filename as a regular expression\n" +
1104 		  " -mf n     max filesystem folder depth (0 = do not descend into subfolders)\n" +
1105 		  " -ma n     max archive depth (0 = do not descend into archives)\n" +
1106 		  " -x        if present, will attempt to recover if errors occur reading archives\n"+
1107 		  "             (errors sent to stderr)\n" +
1108 		  "\n" +
1109 		  "Action when resource found:\n" +
1110 		  " -v        verbose; display filenames with file sizes and timestamps\n" +
1111 		  " -vv       display MD5/SHA1 hashes of resources (NB: modifies display order)\n" +
1112 		  " -d n      dump/display the contents of the n'th resource found\n" +
1113 		  " -d all    dump the name and contents of all resources found\n" +
1114 		  " -d names  dump just the names of all resources found (default)\n" +
1115 		  " -d n1,n2... dump the name and contents of the n1'th, n2'nd etc... resources found\n" +
1116 		  " -dm n|all as per -d, but performs manifest unmangling on resource (fixes linewraps)\n" +
1117 		  " -dj n|all as per -d, but performs class decompiling (requires jad to be in PATH)\n" +
1118 		  " -c text   search for text in contents of resource (uses UTF-8 encoding)\n" +
1119 		  " -ci text  case-insensitive search for text in contents of resources\n" +
1120 		  "\n" +
1121 		  "* A maximum of one -d switch should be present\n" +
1122 		  "* The -d and -c switches are mutually exclusive\n";
1123 	}
1124 	
1125 	/** Command-line interface to this class
1126 	 * 
1127 	 * @param args arguments
1128 	 * 
1129 	 * @throws IOException
1130 	 */
1131 	public static void main(String args[]) throws IOException {
1132 		String searchTerm;
1133 		String searchContents = null;
1134 		String dumpResource = "";
1135 		int     dumpType = DisplayResourceFinderCallback.DUMP_NAMES;
1136 		List<Integer> dumpResourceList = null;
1137 		int     argIndex = 0;
1138 		int     matchType = MATCHTYPE_CONTAINS;
1139 		long    maxArchiveDepth = -1;
1140 		long    maxFolderDepth = -1;
1141 		boolean followSymlinks = false;
1142 		boolean verbose = false;
1143 		boolean showHashes = false;
1144 		boolean showAll = false;
1145 		boolean manifests = false;
1146 		boolean decompile = false;
1147 		boolean ignoreCase = false;
1148 		boolean ignoreErrors = false;
1149 		boolean searchContentsIgnoreCase = false;
1150 		
1151 		if (args.length < 1) { 
1152 			System.out.println(usage());
1153 			throw new IllegalArgumentException("Expected resource search term or options");
1154 		}
1155 		while (argIndex < args.length && args[argIndex].startsWith("-")) {
1156 			if (args[argIndex].startsWith("-d")) {
1157 				if (args[argIndex].equals("-dm")) { manifests = true; }
1158 				if (args[argIndex].equals("-dj")) { decompile = true; }
1159 				
1160 			    dumpResource = args[argIndex + 1];
1161 			    if (dumpResource.equals("all")) {
1162 			    	dumpType = DisplayResourceFinderCallback.DUMP_NAMES_AND_RESOURCES;
1163 			    } else if (dumpResource.equals("names")) {
1164 			    	dumpType = DisplayResourceFinderCallback.DUMP_NAMES;
1165 			    } else {
1166 			    	dumpResourceList = new ArrayList<Integer>();
1167 			    	String[] resources = dumpResource.split(",");
1168 			    	for (String resource : resources) {
1169 			    		try {
1170 			    			dumpResourceList.add(new Integer(resource));
1171 			    		} catch (NumberFormatException nfe) {
1172 					    	// @TODO if it's not a number, then could use it as a resource id
1173 					    	throw new IllegalArgumentException("Expected numeric resource id (found '" + dumpResource + "')");
1174 					    }
1175 			    	}
1176 			    	if (dumpResourceList.size() == 1) {
1177 			    		dumpType = DisplayResourceFinderCallback.DUMP_RESOURCES;
1178 			    	} else {
1179 			    		dumpType = DisplayResourceFinderCallback.DUMP_NAMES_AND_RESOURCES;
1180 			    	}
1181 			    }
1182 			    // 1.6 method args = Arrays.copyOfRange(args, 2, args.length);
1183 			    argIndex += 2;
1184 			    
1185 			} else if (args[argIndex].equals("-mf")) {
1186 				maxFolderDepth = Long.parseLong(args[argIndex + 1]);
1187 				argIndex += 2;
1188 		    } else if (args[argIndex].equals("-ma")) {
1189 				maxArchiveDepth = Long.parseLong(args[argIndex + 1]);
1190 				argIndex += 2;
1191 			} else if (args[argIndex].equals("-v")) {
1192 			    verbose = true;
1193 			    argIndex ++;
1194 			} else if (args[argIndex].equals("-vv")) {
1195 				verbose = true;
1196 			    showHashes = true;
1197 			    argIndex ++;
1198 			} else if (args[argIndex].equals("-f")) {
1199 			    followSymlinks = true;
1200 			    argIndex ++;
1201 			} else if (args[argIndex].equals("-a")) {
1202 			    showAll = true;
1203 			    argIndex ++;
1204 			} else if (args[argIndex].equals("-sc")) {
1205 			    matchType = MATCHTYPE_CONTAINS;
1206 			    argIndex ++;
1207 			} else if (args[argIndex].equals("-ss")) {
1208 			    matchType = MATCHTYPE_STARTSWITH;
1209 			    argIndex ++;
1210 			} else if (args[argIndex].equals("-sr")) {
1211 			    matchType = MATCHTYPE_REGEX;
1212 			    argIndex ++;
1213 			} else if (args[argIndex].equals("-se")) {
1214 			    matchType = MATCHTYPE_EXACT;
1215 			    argIndex ++;
1216 			} else if (args[argIndex].equals("-c")) {
1217 			    searchContents = args[argIndex + 1];
1218 			    // System.out.println("1 searchContents is " + searchContents);
1219 			    argIndex += 2;		
1220 			} else if (args[argIndex].equals("-ci")) {
1221 			    searchContents = args[argIndex + 1];
1222 			    searchContentsIgnoreCase = true;
1223 			    // System.out.println("1 searchContents is " + searchContents);
1224 			    argIndex += 2;		
1225 			} else if (args[argIndex].equals("-i")) {
1226 			    ignoreCase = true;
1227 			    argIndex ++;
1228 			} else if (args[argIndex].equals("-x")) {
1229 			    ignoreErrors = true;
1230 			    argIndex ++;
1231 			} else if (args[argIndex].equals("-h") || args[argIndex].equals("-?")) {
1232 				System.out.println(usage());
1233 				System.exit(0);
1234 			} else {
1235 				System.out.println(usage());
1236 				throw new IllegalArgumentException("Unknown switch '" + args[argIndex] + "' supplied");
1237 			}
1238 		}
1239 
1240 		if (showAll) {
1241 			searchTerm = "maguffin";
1242 		} else {
1243 			if (args.length < argIndex + 1) {
1244 				System.out.println(usage());
1245 				throw new IllegalArgumentException("Expected resource search term");
1246 			}
1247 			searchTerm = args[argIndex++];
1248 		}
1249 		
1250 		ResourceFinderCallback callback;
1251 		if (showHashes) {
1252 			callback = new HashingResourceFinderCallback(ignoreErrors);
1253 		} else {
1254 			callback = new DisplayResourceFinderCallback(dumpType, dumpResourceList, verbose, manifests, decompile, 
1255 			  searchContents, searchContentsIgnoreCase);
1256 		}
1257 		
1258 		ResourceFinder resourceFinder = new ResourceFinder(searchTerm, matchType, ignoreCase, new File("."), callback);
1259 		resourceFinder.setFollowSymLinks(followSymlinks);
1260 		resourceFinder.setShowAll(showAll);
1261 		resourceFinder.setIgnoreErrors(ignoreErrors);
1262 		if (maxArchiveDepth != -1) { resourceFinder.setMaxArchiveDepth(maxArchiveDepth); }
1263 		if (maxFolderDepth != -1) { resourceFinder.setMaxFolderDepth(maxFolderDepth); }
1264 		 
1265 		resourceFinder.find();
1266 		
1267 		
1268 	}
1269 }
1270