001: /*
002: * FindBugs - Find bugs in Java programs
003: * Copyright (C) 2004, University of Maryland
004: *
005: * This library is free software; you can redistribute it and/or
006: * modify it under the terms of the GNU Lesser General Public
007: * License as published by the Free Software Foundation; either
008: * version 2.1 of the License, or (at your option) any later version.
009: *
010: * This library is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: * Lesser General Public License for more details.
014: *
015: * You should have received a copy of the GNU Lesser General Public
016: * License along with this library; if not, write to the Free Software
017: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: */
019:
020: package edu.umd.cs.findbugs.ba;
021:
022: import java.io.BufferedInputStream;
023: import java.io.File;
024: import java.io.FileInputStream;
025: import java.io.IOException;
026: import java.io.InputStream;
027: import java.io.Serializable;
028: import java.net.URL;
029: import java.util.HashSet;
030: import java.util.LinkedList;
031: import java.util.List;
032: import java.util.Set;
033: import java.util.zip.ZipEntry;
034: import java.util.zip.ZipFile;
035:
036: import org.apache.bcel.classfile.ClassFormatException;
037: import org.apache.bcel.classfile.ClassParser;
038: import org.apache.bcel.classfile.JavaClass;
039:
040: import edu.umd.cs.findbugs.FindBugs;
041: import edu.umd.cs.findbugs.util.Archive;
042:
043: /**
044: * A work-alike class to use instead of BCEL's ClassPath class.
045: * The main difference is that URLClassPath can load
046: * classfiles from URLs.
047: *
048: * @author David Hovemeyer
049: */
050: public class URLClassPath implements Serializable {
051: private static final long serialVersionUID = 1L;
052:
053: /**
054: * Interface describing a single classpath entry.
055: */
056: private interface Entry {
057: /**
058: * Open an input stream to read a resource in the codebase
059: * described by this classpath entry.
060: *
061: * @param resourceName name of resource to load: e.g., "java/lang/Object.class"
062: * @return an InputStream, or null if the resource wasn't found
063: * @throws IOException if an I/O error occurs
064: */
065: public InputStream openStream(String resourceName)
066: throws IOException;
067:
068: /**
069: * Get filename or URL as string.
070: */
071: public String getURL();
072:
073: /**
074: * Close the underlying resource.
075: */
076: public void close();
077: }
078:
079: /**
080: * Classpath entry class to load files from a zip/jar file
081: * in the local filesystem.
082: */
083: private static class LocalArchiveEntry implements Entry {
084: private ZipFile zipFile;
085:
086: public LocalArchiveEntry(String fileName) throws IOException {
087: try {
088: zipFile = new ZipFile(fileName);
089: } catch (IOException e) {
090: IOException ioe = new IOException(
091: "Could not open archive file " + fileName);
092: ioe.initCause(e);
093: throw ioe;
094: }
095: }
096:
097: /* (non-Javadoc)
098: * @see edu.umd.cs.findbugs.URLClassPath.Entry#openStream(java.lang.String)
099: */
100: public InputStream openStream(String resourceName)
101: throws IOException {
102: ZipEntry zipEntry = zipFile.getEntry(resourceName);
103: if (zipEntry == null)
104: return null;
105: return zipFile.getInputStream(zipEntry);
106: }
107:
108: /* (non-Javadoc)
109: * @see edu.umd.cs.findbugs.URLClassPath.Entry#getURL()
110: */
111: public String getURL() {
112: return zipFile.getName();
113: }
114:
115: public void close() {
116: try {
117: zipFile.close();
118: } catch (IOException e) {
119: // Ignore
120: }
121: }
122: }
123:
124: /**
125: * Classpath entry class to load files from a directory
126: * in the local filesystem.
127: */
128: private static class LocalDirectoryEntry implements Entry {
129: private String dirName;
130:
131: /**
132: * Constructor.
133: *
134: * @param dirName name of the local directory
135: * @throws IOException if dirName is not a directory
136: */
137: public LocalDirectoryEntry(String dirName) throws IOException {
138: this .dirName = dirName;
139: if (!(new File(dirName).isDirectory()))
140: throw new IOException(dirName + " is not a directory");
141: }
142:
143: /* (non-Javadoc)
144: * @see edu.umd.cs.findbugs.URLClassPath.Entry#openStream(java.lang.String)
145: */
146: public InputStream openStream(String resourceName)
147: throws IOException {
148: File file = new File(dirName, resourceName);
149: if (!file.exists())
150: return null;
151: return new BufferedInputStream(new FileInputStream(file));
152: }
153:
154: /* (non-Javadoc)
155: * @see edu.umd.cs.findbugs.URLClassPath.Entry#getURL()
156: */
157: public String getURL() {
158: return dirName;
159: }
160:
161: public void close() {
162: // Nothing to do here
163: }
164:
165: }
166:
167: /**
168: * Classpath entry class to load files from a remote archive URL.
169: * It uses jar URLs to specify individual files within the
170: * remote archive.
171: */
172: private static class RemoteArchiveEntry implements Entry {
173: private URL remoteArchiveURL;
174:
175: /**
176: * Constructor.
177: * @param remoteArchiveURL the remote zip/jar file URL
178: */
179: public RemoteArchiveEntry(URL remoteArchiveURL) {
180: this .remoteArchiveURL = remoteArchiveURL;
181: }
182:
183: /* (non-Javadoc)
184: * @see edu.umd.cs.findbugs.URLClassPath.Entry#openStream(java.lang.String)
185: */
186: public InputStream openStream(String resourceName)
187: throws IOException {
188: URL remoteFileURL = new URL("jar:"
189: + remoteArchiveURL.toString() + "/" + resourceName);
190: try {
191: return remoteFileURL.openStream();
192: } catch (IOException e) {
193: return null;
194: }
195: }
196:
197: /* (non-Javadoc)
198: * @see edu.umd.cs.findbugs.URLClassPath.Entry#getURL()
199: */
200: public String getURL() {
201: return remoteArchiveURL.toString();
202: }
203:
204: public void close() {
205: // Nothing to do
206: }
207:
208: }
209:
210: /**
211: * Classpath entry class to load files from a remote directory URL.
212: */
213: private static class RemoteDirectoryEntry implements Entry {
214: private URL remoteDirURL;
215:
216: /**
217: * Constructor.
218: * @param remoteDirURL URL of the remote directory; must end in "/"
219: */
220: public RemoteDirectoryEntry(URL remoteDirURL) {
221: this .remoteDirURL = remoteDirURL;
222: }
223:
224: /* (non-Javadoc)
225: * @see edu.umd.cs.findbugs.URLClassPath.Entry#openStream(java.lang.String)
226: */
227: public InputStream openStream(String resourceName)
228: throws IOException {
229: URL remoteFileURL = new URL(remoteDirURL.toString()
230: + resourceName);
231: try {
232: return remoteFileURL.openStream();
233: } catch (IOException e) {
234: return null;
235: }
236: }
237:
238: /* (non-Javadoc)
239: * @see edu.umd.cs.findbugs.URLClassPath.Entry#getURL()
240: */
241: public String getURL() {
242: return remoteDirURL.toString();
243: }
244:
245: public void close() {
246: // Nothing to do
247: }
248: }
249:
250: // Fields
251: private List<Entry> entryList;
252:
253: /**
254: * Constructor.
255: * Creates a classpath with no elements.
256: */
257: public URLClassPath() {
258: this .entryList = new LinkedList<Entry>();
259: }
260:
261: /**
262: * Add given filename/URL to the classpath.
263: * If no URL protocol is given, the filename is assumed
264: * to be a local file or directory.
265: * Remote directories must be specified with a "/" character at the
266: * end of the URL.
267: *
268: * @param fileName filename or URL of codebase (directory or archive file)
269: * @throws IOException if entry is invalid or does not exist
270: */
271: public void addURL(String fileName) throws IOException {
272: String protocol = URLClassPath.getURLProtocol(fileName);
273: if (protocol == null) {
274: fileName = "file:" + fileName;
275: protocol = "file";
276: }
277:
278: String fileExtension = URLClassPath.getFileExtension(fileName);
279: boolean isArchive = fileExtension != null
280: && URLClassPath.isArchiveExtension(fileExtension);
281:
282: Entry entry;
283: if (protocol.equals("file")) {
284: String localFileName = fileName.substring("file:".length());
285:
286: if (fileName.endsWith("/")
287: || new File(localFileName).isDirectory())
288: entry = new LocalDirectoryEntry(localFileName);
289: else if (isArchive)
290: entry = new LocalArchiveEntry(localFileName);
291: else
292: throw new IOException("Classpath entry " + fileName
293: + " is not a directory or archive file");
294: } else {
295: if (fileName.endsWith("/"))
296: entry = new RemoteDirectoryEntry(new URL(fileName));
297: else if (isArchive)
298: entry = new RemoteArchiveEntry(new URL(fileName));
299: else
300: throw new IOException("Classpath entry " + fileName
301: + " is not a remote directory or archive file");
302: }
303:
304: entryList.add(entry);
305: }
306:
307: /**
308: * Return the classpath string.
309: * @return the classpath string
310: */
311: public String getClassPath() {
312: StringBuffer buf = new StringBuffer();
313: for (Entry entry : entryList) {
314: if (buf.length() > 0)
315: buf.append(File.pathSeparator);
316: buf.append(entry.getURL());
317: }
318: return buf.toString();
319: }
320:
321: /**
322: * Open a stream to read given resource.
323: *
324: * @param resourceName name of resource to load, e.g. "java/lang/Object.class"
325: * @return input stream to read resource, or null if resource
326: * could not be found
327: * @throws IOException if an IO error occurs trying to determine
328: * whether or not the resource exists
329: */
330: private InputStream getInputStreamForResource(String resourceName)
331: throws IOException {
332: // Try each classpath entry, in order, until we find one
333: // that has the resource. Catch and ignore IOExceptions.
334:
335: // FIXME: The following code should throw IOException.
336: //
337: // URL.openStream() does not seem to distinguish
338: // whether the resource does not exist, vs. some
339: // transient error occurring while trying to access it.
340: // This is unfortunate, because we really should throw
341: // an exception out of this method in the latter case,
342: // since it means our knowledge of the classpath is
343: // incomplete.
344: //
345: // Short of reimplementing HTTP, etc., ourselves,
346: // there is probably nothing we can do about this problem.
347:
348: for (Entry entry : entryList) {
349: InputStream in;
350: try {
351: in = entry.openStream(resourceName);
352: if (in != null) {
353: if (URLClassPathRepository.DEBUG) {
354: System.out.println("\t==> found "
355: + resourceName + " in "
356: + entry.getURL());
357: }
358: return in;
359: }
360: } catch (IOException ignore) {
361: // Ignore
362: }
363: }
364: if (URLClassPathRepository.DEBUG) {
365: System.out.println("\t==> could not find " + resourceName
366: + " on classpath");
367: }
368: return null;
369: }
370:
371: private Set<String> classesThatCantBeFound = new HashSet<String>();
372:
373: /**
374: * Look up a class from the classpath.
375: *
376: * @param className name of class to look up
377: * @return the JavaClass object for the class
378: * @throws ClassNotFoundException if the class couldn't be found
379: * @throws ClassFormatException if the classfile format is invalid
380: */
381: public JavaClass lookupClass(String className)
382: throws ClassNotFoundException {
383: if (classesThatCantBeFound.contains(className)) {
384: throw new ClassNotFoundException(
385: "Error while looking for class " + className
386: + ": class not found");
387: }
388: String resourceName = className.replace('.', '/') + ".class";
389: InputStream in = null;
390: boolean parsedClass = false;
391:
392: try {
393:
394: in = getInputStreamForResource(resourceName);
395: if (in == null) {
396: classesThatCantBeFound.add(className);
397: throw new ClassNotFoundException(
398: "Error while looking for class " + className
399: + ": class not found");
400: }
401:
402: ClassParser classParser = new ClassParser(in, resourceName);
403: JavaClass javaClass = classParser.parse();
404: parsedClass = true;
405:
406: return javaClass;
407: } catch (IOException e) {
408: classesThatCantBeFound.add(className);
409: throw new ClassNotFoundException(
410: "IOException while looking for class " + className,
411: e);
412: } finally {
413: if (in != null && !parsedClass) {
414: try {
415: in.close();
416: } catch (IOException ignore) {
417: // Ignore
418: }
419: }
420: }
421: }
422:
423: /**
424: * Close all underlying resources.
425: */
426: public void close() {
427: for (Entry entry : entryList) {
428: entry.close();
429: }
430: entryList.clear();
431: }
432:
433: /**
434: * Get the URL protocol of given URL string.
435: * @param urlString the URL string
436: * @return the protocol name ("http", "file", etc.), or null if there is no protocol
437: */
438: public static String getURLProtocol(String urlString) {
439: String protocol = null;
440: int firstColon = urlString.indexOf(':');
441: if (firstColon >= 0) {
442: String specifiedProtocol = urlString.substring(0,
443: firstColon);
444: if (FindBugs.knownURLProtocolSet
445: .contains(specifiedProtocol))
446: protocol = specifiedProtocol;
447: }
448: return protocol;
449: }
450:
451: /**
452: * Get the file extension of given fileName.
453: * @return the file extension, or null if there is no file extension
454: */
455: public static String getFileExtension(String fileName) {
456: int lastDot = fileName.lastIndexOf('.');
457: return (lastDot >= 0) ? fileName.substring(lastDot) : null;
458: }
459:
460: /**
461: * Determine if given file extension indicates an archive file.
462: *
463: * @param fileExtension the file extension (e.g., ".jar")
464: * @return true if the file extension indicates an archive,
465: * false otherwise
466: */
467: public static boolean isArchiveExtension(String fileExtension) {
468: return Archive.ARCHIVE_EXTENSION_SET.contains(fileExtension);
469: }
470: }
471:
472: // vim:ts=4
|