/*
* Contributions to FindBugs
* Copyright (C) 2006, Institut for Software
* An Institut of the University of Applied Sciences Rapperswil
*
* Author: Thierry Wyss, Marco Busarello
*
* 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.plugin.eclipse.quickfix;
import static edu.umd.cs.findbugs.plugin.eclipse.quickfix.util.ConditionCheck.checkForNull;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Hashtable;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import javax.annotation.CheckForNull;
import javax.annotation.WillClose;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import org.eclipse.ui.IMarkerResolution;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;
import org.xml.sax.SAXException;
import de.tobject.findbugs.FindbugsPlugin;
import edu.umd.cs.findbugs.SystemProperties;
import edu.umd.cs.findbugs.ba.AnalysisContext;
/**
* Loades the <CODE>BugResolution</CODE>s form a xml document. The document
* specifies the supported <CODE>BugResolution</CODE>s for the bug-types. An
* entry has the form:<br>
* <br>
* <CODE>
* <bug type="BUG_TYPE"><br>
* <resolution classname="bugResolutionClassName"><br>
* <attr name="property">value</attr><br>
* </resolution><br>
* </bug><br><br>
* </CODE> The attributes specified for a <CODE>BugResolution</CODE> supports
* all primitive types and strings. If an error occurs while loading a
* <CODE>BugResolution</CODE>, the error will be reported to the error log.
*
* @author <a href="mailto:twyss@hsr.ch">Thierry Wyss</a>
* @author <a href="mailto:mbusarel@hsr.ch">Marco Busarello</a>
* @author <a href="mailto:g1zgragg@hsr.ch">Guido Zgraggen</a>
* @version 1.0
*/
public class BugResolutionLoader {
private static final String BUG = "bug"; //$NON-NLS-1$
private static final String BUG_TYPE = "type"; //$NON-NLS-1$
private static final String RESOLUTION = "resolution"; //$NON-NLS-1$
private static final String RESOLUTION_CLASS = "classname"; //$NON-NLS-1$
private static final String ATTR = "attr"; //$NON-NLS-1$
private static final String ATTR_NAME = "name"; //$NON-NLS-1$
private DocumentBuilder builder;
protected BugResolutionLoader(DocumentBuilder builder) {
super();
this.builder = builder;
}
public BugResolutionLoader() {
this(null);
}
public BugResolutionAssociations loadBugResolutions(File xmlFile) {
return loadBugResolutions(xmlFile, null);
}
public BugResolutionAssociations loadBugResolutions(File xmlFile, BugResolutionAssociations associations) {
Document doc = parseDocument(xmlFile);
if (doc == null) {
return null;
}
return loadBugResolutions(doc, associations);
}
public BugResolutionAssociations loadBugResolutions() {
InputStream is = BugResolutionLoader.class.getResourceAsStream("findbugs-resolutions.xml");
if (is == null) {
return null;
}
Document doc = parseDocument(is);
if (doc == null) {
return null;
}
return loadBugResolutions(doc, null);
}
public BugResolutionAssociations loadBugResolutions(Document document) {
return loadBugResolutions(document, null);
}
/**
* Loades the <CODE>BugResolutions</CODE> from the given XML-Document into
* the specified <CODE>BugResolutionAssociations</CODE>.
*
* @param fixesDoc
* the XML-Document that contains the quick-fixes.
* @param associations
* the <CODE>BugResolutionAssociations</CODE> to load the
* <CODE>BugResolutions</CODE> in.
* @return the <CODE>associations</CODE> or a new instance.
* @throws IllegalArgumentException
* if the <CODE>fixesDoc</CODE> is <CODE>null</CODE>.
*/
public BugResolutionAssociations loadBugResolutions(Document fixesDoc, BugResolutionAssociations associations) {
checkForNull(fixesDoc, "xml document with bug-resolutions");
if (associations == null) {
associations = new BugResolutionAssociations();
}
NodeList bugFixList = fixesDoc.getElementsByTagName(BUG);
int length = bugFixList.getLength();
for (int i = 0; i < length; i++) {
loadBugResolution((Element) bugFixList.item(i), associations);
}
return associations;
}
private void loadBugResolution(Element bugFixElement, BugResolutionAssociations associations) {
String bugType = bugFixElement.getAttribute(BUG_TYPE);
if (bugType == null) {
FindbugsPlugin.getDefault().logError("No bug type found in BugResolution-Element.");
return;
}
Set<Class<? extends IMarkerResolution>> resolutionClasses = new HashSet<Class<? extends IMarkerResolution>>();
Set<IMarkerResolution> resolutions = new HashSet<IMarkerResolution>();
NodeList resolutionNodes = bugFixElement.getElementsByTagName(RESOLUTION);
int length = resolutionNodes.getLength();
for (int i = 0; i < length; i++) {
Class<? extends IMarkerResolution> resolutionClass = parseBugResolutionClass((Element) resolutionNodes.item(i));
if (resolutionClass == null) {
continue;
}
Map<String, String> attributes = parseAttributes((Element) resolutionNodes.item(i));
if (attributes.isEmpty()) {
resolutionClasses.add(resolutionClass);
} else {
IMarkerResolution resolution = instantiateBugResolution(resolutionClass, attributes);
if (resolution != null) {
resolutions.add(resolution);
}
}
}
associations.registerBugResolutions(bugType, resolutionClasses);
associations.addBugResolutions(bugType, resolutions);
}
@CheckForNull
private IMarkerResolution instantiateBugResolution(Class<? extends IMarkerResolution> resolutionClass,
Map<String, String> attributes) {
try {
IMarkerResolution resolution = resolutionClass.newInstance();
loadAttributes(resolution, attributes);
return resolution;
} catch (InstantiationException e) {
FindbugsPlugin.getDefault().logException(e,
"Failed to instaniate BugResolution '" + resolutionClass.getSimpleName() + "'.");
return null;
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
private void loadAttributes(IMarkerResolution resolution, Map<String, String> attributes) {
for (Entry<String, String> attr : attributes.entrySet()) {
String name = attr.getKey();
String value = attr.getValue();
loadAttribute(resolution, name, value);
}
}
private void loadAttribute(IMarkerResolution resolution, String name, String value) {
Class<? extends IMarkerResolution> typeClass = resolution.getClass();
for (Method method : typeClass.getMethods()) {
if (!isPropertySetterMethod(method, name)) {
continue;
}
try {
Class<?> paramType = method.getParameterTypes()[0];
Object val = parseValue(value, paramType);
method.invoke(resolution, val);
return;
} catch (IllegalArgumentException e) {
FindbugsPlugin.getDefault().logException(e, "Failed to parse attribute '" + name + " = " + value + "'.");
} catch (InvocationTargetException e) {
FindbugsPlugin.getDefault().logException(e, "Failed to load attribute '" + name + "' = " + value + "'.");
} catch (IllegalAccessException e) {
throw new IllegalStateException(e);
}
}
}
private boolean isPropertySetterMethod(Method method, String propertyName) {
return method.getParameterTypes().length == 1 && method.getName().startsWith("set") && method.getName().length() > 3
&& method.getName().substring(3).equalsIgnoreCase(propertyName);
}
/**
* Parse a given string value into the specified type. Only primitive types
* are currently supported.
*
* @param value
* the string value
* @param type
* the type of the parsed value.
* @return the parsed value.
* @throws IllegalArgumentException
* if the specified <CODE>type</CODE> isn't parseable.
*/
private <T> Object parseValue(String value, Class<T> type) throws IllegalArgumentException {
if (String.class == type) {
return value;
}
if (boolean.class == type || Boolean.class == type) {
return Boolean.valueOf(value);
}
if (int.class == type || Integer.class == type) {
return Integer.valueOf(value);
}
if (long.class == type || Long.class == type) {
return Long.valueOf(value);
}
if (float.class == type || Float.class == type) {
return Float.valueOf(value);
}
if (double.class == type || Double.class == type) {
return Double.valueOf(value);
}
throw new IllegalArgumentException("Unknown value type '" + type.getName() + "'.");
}
@CheckForNull
private Class<? extends IMarkerResolution> parseBugResolutionClass(Element resolutionElement) {
String className = resolutionElement.getAttribute(RESOLUTION_CLASS);
if (className == null) {
FindbugsPlugin.getDefault().logWarning("Missing a classname in the resolution element.");
return null;
}
try {
Class<?> resolutionClass = Class.forName(className);
if (IMarkerResolution.class.isAssignableFrom(resolutionClass)) {
return resolutionClass.asSubclass(IMarkerResolution.class);
}
FindbugsPlugin.getDefault().logError("BugResolution '" + className + "' not a IMarkerResolution");
} catch (ClassNotFoundException e) {
FindbugsPlugin.getDefault().logException(e, "BugResolution '" + className + "' not found.");
}
return null;
}
private Map<String, String> parseAttributes(Element resolutionElement) {
Map<String, String> attributes = new Hashtable<String, String>();
try {
NodeList attrList = resolutionElement.getElementsByTagName(ATTR);
int length = attrList.getLength();
for (int i = 0; i < length; i++) {
Element attrElement = (Element) attrList.item(i);
String name = attrElement.getAttribute(ATTR_NAME);
String value = attrElement.getTextContent();
if (false && SystemProperties.ASSERTIONS_ENABLED) {
if (value.equals(attrElement.getTextContent())) {
System.out.println("Expected " + attrElement.getTextContent() + ", got " + value);
}
}
if (name != null && value != null) {
attributes.put(name, value);
}
}
} catch (RuntimeException e) {
AnalysisContext.logError("Error parsing attributes", e);
}
return attributes;
}
@CheckForNull
private Document parseDocument(File xmlFile) {
if (!xmlFile.exists()) {
FindbugsPlugin.getDefault().logError("Need file '" + xmlFile.getPath() + "' but it doesn't exist");
return null;
}
if (!xmlFile.canRead()) {
FindbugsPlugin.getDefault().logError("Need file '" + xmlFile.getPath() + "' but it isn't readable");
return null;
}
try {
if (builder == null) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
builder = factory.newDocumentBuilder();
}
return builder.parse(xmlFile);
} catch (ParserConfigurationException e) {
throw new IllegalStateException(e);
} catch (SAXException e) {
FindbugsPlugin.getDefault().logException(e, "Failed to parse xml file '" + xmlFile.getPath() + "'.");
return null;
} catch (IOException e) {
FindbugsPlugin.getDefault().logException(e, "Failed to read the xml file '" + xmlFile.getPath() + "'.");
return null;
}
}
@CheckForNull
private Document parseDocument(@WillClose InputStream is) {
try {
if (builder == null) {
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
builder = factory.newDocumentBuilder();
}
return builder.parse(is);
} catch (ParserConfigurationException e) {
throw new IllegalStateException(e);
} catch (SAXException e) {
FindbugsPlugin.getDefault().logException(e, "Failed to parse xml file");
return null;
} catch (IOException e) {
FindbugsPlugin.getDefault().logException(e, "Failed to read the xml file");
return null;
} finally {
try {
is.close();
} catch (IOException e) {
assert true;
}
}
}
}