001: /*
002: * FindBugs - Find Bugs in Java programs
003: * Copyright (C) 2006, David Hovemeyer <daveho@users.sourceforge.net>
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.BufferedReader;
023: import java.io.IOException;
024: import java.io.InputStream;
025: import java.util.HashMap;
026: import java.util.Map;
027: import java.util.NoSuchElementException;
028: import java.util.StringTokenizer;
029: import java.util.regex.Pattern;
030:
031: import edu.umd.cs.findbugs.SystemProperties;
032: import edu.umd.cs.findbugs.annotations.CheckForNull;
033: import edu.umd.cs.findbugs.annotations.NonNull;
034: import edu.umd.cs.findbugs.util.Util;
035:
036: /**
037: * Global information about the source code for an application.
038: * Currently, this object contains a map of source line information
039: * for fields and classes (items we don't get line number information
040: * for directly in classfiles), and also source line information
041: * for methods that don't appear directly in classfiles,
042: * such as abstract and native methods.
043: *
044: * @author David Hovemeyer
045: */
046: public class SourceInfoMap {
047: static class FieldDescriptor implements Comparable<FieldDescriptor> {
048: String className;
049: String fieldName;
050:
051: public FieldDescriptor(String className, String fieldName) {
052: this .className = className;
053: this .fieldName = fieldName;
054: }
055:
056: @Override
057: public String toString() {
058: return className + "." + fieldName;
059: }
060:
061: /* (non-Javadoc)
062: * @see java.lang.Comparable#compareTo(T)
063: */
064: public int compareTo(FieldDescriptor o) {
065: int cmp = className.compareTo(o.className);
066: if (cmp != 0)
067: return cmp;
068: return fieldName.compareTo(o.fieldName);
069: }
070:
071: /* (non-Javadoc)
072: * @see java.lang.Object#hashCode()
073: */
074: @Override
075: public int hashCode() {
076: return 1277 * className.hashCode() + fieldName.hashCode();
077: }
078:
079: /* (non-Javadoc)
080: * @see java.lang.Object#equals(java.lang.Object)
081: */
082: @Override
083: public boolean equals(Object obj) {
084: if (obj == null || obj.getClass() != this .getClass())
085: return false;
086: FieldDescriptor other = (FieldDescriptor) obj;
087: return className.equals(other.className)
088: && fieldName.equals(other.fieldName);
089: }
090: }
091:
092: static class MethodDescriptor implements
093: Comparable<MethodDescriptor> {
094: private String className;
095: private String methodName;
096: private String methodSignature;
097:
098: public MethodDescriptor(String className, String methodName,
099: String methodSignature) {
100: this .className = className;
101: this .methodName = methodName;
102: this .methodSignature = methodSignature;
103: }
104:
105: @Override
106: public String toString() {
107: return className + "." + methodName + ":" + methodSignature;
108: }
109:
110: /* (non-Javadoc)
111: * @see java.lang.Comparable#compareTo(T)
112: */
113: public int compareTo(MethodDescriptor o) {
114: int cmp;
115: if ((cmp = className.compareTo(o.className)) != 0)
116: return cmp;
117: if ((cmp = methodName.compareTo(o.methodName)) != 0)
118: return cmp;
119: return methodSignature.compareTo(o.methodSignature);
120: }
121:
122: /* (non-Javadoc)
123: * @see java.lang.Object#hashCode()
124: */
125: @Override
126: public int hashCode() {
127: return 1277 * className.hashCode() + 37
128: * methodName.hashCode()
129: + methodSignature.hashCode();
130: }
131:
132: /* (non-Javadoc)
133: * @see java.lang.Object#equals(java.lang.Object)
134: */
135: @Override
136: public boolean equals(Object obj) {
137: if (obj == null || obj.getClass() != this .getClass())
138: return false;
139: MethodDescriptor other = (MethodDescriptor) obj;
140: return className.equals(other.className)
141: && methodName.equals(other.methodName)
142: && methodSignature.equals(other.methodSignature);
143: }
144: }
145:
146: /**
147: * A range of source lines.
148: */
149: public static class SourceLineRange {
150: private final Integer start, end;
151:
152: /**
153: * Constructor for a single line.
154: */
155: public SourceLineRange(@NonNull
156: Integer line) {
157: this .start = this .end = line;
158: }
159:
160: /**
161: * Constructor for a range of lines.
162: *
163: * @param start start line in range
164: * @param end end line in range
165: */
166: public SourceLineRange(@NonNull
167: Integer start, @NonNull
168: Integer end) {
169: this .start = start;
170: this .end = end;
171: }
172:
173: /**
174: * @return Returns the start.
175: */
176: public @NonNull
177: Integer getStart() {
178: return start;
179: }
180:
181: /**
182: * @return Returns the end.
183: */
184: public @NonNull
185: Integer getEnd() {
186: return end;
187: }
188:
189: /* (non-Javadoc)
190: * @see java.lang.Object#toString()
191: */
192: @Override
193: public String toString() {
194: return start + (start.equals(end) ? "" : "-" + end);
195: }
196: }
197:
198: private static final boolean DEBUG = SystemProperties
199: .getBoolean("sourceinfo.debug");
200:
201: private Map<FieldDescriptor, SourceLineRange> fieldLineMap;
202: private Map<MethodDescriptor, SourceLineRange> methodLineMap;
203: private Map<String, SourceLineRange> classLineMap;
204:
205: public boolean fallBackToClassfile() {
206: return isEmpty();
207: }
208:
209: public boolean isEmpty() {
210: return fieldLineMap.isEmpty() && methodLineMap.isEmpty()
211: && classLineMap.isEmpty();
212: }
213:
214: /**
215: * Constructor.
216: * Creates an empty object.
217: */
218: public SourceInfoMap() {
219: this .fieldLineMap = new HashMap<FieldDescriptor, SourceLineRange>();
220: this .methodLineMap = new HashMap<MethodDescriptor, SourceLineRange>();
221: this .classLineMap = new HashMap<String, SourceLineRange>();
222: }
223:
224: /**
225: * Add a line number entry for a field.
226: *
227: * @param className name of class containing the field
228: * @param fieldName name of field
229: * @param range the line number(s) of the field
230: */
231: public void addFieldLine(String className, String fieldName,
232: SourceLineRange range) {
233: fieldLineMap.put(new FieldDescriptor(className, fieldName),
234: range);
235: }
236:
237: /**
238: * Add a line number entry for a method.
239: *
240: * @param className name of class containing the method
241: * @param methodName name of method
242: * @param methodSignature signature of method
243: * @param range the line number of the method
244: */
245: public void addMethodLine(String className, String methodName,
246: String methodSignature, SourceLineRange range) {
247: methodLineMap.put(new MethodDescriptor(className, methodName,
248: methodSignature), range);
249: }
250:
251: /**
252: * Add line number entry for a class.
253: *
254: * @param className name of class
255: * @param range the line numbers of the class
256: */
257: public void addClassLine(String className, SourceLineRange range) {
258: classLineMap.put(className, range);
259: }
260:
261: /**
262: * Look up the line number range for a field.
263: *
264: * @param className name of class containing the field
265: * @param fieldName name of field
266: * @return the line number range, or null if no line number is known for the field
267: */
268: public @CheckForNull
269: SourceLineRange getFieldLine(String className, String fieldName) {
270: return fieldLineMap.get(new FieldDescriptor(className,
271: fieldName));
272: }
273:
274: /**
275: * Look up the line number range for a method.
276: *
277: * @param className name of class containing the method
278: * @param methodName name of method
279: * @param methodSignature signature of method
280: * @return the line number range, or null if no line number is known for the method
281: */
282: public @CheckForNull
283: SourceLineRange getMethodLine(String className, String methodName,
284: String methodSignature) {
285: return methodLineMap.get(new MethodDescriptor(className,
286: methodName, methodSignature));
287: }
288:
289: /**
290: * Look up the line number range for a class.
291: *
292: * @param className name of the class
293: * @return the line number range, or null if no line number is known for the class
294: */
295: public @CheckForNull
296: SourceLineRange getClassLine(String className) {
297: return classLineMap.get(className);
298: }
299:
300: private static final Pattern DIGITS = Pattern.compile("^[0-9]+$");
301:
302: /**
303: * Read source info from given InputStream.
304: * The stream is guaranteed to be closed.
305: *
306: * @param inputStream the InputStream
307: * @throws IOException if an I/O error occurs, or if the format is invalid
308: */
309: public void read(InputStream inputStream) throws IOException {
310: BufferedReader reader = new BufferedReader(Util
311: .getReader(inputStream));
312:
313: int lineNumber = 0;
314: try {
315: String line;
316: int lparen;
317: String version;
318:
319: while ((line = reader.readLine()) != null) {
320: ++lineNumber;
321:
322: if (lineNumber == 1) {
323: if (DEBUG)
324: System.out.println("First line: " + line);
325: // Try to parse the version number string from the first line.
326: // null means that the line does not appear to be a version number.
327: version = parseVersionNumber(line);
328: if (version != null) {
329: // Check to see if version is supported.
330: // Only 1.0 supported for now.
331: if (!version.equals("1.0"))
332: throw new IOException(
333: "Unsupported sourceInfo version "
334: + version);
335:
336: // Version looks good. Skip to next line of file.
337: continue;
338: }
339: }
340:
341: StringTokenizer tokenizer = new StringTokenizer(line,
342: ",");
343:
344: String className = tokenizer.nextToken();
345: String next = tokenizer.nextToken();
346: if (DIGITS.matcher(next).matches()) {
347: // Line number for class
348: SourceLineRange range = createRange(next, tokenizer
349: .nextToken());
350: classLineMap.put(className, range);
351: if (DEBUG)
352: System.out.println("class:" + className + ","
353: + range);
354: } else if ((lparen = next.indexOf('(')) >= 0) {
355: // Line number for method
356: String methodName = next.substring(0, lparen);
357: String methodSignature = next.substring(lparen);
358:
359: if (methodName.equals("init^"))
360: methodName = "<init>";
361: else if (methodName.equals("clinit^"))
362: methodName = "<clinit>";
363:
364: SourceLineRange range = createRange(tokenizer
365: .nextToken(), tokenizer.nextToken());
366: methodLineMap.put(new MethodDescriptor(className,
367: methodName, methodSignature), range);
368: if (DEBUG)
369: System.out.println("method:" + methodName
370: + methodSignature + "," + range);
371: } else {
372: // Line number for field
373: String fieldName = next;
374: SourceLineRange range = createRange(tokenizer
375: .nextToken(), tokenizer.nextToken());
376: fieldLineMap.put(new FieldDescriptor(className,
377: fieldName), range);
378: if (DEBUG)
379: System.out.println("field:" + className + ","
380: + fieldName + "," + range);
381: }
382:
383: // Note: we could complain if there are more tokens,
384: // but instead we'll just ignore them.
385: }
386: } catch (NoSuchElementException e) {
387: IOException ioe = new IOException(
388: "Invalid syntax in source info file at line "
389: + lineNumber);
390: ioe.initCause(e);
391: throw ioe;
392: } finally {
393: try {
394: reader.close();
395: } catch (IOException e) {
396: // ignore
397: }
398: }
399: }
400:
401: /**
402: * Parse the sourceInfo version string.
403: *
404: * @param line the first line of the sourceInfo file
405: * @return the version number constant, or null if
406: * the line does not appear to be a version string
407: */
408: private static String parseVersionNumber(String line) {
409: StringTokenizer tokenizer = new StringTokenizer(line, " \t");
410:
411: if (!expect(tokenizer, "sourceInfo")
412: || !expect(tokenizer, "version")
413: || !tokenizer.hasMoreTokens())
414: return null;
415:
416: return tokenizer.nextToken();
417: }
418:
419: /**
420: * Expect a particular token string to be returned by the given
421: * StringTokenizer.
422: *
423: * @param tokenizer the StringTokenizer
424: * @param token the expectedToken
425: * @return true if the expected token was returned, false if not
426: */
427: private static boolean expect(StringTokenizer tokenizer,
428: String token) {
429: if (!tokenizer.hasMoreTokens())
430: return false;
431: String s = tokenizer.nextToken();
432: if (DEBUG)
433: System.out.println("token=" + s);
434: return s.equals(token);
435: }
436:
437: private static SourceLineRange createRange(String start, String end) {
438: return new SourceLineRange(Integer.valueOf(start), Integer
439: .valueOf(end));
440: }
441: }
|