/*
* FindBugs - Find Bugs in Java programs
* Copyright (C) 2006, David Hovemeyer <daveho@users.sourceforge.net>
*
* This library is free software; you can redistribute it and/or
* modify it under the terms of the GNU Lesser General Public
* License as published by the Free Software Foundation; either
* version 2.1 of the License, or (at your option) any later version.
*
* This library is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this library; if not, write to the Free Software
* Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
package edu.umd.cs.findbugs.ba;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.StringTokenizer;
import java.util.regex.Pattern;
import javax.annotation.WillClose;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.annotations.CheckForNull;
import edu.umd.cs.findbugs.annotations.NonNull;
import edu.umd.cs.findbugs.util.Util;
/**
* Global information about the source code for an application. Currently, this
* object contains a map of source line information for fields and classes
* (items we don't get line number information for directly in classfiles), and
* also source line information for methods that don't appear directly in
* classfiles, such as abstract and native methods.
*
* @author David Hovemeyer
*/
public class SourceInfoMap {
static class FieldDescriptor implements Comparable<FieldDescriptor> {
String className;
String fieldName;
public FieldDescriptor(String className, String fieldName) {
this.className = className;
this.fieldName = fieldName;
}
@Override
public String toString() {
return className + "." + fieldName;
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(T)
*/
public int compareTo(FieldDescriptor o) {
int cmp = className.compareTo(o.className);
if (cmp != 0)
return cmp;
return fieldName.compareTo(o.fieldName);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return 1277 * className.hashCode() + fieldName.hashCode();
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != this.getClass())
return false;
FieldDescriptor other = (FieldDescriptor) obj;
return className.equals(other.className) && fieldName.equals(other.fieldName);
}
}
static class MethodDescriptor implements Comparable<MethodDescriptor> {
private String className;
private String methodName;
private String methodSignature;
public MethodDescriptor(String className, String methodName, String methodSignature) {
this.className = className;
this.methodName = methodName;
this.methodSignature = methodSignature;
}
@Override
public String toString() {
return className + "." + methodName + ":" + methodSignature;
}
/*
* (non-Javadoc)
*
* @see java.lang.Comparable#compareTo(T)
*/
public int compareTo(MethodDescriptor o) {
int cmp;
if ((cmp = className.compareTo(o.className)) != 0)
return cmp;
if ((cmp = methodName.compareTo(o.methodName)) != 0)
return cmp;
return methodSignature.compareTo(o.methodSignature);
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#hashCode()
*/
@Override
public int hashCode() {
return 1277 * className.hashCode() + 37 * methodName.hashCode() + methodSignature.hashCode();
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#equals(java.lang.Object)
*/
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != this.getClass())
return false;
MethodDescriptor other = (MethodDescriptor) obj;
return className.equals(other.className) && methodName.equals(other.methodName)
&& methodSignature.equals(other.methodSignature);
}
}
/**
* A range of source lines.
*/
public static class SourceLineRange {
private final Integer start, end;
/**
* Constructor for a single line.
*/
public SourceLineRange(@NonNull Integer line) {
this.start = this.end = line;
}
/**
* Constructor for a range of lines.
*
* @param start
* start line in range
* @param end
* end line in range
*/
public SourceLineRange(@NonNull Integer start, @NonNull Integer end) {
this.start = start;
this.end = end;
}
/**
* @return Returns the start.
*/
public @NonNull
Integer getStart() {
return start;
}
/**
* @return Returns the end.
*/
public @NonNull
Integer getEnd() {
return end;
}
/*
* (non-Javadoc)
*
* @see java.lang.Object#toString()
*/
@Override
public String toString() {
return start + (start.equals(end) ? "" : "-" + end);
}
}
private static final boolean DEBUG = SystemProperties.getBoolean("sourceinfo.debug");
private Map<FieldDescriptor, SourceLineRange> fieldLineMap;
private Map<MethodDescriptor, SourceLineRange> methodLineMap;
private Map<String, SourceLineRange> classLineMap;
public boolean fallBackToClassfile() {
return isEmpty();
}
public boolean isEmpty() {
return fieldLineMap.isEmpty() && methodLineMap.isEmpty() && classLineMap.isEmpty();
}
/**
* Constructor. Creates an empty object.
*/
public SourceInfoMap() {
this.fieldLineMap = new HashMap<FieldDescriptor, SourceLineRange>();
this.methodLineMap = new HashMap<MethodDescriptor, SourceLineRange>();
this.classLineMap = new HashMap<String, SourceLineRange>();
}
/**
* Add a line number entry for a field.
*
* @param className
* name of class containing the field
* @param fieldName
* name of field
* @param range
* the line number(s) of the field
*/
public void addFieldLine(String className, String fieldName, SourceLineRange range) {
fieldLineMap.put(new FieldDescriptor(className, fieldName), range);
}
/**
* Add a line number entry for a method.
*
* @param className
* name of class containing the method
* @param methodName
* name of method
* @param methodSignature
* signature of method
* @param range
* the line number of the method
*/
public void addMethodLine(String className, String methodName, String methodSignature, SourceLineRange range) {
methodLineMap.put(new MethodDescriptor(className, methodName, methodSignature), range);
}
/**
* Add line number entry for a class.
*
* @param className
* name of class
* @param range
* the line numbers of the class
*/
public void addClassLine(String className, SourceLineRange range) {
classLineMap.put(className, range);
}
/**
* Look up the line number range for a field.
*
* @param className
* name of class containing the field
* @param fieldName
* name of field
* @return the line number range, or null if no line number is known for the
* field
*/
public @CheckForNull
SourceLineRange getFieldLine(String className, String fieldName) {
return fieldLineMap.get(new FieldDescriptor(className, fieldName));
}
/**
* Look up the line number range for a method.
*
* @param className
* name of class containing the method
* @param methodName
* name of method
* @param methodSignature
* signature of method
* @return the line number range, or null if no line number is known for the
* method
*/
public @CheckForNull
SourceLineRange getMethodLine(String className, String methodName, String methodSignature) {
return methodLineMap.get(new MethodDescriptor(className, methodName, methodSignature));
}
/**
* Look up the line number range for a class.
*
* @param className
* name of the class
* @return the line number range, or null if no line number is known for the
* class
*/
public @CheckForNull
SourceLineRange getClassLine(String className) {
return classLineMap.get(className);
}
private static final Pattern DIGITS = Pattern.compile("^[0-9]+$");
/**
* Read source info from given InputStream. The stream is guaranteed to be
* closed.
*
* @param inputStream
* the InputStream
* @throws IOException
* if an I/O error occurs, or if the format is invalid
*/
public void read(@WillClose InputStream inputStream) throws IOException {
BufferedReader reader = new BufferedReader(Util.getReader(inputStream));
int lineNumber = 0;
try {
String line;
int lparen;
String version;
while ((line = reader.readLine()) != null) {
++lineNumber;
if (lineNumber == 1) {
if (DEBUG)
System.out.println("First line: " + line);
// Try to parse the version number string from the first
// line.
// null means that the line does not appear to be a version
// number.
version = parseVersionNumber(line);
if (version != null) {
// Check to see if version is supported.
// Only 1.0 supported for now.
if (!version.equals("1.0"))
throw new IOException("Unsupported sourceInfo version " + version);
// Version looks good. Skip to next line of file.
continue;
}
}
StringTokenizer tokenizer = new StringTokenizer(line, ",");
String className = tokenizer.nextToken();
String next = tokenizer.nextToken();
if (DIGITS.matcher(next).matches()) {
// Line number for class
SourceLineRange range = createRange(next, tokenizer.nextToken());
classLineMap.put(className, range);
if (DEBUG)
System.out.println("class:" + className + "," + range);
} else if ((lparen = next.indexOf('(')) >= 0) {
// Line number for method
String methodName = next.substring(0, lparen);
String methodSignature = next.substring(lparen);
if (methodName.equals("init^"))
methodName = "<init>";
else if (methodName.equals("clinit^"))
methodName = "<clinit>";
SourceLineRange range = createRange(tokenizer.nextToken(), tokenizer.nextToken());
methodLineMap.put(new MethodDescriptor(className, methodName, methodSignature), range);
if (DEBUG)
System.out.println("method:" + methodName + methodSignature + "," + range);
} else {
// Line number for field
String fieldName = next;
SourceLineRange range = createRange(tokenizer.nextToken(), tokenizer.nextToken());
fieldLineMap.put(new FieldDescriptor(className, fieldName), range);
if (DEBUG)
System.out.println("field:" + className + "," + fieldName + "," + range);
}
// Note: we could complain if there are more tokens,
// but instead we'll just ignore them.
}
} catch (NoSuchElementException e) {
IOException ioe = new IOException("Invalid syntax in source info file at line " + lineNumber);
ioe.initCause(e);
throw ioe;
} finally {
try {
reader.close();
} catch (IOException e) {
// ignore
}
}
}
/**
* Parse the sourceInfo version string.
*
* @param line
* the first line of the sourceInfo file
* @return the version number constant, or null if the line does not appear
* to be a version string
*/
private static String parseVersionNumber(String line) {
StringTokenizer tokenizer = new StringTokenizer(line, " \t");
if (!expect(tokenizer, "sourceInfo") || !expect(tokenizer, "version") || !tokenizer.hasMoreTokens())
return null;
return tokenizer.nextToken();
}
/**
* Expect a particular token string to be returned by the given
* StringTokenizer.
*
* @param tokenizer
* the StringTokenizer
* @param token
* the expectedToken
* @return true if the expected token was returned, false if not
*/
private static boolean expect(StringTokenizer tokenizer, String token) {
if (!tokenizer.hasMoreTokens())
return false;
String s = tokenizer.nextToken();
if (DEBUG)
System.out.println("token=" + s);
return s.equals(token);
}
private static SourceLineRange createRange(String start, String end) {
return new SourceLineRange(Integer.valueOf(start), Integer.valueOf(end));
}
}