/* * FindBugs - Find bugs in Java programs * Copyright (C) 2003-2005 University of Maryland * * 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; import java.io.BufferedReader; import java.io.File; import java.io.FileInputStream; import java.io.IOException; import java.io.InputStream; import java.io.Reader; import java.net.HttpURLConnection; import java.net.MalformedURLException; import java.net.URI; import java.net.URISyntaxException; import java.net.URL; import java.net.URLClassLoader; import java.net.URLConnection; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Arrays; import java.util.Date; import java.util.HashSet; import java.util.Iterator; import java.util.LinkedList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Map.Entry; import java.util.Set; import java.util.TreeSet; import java.util.jar.Attributes; import java.util.jar.JarInputStream; import java.util.jar.Manifest; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import javax.annotation.CheckForNull; import javax.annotation.Nonnull; import javax.annotation.WillClose; import org.dom4j.Document; import org.dom4j.DocumentException; import org.dom4j.Element; import org.dom4j.Node; import org.dom4j.io.SAXReader; import edu.umd.cs.findbugs.ba.AnalysisContext; import edu.umd.cs.findbugs.charsets.UTF8; import edu.umd.cs.findbugs.classfile.IAnalysisEngineRegistrar; import edu.umd.cs.findbugs.cloud.Cloud; import edu.umd.cs.findbugs.cloud.CloudFactory; import edu.umd.cs.findbugs.cloud.CloudPlugin; import edu.umd.cs.findbugs.cloud.CloudPluginBuilder; import edu.umd.cs.findbugs.cloud.username.NameLookup; import edu.umd.cs.findbugs.internalAnnotations.DottedClassName; import edu.umd.cs.findbugs.io.IO; import edu.umd.cs.findbugs.plan.ByInterfaceDetectorFactorySelector; import edu.umd.cs.findbugs.plan.DetectorFactorySelector; import edu.umd.cs.findbugs.plan.DetectorOrderingConstraint; import edu.umd.cs.findbugs.plan.ReportingDetectorFactorySelector; import edu.umd.cs.findbugs.plan.SingleDetectorFactorySelector; import edu.umd.cs.findbugs.plugins.DuplicatePluginIdError; import edu.umd.cs.findbugs.plugins.DuplicatePluginIdException; import edu.umd.cs.findbugs.updates.UpdateChecker; import edu.umd.cs.findbugs.util.ClassName; import edu.umd.cs.findbugs.util.JavaWebStart; import edu.umd.cs.findbugs.util.Util; import edu.umd.cs.findbugs.xml.XMLUtil; /** * Loader for a FindBugs plugin. A plugin is a jar file containing two metadata * files, "findbugs.xml" and "messages.xml". Those files specify * <ul> * <li>the bug pattern Detector classes, * <li>the bug patterns detected (including all text for displaying detected * instances of those patterns), and * <li>the "bug codes" which group together related bug instances * </ul> * * <p> * The PluginLoader creates a Plugin object to store the Detector factories and * metadata. * </p> * * @author David Hovemeyer * @see Plugin * @see PluginException */ public class PluginLoader { private static final String XPATH_PLUGIN_SHORT_DESCRIPTION = "/MessageCollection/Plugin/ShortDescription"; private static final String XPATH_PLUGIN_WEBSITE = "/FindbugsPlugin/@website"; private static final String XPATH_PLUGIN_PROVIDER = "/FindbugsPlugin/@provider"; private static final String XPATH_PLUGIN_PLUGINID = "/FindbugsPlugin/@pluginid"; private static final boolean DEBUG = SystemProperties.getBoolean("findbugs.debug.PluginLoader"); static boolean lazyInitialization = false; static LinkedList<PluginLoader> partiallyInitialized = new LinkedList<PluginLoader>(); // Keep a count of how many plugins we've seen without a // "pluginid" attribute, so we can assign them all unique ids. private static int nextUnknownId; // ClassLoader used to load classes and resources private ClassLoader classLoader; private final ClassLoader classLoaderForResources; // The loaded Plugin private final Plugin plugin; private final boolean corePlugin; boolean initialPlugin; boolean cannotDisable; private boolean optionalPlugin; private final URL loadedFrom; private final String jarName; private final URI loadedFromUri; /** plugin Id for parent plugin */ String parentId; static HashSet<String> loadedPluginIds = new HashSet<String>(); static { if (DEBUG) { System.out.println("Debugging plugin loading. FindBugs version " + Version.getReleaseWithDateIfDev()); } loadInitialPlugins(); } /** * Constructor. * * @param url * the URL of the plugin Jar file * @throws PluginException * if the plugin cannot be fully loaded */ @Deprecated public PluginLoader(URL url) throws PluginException { this(url, toUri(url), null, false, true); } /** * Constructor. * * @param url * the URL of the plugin Jar file * @param parent * the parent classloader * @deprecated Use {@link #PluginLoader(URL,URI,ClassLoader,boolean,boolean)} instead */ @Deprecated public PluginLoader(URL url, ClassLoader parent) throws PluginException { this(url, toUri(url), parent, false, true); } public boolean hasParent() { return parentId != null && parentId.length() > 0; } /** * Constructor. * * @param url * the URL of the plugin Jar file * @param uri * @param parent * the parent classloader * @param isInitial * is this plugin loaded from one of the standard locations for plugins * @param optional * is this an optional plugin */ private PluginLoader(@Nonnull URL url, URI uri, ClassLoader parent, boolean isInitial, boolean optional) throws PluginException { URL[] loaderURLs = createClassloaderUrls(url); classLoaderForResources = new URLClassLoader(loaderURLs); loadedFrom = url; loadedFromUri = uri; jarName = getJarName(url); corePlugin = false; initialPlugin = isInitial; optionalPlugin = optional; plugin = init(); if (!hasParent()) { classLoader = new URLClassLoader(loaderURLs, parent); } else { if (parent != PluginLoader.class.getClassLoader()) throw new IllegalArgumentException("Can't specify parentid " + parentId + " and provide a seperate class loader"); Plugin parentPlugin = Plugin.getByPluginId(parentId); if (parentPlugin != null) { parent = parentPlugin.getClassLoader(); classLoader = new URLClassLoader(loaderURLs, parent); } } if (classLoader == null) { if (!lazyInitialization) throw new IllegalStateException("Can't find parent plugin " + parentId); partiallyInitialized.add(this); } else { loadPluginComponents(); Plugin.putPlugin(loadedFromUri, plugin); } } private static void finishLazyInitialization() { if (!lazyInitialization) throw new IllegalStateException("Not in lazy initialization mode"); while (!partiallyInitialized.isEmpty()) { boolean changed = false; LinkedList<String> unresolved = new LinkedList<String>(); Set<String> needed = new TreeSet<String>(); for (Iterator<PluginLoader> i = partiallyInitialized.iterator(); i.hasNext();) { PluginLoader pluginLoader = i.next(); String pluginId = pluginLoader.getPlugin().getPluginId(); assert pluginLoader.hasParent(); String parentid = pluginLoader.parentId; Plugin parent = Plugin.getByPluginId(parentid); if (parent != null) { i.remove(); try { URL[] loaderURLs = PluginLoader.createClassloaderUrls(pluginLoader.loadedFrom); pluginLoader.classLoader = new URLClassLoader(loaderURLs, parent.getClassLoader()); pluginLoader.loadPluginComponents(); Plugin.putPlugin(pluginLoader.loadedFromUri, pluginLoader.plugin); } catch (PluginException e) { throw new RuntimeException("Unable to load plugin " + pluginId, e); } changed = true; } else { unresolved.add(pluginId); needed.add(parentid); } } if (!changed) { String msg = "Unable to load parent plugins " + needed + " in order to load " + unresolved; System.err.println(msg); AnalysisContext.logError(msg); msg = "Available plugins are " + Plugin.getAllPluginIds(); System.err.println(msg); AnalysisContext.logError(msg); for (Iterator<PluginLoader> i = partiallyInitialized.iterator(); i.hasNext();) { Plugin.removePlugin(i.next().loadedFromUri); } partiallyInitialized.clear(); } } lazyInitialization = false; } /** * Patch for issue 3429143: allow plugins load classes/resources from 3rd * party jars * * @param url * plugin jar location as url * @return non empty list with url to be used for loading classes from given * plugin. If plugin jar contains standard Java manifest file, all * entries of its "Class-Path" attribute will be translated to the * jar-relative url's and added to the returned list. If plugin jar * does not contains a manifest, or manifest does not have * "Class-Path" attribute, the given argument will be the only entry * in the array. * @throws PluginException */ private static @Nonnull URL[] createClassloaderUrls(@Nonnull URL url) throws PluginException { List<URL> urls = new ArrayList<URL>(); urls.add(url); Manifest mf = null; File f = new File(url.getPath()); // default: try with jar/zip/war etc files if(!f.isDirectory()) { JarInputStream jis = null; try { jis = new JarInputStream(url.openStream()); mf = jis.getManifest(); } catch (IOException ioe) { throw new PluginException("Failed loading manifest for plugin jar: " + url, ioe); } finally { IO.close(jis); } } else { // If this is not a jar/zip/war etc file, can we can load from directory? // Allow plugins be loaded from "exploded jar" directories (e.g. while debugging // 3rd party FB plugin projects in Eclipse without packaging them to jars at all) File manifest = guessManifest(f); if(manifest != null){ FileInputStream is = null; try { is = new FileInputStream(manifest); mf = new Manifest(is); } catch (IOException e) { throw new PluginException("Failed loading manifest for plugin jar: " + url, e); } finally { IO.close(is); } } } if(mf != null){ try { addClassPathFromManifest(url, urls, mf); } catch (MalformedURLException e) { throw new PluginException("Failed loading manifest for plugin jar: " + url, e); } } return urls.toArray(new URL[urls.size()]); } private static void addClassPathFromManifest(@Nonnull URL url, @Nonnull List<URL> urls, @Nonnull Manifest mf) throws MalformedURLException { Attributes atts = mf.getMainAttributes(); if (atts == null) { return; } String classPath = atts.getValue(Attributes.Name.CLASS_PATH); if (classPath != null) { String jarRoot = url.toString(); jarRoot = jarRoot.substring(0, jarRoot.lastIndexOf("/") + 1); String[] jars = classPath.split(","); for (String jar : jars) { jar = jarRoot + jar.trim(); urls.add(new URL(jar)); } } } /** * Trying to find the manifest of "exploded plugin" in the current dir, "standard jar" manifest * location or "standard" Eclipse location (sibling to the current classpath) */ @CheckForNull private static File guessManifest(@Nonnull File parent) { File file = new File(parent, "MANIFEST.MF"); if(!file.isFile()){ file = new File(parent, "META-INF/MANIFEST.MF"); } if(!file.isFile()){ file = new File(parent, "../META-INF/MANIFEST.MF"); } if(file.isFile()){ return file; } return null; } /** * Constructor. Loads a plugin using the caller's class loader. This * constructor should only be used to load the "core" findbugs detectors, * which are built into findbugs.jar. * @throws PluginException */ @Deprecated public PluginLoader() throws PluginException { classLoader = getClass().getClassLoader(); classLoaderForResources = classLoader; corePlugin = true; initialPlugin = true; optionalPlugin = false; loadedFrom = computeCoreUrl(); try { loadedFromUri = loadedFrom.toURI(); } catch (URISyntaxException e) { throw new IllegalArgumentException("Failed to parse uri: " + loadedFrom); } jarName = getJarName(loadedFrom); plugin = init(); loadPluginComponents(); Plugin.putPlugin(null, plugin); } /** * Fake plugin. */ @Deprecated public PluginLoader(boolean fake, URL url) throws PluginException { classLoader = getClass().getClassLoader(); classLoaderForResources = classLoader; corePlugin = false; initialPlugin = true; optionalPlugin = false; loadedFrom = url; try { loadedFromUri = loadedFrom.toURI(); } catch (URISyntaxException e) { throw new IllegalArgumentException("Failed to parse uri: " + loadedFrom); } jarName = getJarName(loadedFrom); plugin = null; } private static URL computeCoreUrl() { URL from; String findBugsClassFile = ClassName.toSlashedClassName(FindBugs.class) + ".class"; URL me = FindBugs.class.getClassLoader().getResource(findBugsClassFile); if (DEBUG) System.out.println("FindBugs.class loaded from " + me); if(me == null) { throw new IllegalStateException("Failed to load " + findBugsClassFile); } try { String u = me.toString(); if (u.startsWith("jar:") && u.endsWith("!/" + findBugsClassFile)) { u = u.substring(4, u.indexOf("!/")); from = new URL(u); } else if (u.endsWith(findBugsClassFile)) { u = u.substring(0, u.indexOf(findBugsClassFile)); from = new URL(u); } else { throw new IllegalArgumentException("Unknown url shema: " + u); } } catch (MalformedURLException e) { throw new IllegalArgumentException("Failed to parse url: " + me); } if (DEBUG) System.out.println("Core class files loaded from " + from); return from; } public URL getURL() { return loadedFrom; } public URI getURI() { return loadedFromUri; } private static URI toUri(URL url) throws PluginException { try { return url.toURI(); } catch (URISyntaxException e) { throw new PluginException("Bad uri: " + url, e); } } private static String getJarName(URL url) { String location = url.getPath(); int i = location.lastIndexOf("/"); location = location.substring(i + 1); return location; } public ClassLoader getClassLoader() { if (classLoader == null) throw new IllegalStateException("Plugin not completely initialized; classloader not set yet"); return classLoader; } /** * Get the Plugin. * * @throws PluginException * if the plugin cannot be fully loaded */ public Plugin loadPlugin() throws PluginException { return getPlugin(); } public Plugin getPlugin() { if (plugin == null) throw new AssertionError("plugin not already loaded"); return plugin; } private static URL resourceFromPlugin(URL u, String args) throws MalformedURLException { String path = u.getPath(); if (path.endsWith(".zip") || path.endsWith(".jar")) { return new URL("jar:" + u.toString() + "!/" + args); } else if (path.endsWith("/")) { return new URL(u.toString() + args); } else { return new URL(u.toString() + "/" + args); } } /** * Get a resource using the URLClassLoader classLoader. We try findResource * first because (based on experiment) we can trust it to prefer resources * in the jarfile to resources on the filesystem. Simply calling * classLoader.getResource() allows the filesystem to override the jarfile, * which can mess things up if, for example, there is a findbugs.xml or * messages.xml in the current directory. * * @param name * resource to get * @return URL for the resource, or null if it could not be found */ public URL getResource(String name) { if (isCorePlugin()) { URL url = getCoreResource(name); if (url != null && IO.verifyURL(url)) return url; } if (loadedFrom != null) { try { URL url = resourceFromPlugin(loadedFrom, name); if (DEBUG) { System.out.println("Trying to load " + name + " from " + url); } if (IO.verifyURL(url)) return url; } catch (MalformedURLException e) { assert true; } } if (classLoaderForResources instanceof URLClassLoader) { URLClassLoader urlClassLoader = (URLClassLoader) classLoaderForResources; if (DEBUG) { System.out.println("Trying to load " + name + " using URLClassLoader.findResource"); System.out.println(" from urls: " + Arrays.asList(urlClassLoader.getURLs())); } URL url = urlClassLoader.findResource(name); if (url == null) url = urlClassLoader.findResource("/" + name); if (IO.verifyURL(url)) return url; } if (DEBUG) { System.out.println("Trying to load " + name + " using ClassLoader.getResource"); } URL url = classLoaderForResources.getResource(name); if (url == null) url = classLoaderForResources.getResource("/" + name); if (IO.verifyURL(url)) return url; return null; } static @CheckForNull URL getCoreResource(String name) { URL u = loadFromFindBugsPluginDir(name); if (u != null) return u; u = loadFromFindBugsEtcDir(name); if (u != null) return u; u = PluginLoader.class.getResource(name); if (u != null) return u; u = PluginLoader.class.getResource("/"+name); return u; } public static @CheckForNull URL loadFromFindBugsEtcDir(String name) { String findBugsHome = DetectorFactoryCollection.getFindBugsHome(); if (findBugsHome != null) { File f = new File(new File(new File(findBugsHome), "etc"), name); if (f.canRead()) { try { return f.toURL(); } catch (MalformedURLException e) { // ignore it assert true; } } } return null; } public static @CheckForNull URL loadFromFindBugsPluginDir(String name) { String findBugsHome = DetectorFactoryCollection.getFindBugsHome(); if (findBugsHome != null) { File f = new File(new File(new File(findBugsHome), "plugin"), name); if (f.canRead()) { try { return f.toURI().toURL(); } catch (MalformedURLException e) { // ignore it assert true; } } } return null; } private static <T> Class<? extends T> getClass(ClassLoader loader, @DottedClassName String className, Class<T> type) throws PluginException { try { return loader.loadClass(className).asSubclass(type); } catch (ClassNotFoundException e) { throw new PluginException("Unable to load " + className, e); } catch (ClassCastException e) { throw new PluginException("Cannot cast " + className + " to " + type.getName(), e); } } private Plugin init() throws PluginException { if (DEBUG) System.out.println("Loading plugin from " + loadedFrom); // Plugin descriptor (a.k.a, "findbugs.xml"). Defines // the bug detectors and bug patterns that the plugin provides. Document pluginDescriptor = getPluginDescriptor(); List<Document> messageCollectionList = getMessageDocuments(); Plugin constructedPlugin = constructMinimalPlugin(pluginDescriptor, messageCollectionList); // Success! if (DEBUG) System.out.println("Loaded " + constructedPlugin.getPluginId() + " from " + loadedFrom); return constructedPlugin; } private void loadPluginComponents() throws PluginException { Document pluginDescriptor = getPluginDescriptor(); List<Document> messageCollectionList = getMessageDocuments(); List<Node> cloudNodeList = XMLUtil.selectNodes(pluginDescriptor, "/FindbugsPlugin/Cloud"); for (Node cloudNode : cloudNodeList) { String cloudClassname = cloudNode.valueOf("@cloudClass"); String cloudId = cloudNode.valueOf("@id"); String usernameClassname = cloudNode.valueOf("@usernameClass"); boolean onlineStorage = Boolean.valueOf(cloudNode.valueOf("@onlineStorage")); String propertiesLocation = cloudNode.valueOf("@properties"); boolean disabled = Boolean.valueOf(cloudNode.valueOf("@disabled")) && !cloudId.equals(CloudFactory.DEFAULT_CLOUD); if (disabled) continue; boolean hidden = Boolean.valueOf(cloudNode.valueOf("@hidden")) && !cloudId.equals(CloudFactory.DEFAULT_CLOUD); Class<? extends Cloud> cloudClass = getClass(classLoader, cloudClassname, Cloud.class); Class<? extends NameLookup> usernameClass = getClass(classLoader, usernameClassname, NameLookup.class); Node cloudMessageNode = findMessageNode(messageCollectionList, "/MessageCollection/Cloud[@id='" + cloudId + "']", "Missing Cloud description for cloud " + cloudId); String description = getChildText(cloudMessageNode, "Description").trim(); String details = getChildText(cloudMessageNode, "Details").trim(); PropertyBundle properties = new PropertyBundle(); if (propertiesLocation != null && propertiesLocation.length() > 0) { URL properiesURL = classLoader.getResource(propertiesLocation); if (properiesURL == null) continue; properties.loadPropertiesFromURL(properiesURL); } List<Node> propertyNodes = XMLUtil.selectNodes(cloudNode, "Property"); for (Node node : propertyNodes) { String key = node.valueOf("@key"); String value = node.getText().trim(); properties.setProperty(key, value); } CloudPlugin cloudPlugin = new CloudPluginBuilder().setFindbugsPluginId(plugin.getPluginId()).setCloudid(cloudId).setClassLoader(classLoader) .setCloudClass(cloudClass).setUsernameClass(usernameClass).setHidden(hidden).setProperties(properties) .setDescription(description).setDetails(details).setOnlineStorage(onlineStorage).createCloudPlugin(); plugin.addCloudPlugin(cloudPlugin); } // Create PluginComponents try { List<Node> componentNodeList = XMLUtil.selectNodes(pluginDescriptor, "/FindbugsPlugin/PluginComponent"); for (Node componentNode : componentNodeList) { @DottedClassName String componentKindname = componentNode.valueOf("@componentKind"); if (componentKindname == null) { throw new PluginException("Missing @componentKind for " + plugin.getPluginId() + " loaded from " + loadedFrom); } @DottedClassName String componentClassname = componentNode.valueOf("@componentClass"); if (componentClassname == null) { throw new PluginException("Missing @componentClassname for " + plugin.getPluginId() + " loaded from " + loadedFrom); } String componentId = componentNode.valueOf("@id"); if (componentId == null) throw new PluginException("Missing @id for " + plugin.getPluginId() + " loaded from " + loadedFrom); try { String propertiesLocation = componentNode.valueOf("@properties"); boolean disabled = Boolean.valueOf(componentNode.valueOf("@disabled")); Node filterMessageNode = findMessageNode(messageCollectionList, "/MessageCollection/PluginComponent[@id='" + componentId + "']", "Missing Cloud description for PluginComponent " + componentId); String description = getChildText(filterMessageNode, "Description").trim(); String details = getChildText(filterMessageNode, "Details").trim(); PropertyBundle properties = new PropertyBundle(); if (propertiesLocation != null && propertiesLocation.length() > 0) { URL properiesURL = classLoaderForResources.getResource(propertiesLocation); if (properiesURL == null) { AnalysisContext.logError("Could not load properties for " + plugin.getPluginId() + " component " + componentId + " from " + propertiesLocation); continue; } properties.loadPropertiesFromURL(properiesURL); } List<Node> propertyNodes = XMLUtil.selectNodes(componentNode, "Property"); for (Node node : propertyNodes) { String key = node.valueOf("@key"); String value = node.getText(); properties.setProperty(key, value); } Class<?> componentKind = classLoader.loadClass(componentKindname); loadComponentPlugin(plugin, componentKind, componentClassname, componentId, disabled, description, details, properties); } catch (RuntimeException e) { AnalysisContext.logError("Unable to load ComponentPlugin " + componentId + " : " + componentClassname + " implementing " + componentKindname, e); } } // Create FindBugsMains if (!FindBugs.isNoMains()) { List<Node> findBugsMainList = XMLUtil.selectNodes(pluginDescriptor, "/FindbugsPlugin/FindBugsMain"); for (Node main : findBugsMainList) { String className = main.valueOf("@class"); if (className == null) { throw new PluginException("Missing @class for FindBugsMain in plugin" + plugin.getPluginId() + " loaded from " + loadedFrom); } String cmd = main.valueOf("@cmd"); if (cmd == null) { throw new PluginException("Missing @cmd for for FindBugsMain in plugin " + plugin.getPluginId() + " loaded from " + loadedFrom); } String kind = main.valueOf("@kind"); boolean analysis = Boolean.valueOf(main.valueOf("@analysis")); Element mainMessageNode = (Element) findMessageNode(messageCollectionList, "/MessageCollection/FindBugsMain[@cmd='" + cmd // + " and @class='" + className +"']/Description", "Missing FindBugsMain description for cmd " + cmd); String description = mainMessageNode.getTextTrim(); try { Class<?> mainClass = classLoader.loadClass(className); plugin.addFindBugsMain(mainClass, cmd, description, kind, analysis); } catch (Exception e) { String msg = "Unable to load FindBugsMain " + cmd + " : " + className + " in plugin " + plugin.getPluginId() + " loaded from " + loadedFrom; PluginException e2 = new PluginException(msg, e); AnalysisContext.logError(msg, e2); } } } List<Node> detectorNodeList = XMLUtil.selectNodes(pluginDescriptor, "/FindbugsPlugin/Detector"); int detectorCount = 0; for (Node detectorNode : detectorNodeList) { String className = detectorNode.valueOf("@class"); String speed = detectorNode.valueOf("@speed"); String disabled = detectorNode.valueOf("@disabled"); String reports = detectorNode.valueOf("@reports"); String requireJRE = detectorNode.valueOf("@requirejre"); String hidden = detectorNode.valueOf("@hidden"); if (speed == null || speed.length() == 0) { speed = "fast"; } // System.out.println("Found detector: class="+className+", disabled="+disabled); // Create DetectorFactory for the detector Class<?> detectorClass = null; if (!FindBugs.isNoAnalysis()) { detectorClass = classLoader.loadClass(className); if (!Detector.class.isAssignableFrom(detectorClass) && !Detector2.class.isAssignableFrom(detectorClass)) { throw new PluginException("Class " + className + " does not implement Detector or Detector2"); } } DetectorFactory factory = new DetectorFactory(plugin, className, detectorClass, !disabled.equals("true"), speed, reports, requireJRE); if (Boolean.valueOf(hidden).booleanValue()) { factory.setHidden(true); } factory.setPositionSpecifiedInPluginDescriptor(detectorCount++); plugin.addDetectorFactory(factory); // Find Detector node in one of the messages files, // to get the detail HTML. Node node = findMessageNode(messageCollectionList, "/MessageCollection/Detector[@class='" + className + "']/Details", "Missing Detector description for detector " + className); Element details = (Element) node; String detailHTML = details.getText(); StringBuilder buf = new StringBuilder(); buf.append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n"); buf.append("<HTML><HEAD><TITLE>Detector Description</TITLE></HEAD><BODY>\n"); buf.append(detailHTML); buf.append("</BODY></HTML>\n"); factory.setDetailHTML(buf.toString()); } } catch (ClassNotFoundException e) { throw new PluginException("Could not instantiate detector class: " + e, e); } // Create ordering constraints Node orderingConstraintsNode = pluginDescriptor.selectSingleNode("/FindbugsPlugin/OrderingConstraints"); if (orderingConstraintsNode != null) { // Get inter-pass and intra-pass constraints List<Element> elements = XMLUtil.selectNodes(orderingConstraintsNode, "./SplitPass|./WithinPass"); for (Element constraintElement : elements) { // Create the selectors which determine which detectors are // involved in the constraint DetectorFactorySelector earlierSelector = getConstraintSelector(constraintElement, plugin, "Earlier"); DetectorFactorySelector laterSelector = getConstraintSelector(constraintElement, plugin, "Later"); // Create the constraint DetectorOrderingConstraint constraint = new DetectorOrderingConstraint(earlierSelector, laterSelector); // Keep track of which constraints are single-source constraint.setSingleSource(earlierSelector instanceof SingleDetectorFactorySelector); // Add the constraint to the plugin if (constraintElement.getName().equals("SplitPass")) { plugin.addInterPassOrderingConstraint(constraint); } else { plugin.addIntraPassOrderingConstraint(constraint); } } } // register global Category descriptions List<Node> categoryNodeListGlobal = XMLUtil.selectNodes(pluginDescriptor, "/FindbugsPlugin/BugCategory"); for(Node categoryNode : categoryNodeListGlobal) { String key = categoryNode.valueOf("@category"); if (key.equals("")) { throw new PluginException("BugCategory element with missing category attribute"); } BugCategory bc = plugin.addOrCreateBugCategory(key); boolean hidden = Boolean.valueOf(categoryNode.valueOf("@hidden")); if (hidden) { bc.setHidden(hidden); } } for (Document messageCollection : messageCollectionList) { List<Node> categoryNodeList = XMLUtil.selectNodes(messageCollection, "/MessageCollection/BugCategory"); if (DEBUG) System.out.println("found " + categoryNodeList.size() + " categories in " + plugin.getPluginId()); for (Node categoryNode : categoryNodeList) { String key = categoryNode.valueOf("@category"); if (key.equals("")) { throw new PluginException("BugCategory element with missing category attribute"); } BugCategory bc = plugin.addOrCreateBugCategory(key); String shortDesc = getChildText(categoryNode, "Description"); bc.setShortDescription(shortDesc); try { String abbrev = getChildText(categoryNode, "Abbreviation"); if (bc.getAbbrev() == null) { bc.setAbbrev(abbrev); if (DEBUG) System.out.println("category " + key + " abbrev -> " + abbrev); } else if (DEBUG) System.out.println("rejected abbrev '" + abbrev + "' for category " + key + ": " + bc.getAbbrev()); } catch (PluginException pe) { if (DEBUG) System.out.println("missing Abbreviation for category " + key + "/" + shortDesc); // do nothing else -- Abbreviation is required, but handle // its omission gracefully } try { String details = getChildText(categoryNode, "Details"); if (bc.getDetailText() == null) { bc.setDetailText(details); if (DEBUG) System.out.println("category " + key + " details -> " + details); } else if (DEBUG) System.out.println("rejected details [" + details + "] for category " + key + ": [" + bc.getDetailText() + ']'); } catch (PluginException pe) { // do nothing -- LongDescription is optional } } } // Create BugPatterns List<Node> bugPatternNodeList = XMLUtil.selectNodes(pluginDescriptor, "/FindbugsPlugin/BugPattern"); for (Node bugPatternNode : bugPatternNodeList) { String type = bugPatternNode.valueOf("@type"); String abbrev = bugPatternNode.valueOf("@abbrev"); String category = bugPatternNode.valueOf("@category"); String experimental = bugPatternNode.valueOf("@experimental"); // Find the matching element in messages.xml (or translations) String query = "/MessageCollection/BugPattern[@type='" + type + "']"; Node messageNode = findMessageNode(messageCollectionList, query, "messages.xml missing BugPattern element for type " + type); String shortDesc = getChildText(messageNode, "ShortDescription"); String longDesc = getChildText(messageNode, "LongDescription"); String detailText = getChildText(messageNode, "Details"); int cweid = 0; try { String cweString = bugPatternNode.valueOf("@cweid"); if (cweString.length() > 0) cweid = Integer.parseInt(cweString); } catch (RuntimeException e) { assert true; // ignore } BugPattern bugPattern = new BugPattern(type, abbrev, category, Boolean.valueOf(experimental).booleanValue(), shortDesc, longDesc, detailText, cweid); try { String deprecatedStr = bugPatternNode.valueOf("@deprecated"); boolean deprecated = deprecatedStr.length() > 0 && Boolean.valueOf(deprecatedStr).booleanValue(); if (deprecated) { bugPattern.setDeprecated(deprecated); } } catch (RuntimeException e) { assert true; // ignore } plugin.addBugPattern(bugPattern); } // Create BugCodes Set<String> definedBugCodes = new HashSet<String>(); for (Document messageCollection : messageCollectionList) { List<Node> bugCodeNodeList = XMLUtil.selectNodes(messageCollection, "/MessageCollection/BugCode"); for (Node bugCodeNode : bugCodeNodeList) { String abbrev = bugCodeNode.valueOf("@abbrev"); if (abbrev.equals("")) { throw new PluginException("BugCode element with missing abbrev attribute"); } if (definedBugCodes.contains(abbrev)) { continue; } String description = bugCodeNode.getText(); String query = "/FindbugsPlugin/BugCode[@abbrev='" + abbrev + "']"; Node fbNode = pluginDescriptor.selectSingleNode(query); int cweid = 0; if (fbNode != null) try { cweid = Integer.parseInt(fbNode.valueOf("@cweid")); } catch (RuntimeException e) { assert true; // ignore } BugCode bugCode = new BugCode(abbrev, description, cweid); plugin.addBugCode(bugCode); definedBugCodes.add(abbrev); } } // If an engine registrar is specified, make a note of its classname Node node = pluginDescriptor.selectSingleNode("/FindbugsPlugin/EngineRegistrar"); if (node != null) { String engineClassName = node.valueOf("@class"); if (engineClassName == null) { throw new PluginException("EngineRegistrar element with missing class attribute"); } try { Class<?> engineRegistrarClass = classLoader.loadClass(engineClassName); if (!IAnalysisEngineRegistrar.class.isAssignableFrom(engineRegistrarClass)) { throw new PluginException(engineRegistrarClass + " does not implement IAnalysisEngineRegistrar"); } plugin.setEngineRegistrarClass(engineRegistrarClass .<IAnalysisEngineRegistrar> asSubclass(IAnalysisEngineRegistrar.class)); } catch (ClassNotFoundException e) { throw new PluginException("Could not instantiate analysis engine registrar class: " + e, e); } } try { URL bugRankURL = getResource(BugRanker.FILENAME); if (bugRankURL == null) { // see // https://sourceforge.net/tracker/?func=detail&aid=2816102&group_id=96405&atid=614693 // plugin can not have bugrank.txt. In this case, an empty // bugranker will be created if (DEBUG) System.out.println("No " + BugRanker.FILENAME + " for plugin " + plugin.getPluginId()); } BugRanker ranker = new BugRanker(bugRankURL); plugin.setBugRanker(ranker); } catch (IOException e) { throw new PluginException("Couldn't parse \"" + BugRanker.FILENAME + "\"", e); } } private Plugin constructMinimalPlugin(Document pluginDescriptor, List<Document> messageCollectionList) throws DuplicatePluginIdError { // Get the unique plugin id (or generate one, if none is present) // Unique plugin id String pluginId = pluginDescriptor.valueOf(XPATH_PLUGIN_PLUGINID); if (pluginId.equals("")) { synchronized (PluginLoader.class) { pluginId = "plugin" + nextUnknownId++; } } cannotDisable = Boolean.parseBoolean(pluginDescriptor.valueOf("/FindbugsPlugin/@cannotDisable")); String de = pluginDescriptor.valueOf("/FindbugsPlugin/@defaultenabled"); if (de != null && de.toLowerCase().trim().equals("false")) { optionalPlugin = true; } if (optionalPlugin) { cannotDisable = false; } if (!loadedPluginIds.add(pluginId)) { Plugin existingPlugin = Plugin.getByPluginId(pluginId); URL u = existingPlugin == null ? null : existingPlugin.getPluginLoader().getURL(); if (cannotDisable && initialPlugin) { throw new DuplicatePluginIdError(pluginId, loadedFrom, u); } else { throw new DuplicatePluginIdException(pluginId, loadedFrom, u); } } parentId = pluginDescriptor.valueOf("/FindbugsPlugin/@parentid"); String version = pluginDescriptor.valueOf("/FindbugsPlugin/@version"); String releaseDate = pluginDescriptor.valueOf("/FindbugsPlugin/@releaseDate"); if ((releaseDate == null || releaseDate.length() == 0) && isCorePlugin()) { releaseDate = Version.CORE_PLUGIN_RELEASE_DATE; } // Create the Plugin object (but don't assign to the plugin field yet, // since we're still not sure if everything will load correctly) Date parsedDate = parseDate(releaseDate); Plugin constructedPlugin = new Plugin(pluginId, version, parsedDate, this, !optionalPlugin, cannotDisable); // Set provider and website, if specified String provider = pluginDescriptor.valueOf(XPATH_PLUGIN_PROVIDER).trim(); if (!provider.equals("")) { constructedPlugin.setProvider(provider); } String website = pluginDescriptor.valueOf(XPATH_PLUGIN_WEBSITE).trim(); if (!website.equals("")) { try { constructedPlugin.setWebsite(website); } catch (URISyntaxException e1) { AnalysisContext.logError("Plugin " + constructedPlugin.getPluginId() + " has invalid website: " + website, e1); } } String updateUrl = pluginDescriptor.valueOf("/FindbugsPlugin/@update-url").trim(); if (!updateUrl.equals("")) { try { constructedPlugin.setUpdateUrl(updateUrl); } catch (URISyntaxException e1) { AnalysisContext.logError("Plugin " + constructedPlugin.getPluginId() + " has invalid update check URL: " + website, e1); } } // Set short description, if specified Node pluginShortDesc = null; try { pluginShortDesc = findMessageNode(messageCollectionList, XPATH_PLUGIN_SHORT_DESCRIPTION, "no plugin description"); } catch (PluginException e) { // Missing description is not fatal, so ignore } if (pluginShortDesc != null) { constructedPlugin.setShortDescription(pluginShortDesc.getText().trim()); } Node detailedDescription = null; try { detailedDescription = findMessageNode(messageCollectionList, "/MessageCollection/Plugin/Details", "no plugin description"); } catch (PluginException e) { // Missing description is not fatal, so ignore } if (detailedDescription != null) { constructedPlugin.setDetailedDescription(detailedDescription.getText().trim()); } List<Node> globalOptionNodes = XMLUtil.selectNodes(pluginDescriptor, "/FindbugsPlugin/GlobalOptions/Property"); for(Node optionNode : globalOptionNodes) { String key = optionNode.valueOf("@key"); String value = optionNode.getText().trim(); constructedPlugin.setMyGlobalOption(key, value); } return constructedPlugin; } public Document getPluginDescriptor() throws PluginException, PluginDoesntContainMetadataException { Document pluginDescriptor; // Read the plugin descriptor String name = "findbugs.xml"; URL findbugsXML_URL = getResource(name); if (findbugsXML_URL == null) { throw new PluginException("Couldn't find \"" + name + "\" in plugin " + this); } if (DEBUG) System.out.println("PluginLoader found " + name + " at: " + findbugsXML_URL); if (jarName != null && !findbugsXML_URL.toString().contains(jarName) && !(corePlugin && findbugsXML_URL.toString().endsWith("etc/findbugs.xml"))) { String classloaderName = classLoader.getClass().getName(); if (classLoader instanceof URLClassLoader) { classloaderName += Arrays.asList(((URLClassLoader) classLoader).getURLs()); } throw new PluginDoesntContainMetadataException((corePlugin ? "Core plugin" : "Plugin ") + jarName + " doesn't contain findbugs.xml; got " + findbugsXML_URL + " from " + classloaderName); } SAXReader reader = new SAXReader(); Reader r = null; try { r = UTF8.bufferedReader(findbugsXML_URL.openStream()); pluginDescriptor = reader.read(r); } catch (DocumentException e) { throw new PluginException("Couldn't parse \"" + findbugsXML_URL + "\" using " + reader.getClass().getName(), e); } catch (IOException e) { throw new PluginException("Couldn't open \"" + findbugsXML_URL + "\"", e); } finally { IO.close(r); } return pluginDescriptor; } private static List<String> getPotentialMessageFiles() { // Load the message collections Locale locale = Locale.getDefault(); String language = locale.getLanguage(); String country = locale.getCountry(); List<String> potential = new ArrayList<String>(3); if (country != null) potential.add("messages_" + language + "_" + country + ".xml"); potential.add("messages_" + language + ".xml"); potential.add("messages.xml"); return potential; } private List<Document> getMessageDocuments() throws PluginException { // List of message translation files in decreasing order of precedence ArrayList<Document> messageCollectionList = new ArrayList<Document>(); PluginException caught = null; for (String m : getPotentialMessageFiles()) try { addCollection(messageCollectionList, m); } catch (PluginException e) { caught = e; AnalysisContext.logError( "Error loading localized message file:" + m, e); } if (messageCollectionList.isEmpty()) { if (caught != null) { throw caught; } throw new PluginException("No message.xml files found"); } return messageCollectionList; } private <T> void loadComponentPlugin(@SuppressWarnings("hiding") Plugin plugin, Class<T> componentKind, @DottedClassName String componentClassname, String filterId, boolean disabled, String description, String details, PropertyBundle properties) throws PluginException { Class<? extends T> componentClass = null; if (!FindBugs.isNoAnalysis() || componentKind == edu.umd.cs.findbugs.bugReporter.BugReporterDecorator.class) { componentClass = getClass(classLoader, componentClassname, componentKind); } ComponentPlugin<T> componentPlugin = new ComponentPlugin<T>(plugin, filterId, classLoader, componentClass, properties, !disabled, description, details); plugin.addComponentPlugin(componentKind, componentPlugin); } private static Date parseDate(String releaseDate) { if (releaseDate == null || releaseDate.length() == 0) { return null; } try { SimpleDateFormat releaseDateFormat = new SimpleDateFormat(UpdateChecker.PLUGIN_RELEASE_DATE_FMT, Locale.ENGLISH); Date result = releaseDateFormat.parse(releaseDate); return result; } catch (ParseException e) { AnalysisContext.logError("unable to parse date " + releaseDate, e); return null; } } private static DetectorFactorySelector getConstraintSelector(Element constraintElement, Plugin plugin, String singleDetectorElementName/* * , String * detectorCategoryElementName */) throws PluginException { Node node = constraintElement.selectSingleNode("./" + singleDetectorElementName); if (node != null) { String detectorClass = node.valueOf("@class"); return new SingleDetectorFactorySelector(plugin, detectorClass); } node = constraintElement.selectSingleNode("./" + singleDetectorElementName + "Category"); if (node != null) { boolean spanPlugins = Boolean.valueOf(node.valueOf("@spanplugins")).booleanValue(); String categoryName = node.valueOf("@name"); if (!categoryName.equals("")) { if (categoryName.equals("reporting")) { return new ReportingDetectorFactorySelector(spanPlugins ? null : plugin); } else if (categoryName.equals("training")) { return new ByInterfaceDetectorFactorySelector(spanPlugins ? null : plugin, TrainingDetector.class); } else if (categoryName.equals("interprocedural")) { return new ByInterfaceDetectorFactorySelector(spanPlugins ? null : plugin, InterproceduralFirstPassDetector.class); } else { throw new PluginException("Invalid category name " + categoryName + " in constraint selector node"); } } } node = constraintElement.selectSingleNode("./" + singleDetectorElementName + "Subtypes"); if (node != null) { boolean spanPlugins = Boolean.valueOf(node.valueOf("@spanplugins")).booleanValue(); String superName = node.valueOf("@super"); if (!superName.equals("")) { try { Class<?> superClass = Class.forName(superName); return new ByInterfaceDetectorFactorySelector(spanPlugins ? null : plugin, superClass); } catch (ClassNotFoundException e) { throw new PluginException("Unknown class " + superName + " in constraint selector node"); } } } throw new PluginException("Invalid constraint selector node"); } private void addCollection(List<Document> messageCollectionList, String filename) throws PluginException { URL messageURL = getResource(filename); if (messageURL != null) { SAXReader reader = new SAXReader(); try { Reader stream = UTF8.bufferedReader(messageURL.openStream()); Document messageCollection; try { messageCollection = reader.read(stream); } finally { stream.close(); } messageCollectionList.add(messageCollection); } catch (Exception e) { throw new PluginException("Couldn't parse \"" + messageURL + "\"", e); } } } private static Node findMessageNode(List<Document> messageCollectionList, String xpath, String missingMsg) throws PluginException { for (Document document : messageCollectionList) { Node node = document.selectSingleNode(xpath); if (node != null) { return node; } } throw new PluginException(missingMsg); } private static String findMessageText(List<Document> messageCollectionList, String xpath, String missingMsg) { for (Document document : messageCollectionList) { Node node = document.selectSingleNode(xpath); if (node != null) { return node.getText().trim(); } } return missingMsg; } private static String getChildText(Node node, String childName) throws PluginException { Node child = node.selectSingleNode(childName); if (child == null) { throw new PluginException("Could not find child \"" + childName + "\" for node"); } return child.getText(); } public static PluginLoader getPluginLoader(URL url, ClassLoader parent, boolean isInitial, boolean optional) throws PluginException { URI uri = toUri(url); Plugin plugin = Plugin.getPlugin(uri); if (plugin != null) { PluginLoader loader = plugin.getPluginLoader(); assert loader.getClassLoader().getParent().equals(parent); return loader; } return new PluginLoader(url, uri, parent, isInitial, optional); } @Nonnull public static synchronized PluginLoader getCorePluginLoader() { Plugin plugin = Plugin.getPlugin(null); if (plugin != null) { return plugin.getPluginLoader(); } throw new IllegalStateException("Core plugin not loaded yet!"); } public boolean isCorePlugin() { return corePlugin; } static void installStandardPlugins() { String homeDir = DetectorFactoryCollection.getFindBugsHome(); if (homeDir == null) { return; } File home = new File(homeDir); loadPlugins(home); } private static void loadPlugins(File home) { if (home.canRead() && home.isDirectory()) { loadPluginsInDir(new File(home, "plugin"), false); loadPluginsInDir(new File(home, "optionalPlugin"), true); } } static void installUserInstalledPlugins() { String homeDir = System.getProperty("user.home"); if (homeDir == null) { return; } File homeFindBugs = new File(new File(homeDir), ".findbugs"); loadPlugins(homeFindBugs); } private static void loadPluginsInDir(File pluginDir, boolean optional) { File[] contentList = pluginDir.listFiles(); if (contentList == null) { return; } for (File file : contentList) { if (file.getName().endsWith(".jar")) { try { URL url = file.toURI().toURL(); if (IO.verifyURL(url)) { loadInitialPlugin(url, true, optional); if (FindBugs.DEBUG) System.out.println("Found plugin: " + file.toString()); } } catch (MalformedURLException e) { } } } } static synchronized void loadInitialPlugins() { lazyInitialization = true; loadCorePlugin(); if (JavaWebStart.isRunningViaJavaWebstart()) { installWebStartPlugins(); } else { installStandardPlugins(); installUserInstalledPlugins(); } Set<Entry<Object, Object>> entrySet = SystemProperties.getAllProperties().entrySet(); for (Map.Entry<?, ?> e : entrySet) { if (e.getKey() instanceof String && e.getValue() instanceof String && ((String) e.getKey()).startsWith("findbugs.plugin.")) { try { String value = (String) e.getValue(); if (value.startsWith("file:") && !value.endsWith(".jar") && !value.endsWith("/")) { value += "/"; } URL url = JavaWebStart.resolveRelativeToJnlpCodebase(value); System.out.println("Loading " + e.getKey() + " from " + url); loadInitialPlugin(url, true, false); } catch (MalformedURLException e1) { AnalysisContext.logError(String.format("Bad URL for plugin: %s=%s", e.getKey(), e.getValue()), e1); } } } if (Plugin.getAllPlugins().size() > 1 && JavaWebStart.isRunningViaJavaWebstart()) { // disable security manager; plugins cause problems // http://lopica.sourceforge.net/faq.html // URL policyUrl = // Thread.currentThread().getContextClassLoader().getResource("my.java.policy"); // Policy.getPolicy().refresh(); try { System.setSecurityManager(null); } catch (Throwable e) { assert true; // keep going } } finishLazyInitialization(); } private static void loadCorePlugin() { try { Plugin plugin = Plugin.getPlugin(null); if (plugin != null) { throw new IllegalStateException("Already loaded"); } PluginLoader pluginLoader = new PluginLoader(); plugin = pluginLoader.getPlugin(); Plugin.putPlugin(null, plugin); } catch (PluginException e1) { throw new IllegalStateException("Unable to load core plugin", e1); } } private static void loadInitialPlugin(URL u, boolean initial, boolean optional) { try { getPluginLoader(u, PluginLoader.class.getClassLoader(), initial, optional); } catch (DuplicatePluginIdException ignored) { assert true; }catch (PluginException e) { AnalysisContext.logError("Unable to load plugin from " + u, e); if (DEBUG) e.printStackTrace(); } } static void installWebStartPlugins() { URL pluginListProperties = getCoreResource("pluginlist.properties"); BufferedReader in = null; if (pluginListProperties != null) { try { DetectorFactoryCollection.jawsDebugMessage(pluginListProperties.toString()); URL base = getUrlBase(pluginListProperties); in = UTF8.bufferedReader(pluginListProperties.openStream()); while (true) { String plugin = in.readLine(); if (plugin == null) { break; } URL url = new URL(base, plugin); try { URLConnection connection = url.openConnection(); String contentType = connection.getContentType(); DetectorFactoryCollection.jawsDebugMessage("contentType : " + contentType); if (connection instanceof HttpURLConnection) { ((HttpURLConnection) connection).disconnect(); } loadInitialPlugin(url, true, false); } catch (Exception e) { DetectorFactoryCollection.jawsDebugMessage("error loading " + url + " : " + e.getMessage()); } } } catch (Exception e) { DetectorFactoryCollection.jawsDebugMessage("error : " + e.getMessage()); } finally { Util.closeSilently(in); } } } private static URL getUrlBase(URL pluginListProperties) throws MalformedURLException { String urlname = pluginListProperties.toString(); URL base = pluginListProperties; int pos = urlname.indexOf("!/"); if (pos >= 0 && urlname.startsWith("jar:")) { urlname = urlname.substring(4, pos); base = new URL(urlname); } return base; } @Override public String toString() { if (plugin == null) { return String.format("PluginLoader(%s)", loadedFrom); } return String.format("PluginLoader(%s, %s)", plugin.getPluginId(), loadedFrom); } static public class Summary { public final String id; public final String description; public final String provider; public final String webbsite; public Summary(String id, String description, String provider, String website) { super(); this.id = id; this.description = description; this.provider = provider; this.webbsite = website; } } public static Summary validate(File file) throws IllegalArgumentException { String path = file.getPath(); if (!file.getName().endsWith(".jar")) { String message = "File " + path + " is not a .jar file"; throw new IllegalArgumentException(message); } if (!file.isFile() || !file.canRead()) { String message = "File " + path + " is not a file or is not readable"; throw new IllegalArgumentException(message); } if (file.length() == 0) { String message = "File " + path + " is empty"; throw new IllegalArgumentException(message); } ZipFile zip = null; try { zip = new ZipFile(file); ZipEntry findbugsXML = zip.getEntry("findbugs.xml"); if (findbugsXML == null) { throw new IllegalArgumentException( "plugin doesn't contain a findbugs.xml file"); } ZipEntry messagesXML = zip.getEntry("messages.xml"); if (messagesXML == null) { throw new IllegalArgumentException( "plugin doesn't contain a messages.xml file"); } Document pluginDocument = parseDocument(zip.getInputStream(findbugsXML)); String pluginId = pluginDocument.valueOf(XPATH_PLUGIN_PLUGINID).trim(); String provider = pluginDocument.valueOf(XPATH_PLUGIN_PROVIDER).trim(); String website = pluginDocument.valueOf(XPATH_PLUGIN_WEBSITE).trim(); List<Document> msgDocuments = new ArrayList<Document>(3); for (String msgFile : getPotentialMessageFiles()) { ZipEntry msgEntry = zip.getEntry(msgFile); if (msgEntry == null) { continue; } Document msgDocument = parseDocument(zip.getInputStream(msgEntry)); msgDocuments.add(msgDocument); } String shortDesc = findMessageText(msgDocuments, XPATH_PLUGIN_SHORT_DESCRIPTION, ""); return new Summary(pluginId, shortDesc, provider, website); } catch (DocumentException e) { throw new IllegalArgumentException(e); } catch (IOException e) { throw new IllegalArgumentException(e); } finally { Util.closeSilently(zip); } } private static Document parseDocument(@WillClose InputStream in) throws DocumentException { Reader r = UTF8.bufferedReader(in); try { SAXReader reader = new SAXReader(); Document d = reader.read(r); return d; } finally { Util.closeSilently(r); } } }