001: /*
002: * FindBugs - Find bugs in Java programs
003: * Copyright (C) 2003-2005 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;
021:
022: import java.net.URL;
023: import java.net.URLClassLoader;
024: import java.util.ArrayList;
025: import java.util.HashSet;
026: import java.util.List;
027: import java.util.Locale;
028: import java.util.Set;
029:
030: import javax.annotation.CheckForNull;
031:
032: import org.dom4j.Document;
033: import org.dom4j.DocumentException;
034: import org.dom4j.Element;
035: import org.dom4j.Node;
036: import org.dom4j.io.SAXReader;
037:
038: import edu.umd.cs.findbugs.classfile.IAnalysisEngineRegistrar;
039: import edu.umd.cs.findbugs.plan.ByInterfaceDetectorFactorySelector;
040: import edu.umd.cs.findbugs.plan.DetectorFactorySelector;
041: import edu.umd.cs.findbugs.plan.DetectorOrderingConstraint;
042: import edu.umd.cs.findbugs.plan.ReportingDetectorFactorySelector;
043: import edu.umd.cs.findbugs.plan.SingleDetectorFactorySelector;
044:
045: /**
046: * Loader for a FindBugs plugin.
047: * A plugin is a jar file containing two metadata files,
048: * "findbugs.xml" and "messages.xml". Those files specify
049: * <ul>
050: * <li> the bug pattern Detector classes,
051: * <li> the bug patterns detected (including all text for displaying
052: * detected instances of those patterns), and
053: * <li> the "bug codes" which group together related bug instances
054: * </ul>
055: *
056: * <p> The PluginLoader creates a Plugin object to store
057: * the Detector factories and metadata.</p>
058: *
059: * @author David Hovemeyer
060: * @see Plugin
061: * @see PluginException
062: */
063: public class PluginLoader {
064:
065: private static final boolean DEBUG = SystemProperties
066: .getBoolean("findbugs.debug.PluginLoader");
067:
068: // ClassLoader used to load classes and resources
069: private URLClassLoader classLoader;
070:
071: // Keep a count of how many plugins we've seen without a
072: // "pluginid" attribute, so we can assign them all unique ids.
073: private static int nextUnknownId;
074:
075: // The loaded Plugin
076: private Plugin plugin;
077:
078: /**
079: * Constructor.
080: *
081: * @param url the URL of the plugin Jar file
082: * @throws PluginException if the plugin cannot be fully loaded
083: */
084: public PluginLoader(URL url) throws PluginException {
085: this .classLoader = new URLClassLoader(new URL[] { url });
086: init();
087: }
088:
089: /**
090: * Constructor.
091: *
092: * @param url the URL of the plugin Jar file
093: * @param parent the parent classloader
094: */
095: public PluginLoader(URL url, ClassLoader parent)
096: throws PluginException {
097: this .classLoader = new URLClassLoader(new URL[] { url }, parent);
098: init();
099: }
100:
101: /**
102: * @return Returns the classLoader.
103: */
104: public ClassLoader getClassLoader() {
105: return classLoader;
106: }
107:
108: /**
109: * Get the Plugin.
110: * @throws PluginException if the plugin cannot be fully loaded
111: */
112: public Plugin getPlugin() throws PluginException {
113: if (plugin == null)
114: init();
115: return plugin;
116: }
117:
118: /**
119: * Get a resource using the URLClassLoader classLoader. We try
120: * findResource first because (based on experiment) we can trust it
121: * to prefer resources in the jarfile to resources on the
122: * filesystem. Simply calling classLoader.getResource() allows the
123: * filesystem to override the jarfile, which can mess things up if,
124: * for example, there is a findbugs.xml or messages.xml in the
125: * current directory.
126: * @param name resource to get
127: * @return URL for the resource, or null if it could not be found
128: */
129: private URL getResource(String name) {
130: URL url = classLoader.findResource(name);
131: if (url == null) {
132: url = classLoader.getResource(name);
133: }
134: return url;
135: }
136:
137: private void init() throws PluginException {
138: // Plugin descriptor (a.k.a, "findbugs.xml"). Defines
139: // the bug detectors and bug patterns that the plugin provides.
140: Document pluginDescriptor;
141:
142: // Unique plugin id
143: String pluginId;
144:
145: // List of message translation files in decreasing order of precedence
146: ArrayList<Document> messageCollectionList = new ArrayList<Document>();
147:
148: // Read the plugin descriptor
149: String name = "findbugs.xml";
150: try {
151: URL descriptorURL = getResource(name);
152: if (descriptorURL == null)
153: throw new PluginException("Couldn't find \"" + name
154: + "\" in plugin");
155:
156: if (DEBUG) {
157: System.out.println("PluginLoader found " + name
158: + " at: " + descriptorURL);
159: }
160:
161: SAXReader reader = new SAXReader();
162: pluginDescriptor = reader.read(descriptorURL);
163: } catch (DocumentException e) {
164: throw new PluginException(
165: "Couldn't parse \"" + name + "\"", e);
166: }
167:
168: // Get the unique plugin id (or generate one, if none is present)
169: pluginId = pluginDescriptor
170: .valueOf("/FindbugsPlugin/@pluginid");
171: if (pluginId.equals("")) {
172: synchronized (PluginLoader.class) {
173: pluginId = "plugin" + nextUnknownId++;
174: }
175: }
176:
177: // See if the plugin is enabled or disabled by default.
178: // Note that if there is no "defaultenabled" attribute,
179: // then we assume that the plugin IS enabled by default.
180: String defaultEnabled = pluginDescriptor
181: .valueOf("/FindbugsPlugin/@defaultenabled");
182: boolean pluginEnabled = defaultEnabled.equals("")
183: || Boolean.valueOf(defaultEnabled).booleanValue();
184:
185: // Load the message collections
186: try {
187: //Locale locale = Locale.getDefault();
188: Locale locale = I18N.defaultLocale;
189: String language = locale.getLanguage();
190: String country = locale.getCountry();
191:
192: if (country != null)
193: addCollection(messageCollectionList, "messages_"
194: + language + "_" + country + ".xml");
195: addCollection(messageCollectionList, "messages_" + language
196: + ".xml");
197: addCollection(messageCollectionList, "messages.xml");
198: } catch (DocumentException e) {
199: e.printStackTrace();
200: throw new PluginException(
201: "Couldn't parse \"messages.xml\"", e);
202: }
203:
204: // Create the Plugin object (but don't assign to the plugin field yet,
205: // since we're still not sure if everything will load correctly)
206: Plugin plugin = new Plugin(pluginId, this );
207: plugin.setEnabled(pluginEnabled);
208:
209: // Set provider and website, if specified
210: String provider = pluginDescriptor
211: .valueOf("/FindbugsPlugin/@provider");
212: if (!provider.equals(""))
213: plugin.setProvider(provider);
214: String website = pluginDescriptor
215: .valueOf("/FindbugsPlugin/@website");
216: if (!website.equals(""))
217: plugin.setWebsite(website);
218:
219: // Set short description, if specified
220: Node pluginShortDesc = null;
221: try {
222: pluginShortDesc = findMessageNode(messageCollectionList,
223: "/MessageCollection/Plugin/ShortDescription",
224: "no plugin description");
225: } catch (PluginException e) {
226: // Missing description is not fatal, so ignore
227: }
228: if (pluginShortDesc != null) {
229: plugin.setShortDescription(pluginShortDesc.getText());
230: }
231:
232: // Create a DetectorFactory for all Detector nodes
233: try {
234: List<Node> detectorNodeList = pluginDescriptor
235: .selectNodes("/FindbugsPlugin/Detector");
236: int detectorCount = 0;
237: for (Node detectorNode : detectorNodeList) {
238: String className = detectorNode.valueOf("@class");
239: String speed = detectorNode.valueOf("@speed");
240: String disabled = detectorNode.valueOf("@disabled");
241: String reports = detectorNode.valueOf("@reports");
242: String requireJRE = detectorNode.valueOf("@requirejre");
243: String hidden = detectorNode.valueOf("@hidden");
244: if (speed == null || speed.length() == 0)
245: speed = "fast";
246:
247: //System.out.println("Found detector: class="+className+", disabled="+disabled);
248:
249: // Create DetectorFactory for the detector
250: Class<?> detectorClass = classLoader
251: .loadClass(className);
252: if (!Detector.class.isAssignableFrom(detectorClass)
253: && !Detector2.class
254: .isAssignableFrom(detectorClass))
255: throw new PluginException(
256: "Class "
257: + className
258: + " does not implement Detector or Detector2");
259: DetectorFactory factory = new DetectorFactory(plugin,
260: detectorClass, !disabled.equals("true"), speed,
261: reports, requireJRE);
262: if (Boolean.valueOf(hidden).booleanValue())
263: factory.setHidden(true);
264: factory
265: .setPositionSpecifiedInPluginDescriptor(detectorCount++);
266: plugin.addDetectorFactory(factory);
267:
268: // Find Detector node in one of the messages files,
269: // to get the detail HTML.
270: Node node = findMessageNode(messageCollectionList,
271: "/MessageCollection/Detector[@class='"
272: + className + "']/Details",
273: "Missing Detector description for detector "
274: + className);
275:
276: Element details = (Element) node;
277: String detailHTML = details.getText();
278: StringBuffer buf = new StringBuffer();
279: buf
280: .append("<!DOCTYPE HTML PUBLIC \"-//W3C//DTD HTML 4.01 Transitional//EN\">\n");
281: buf
282: .append("<HTML><HEAD><TITLE>Detector Description</TITLE></HEAD><BODY>\n");
283: buf.append(detailHTML);
284: buf.append("</BODY></HTML>\n");
285: factory.setDetailHTML(buf.toString());
286: }
287: } catch (ClassNotFoundException e) {
288: throw new PluginException(
289: "Could not instantiate detector class: " + e, e);
290: }
291:
292: // Create ordering constraints
293: Node orderingConstraintsNode = pluginDescriptor
294: .selectSingleNode("/FindbugsPlugin/OrderingConstraints");
295: if (orderingConstraintsNode != null) {
296: // Get inter-pass and intra-pass constraints
297: for (Element constraintElement : (List<Element>) orderingConstraintsNode
298: .selectNodes("./SplitPass|./WithinPass")) {
299: // Create the selectors which determine which detectors are
300: // involved in the constraint
301: DetectorFactorySelector earlierSelector = getConstraintSelector(
302: constraintElement, plugin, "Earlier");
303: DetectorFactorySelector laterSelector = getConstraintSelector(
304: constraintElement, plugin, "Later");
305:
306: // Create the constraint
307: DetectorOrderingConstraint constraint = new DetectorOrderingConstraint(
308: earlierSelector, laterSelector);
309:
310: // Add the constraint to the plugin
311: if (constraintElement.getName().equals("SplitPass"))
312: plugin.addInterPassOrderingConstraint(constraint);
313: else
314: plugin.addIntraPassOrderingConstraint(constraint);
315: }
316: }
317:
318: // register global Category descriptions
319: I18N i18n = I18N.instance();
320: for (Document messageCollection : messageCollectionList) {
321: List<Node> categoryNodeList = messageCollection
322: .selectNodes("/MessageCollection/BugCategory");
323: if (DEBUG)
324: System.out.println("found " + categoryNodeList.size()
325: + " categories in "
326: + messageCollection.getName());
327: for (Node categoryNode : categoryNodeList) {
328: String key = categoryNode.valueOf("@category");
329: if (key.equals(""))
330: throw new PluginException(
331: "BugCategory element with missing category attribute");
332: String shortDesc = getChildText(categoryNode,
333: "Description");
334: BugCategory bc = new BugCategory(key, shortDesc);
335: boolean b = i18n.registerBugCategory(key, bc);
336: if (DEBUG)
337: System.out.println(b ? "category " + key + " -> "
338: + shortDesc : "rejected \"" + shortDesc
339: + "\" for category " + key + ": "
340: + i18n.getBugCategoryDescription(key));
341: /* Now set the abbreviation and details. Be prepared for messages_fr.xml
342: * to specify only the shortDesc (though it should set the abbreviation
343: * too) and fall back to messages.xml for the abbreviation and details. */
344: if (!b)
345: bc = i18n.getBugCategory(key); // get existing BugCategory object
346: try {
347: String abbrev = getChildText(categoryNode,
348: "Abbreviation");
349: if (bc.getAbbrev() == null) {
350: bc.setAbbrev(abbrev);
351: if (DEBUG)
352: System.out.println("category " + key
353: + " abbrev -> " + abbrev);
354: } else if (DEBUG)
355: System.out.println("rejected abbrev '" + abbrev
356: + "' for category " + key + ": "
357: + bc.getAbbrev());
358: } catch (PluginException pe) {
359: if (DEBUG)
360: System.out
361: .println("missing Abbreviation for category "
362: + key + "/" + shortDesc);
363: // do nothing else -- Abbreviation is required, but handle its omission gracefully
364: }
365: try {
366: String details = getChildText(categoryNode,
367: "Details");
368: if (bc.getDetailText() == null) {
369: bc.setDetailText(details);
370: if (DEBUG)
371: System.out.println("category " + key
372: + " details -> " + details);
373: } else if (DEBUG)
374: System.out.println("rejected details ["
375: + details + "] for category " + key
376: + ": [" + bc.getDetailText() + ']');
377: } catch (PluginException pe) {
378: // do nothing -- LongDescription is optional
379: }
380: }
381: }
382:
383: // Create BugPatterns
384: List<Node> bugPatternNodeList = pluginDescriptor
385: .selectNodes("/FindbugsPlugin/BugPattern");
386: for (Node bugPatternNode : bugPatternNodeList) {
387: String type = bugPatternNode.valueOf("@type");
388: String abbrev = bugPatternNode.valueOf("@abbrev");
389: String category = bugPatternNode.valueOf("@category");
390: String experimental = bugPatternNode
391: .valueOf("@experimental");
392:
393: // Find the matching element in messages.xml (or translations)
394: String query = "/MessageCollection/BugPattern[@type='"
395: + type + "']";
396: Node messageNode = findMessageNode(messageCollectionList,
397: query,
398: "messages.xml missing BugPattern element for type "
399: + type);
400:
401: String shortDesc = getChildText(messageNode,
402: "ShortDescription");
403: String longDesc = getChildText(messageNode,
404: "LongDescription");
405: String detailText = getChildText(messageNode, "Details");
406: int cweid = 0;
407: try {
408: cweid = Integer.parseInt(messageNode.valueOf("@cweid"));
409: } catch (RuntimeException e) {
410: assert true; // ignore
411: }
412:
413: BugPattern bugPattern = new BugPattern(type, abbrev,
414: category, Boolean.valueOf(experimental)
415: .booleanValue(), shortDesc, longDesc,
416: detailText, cweid);
417: plugin.addBugPattern(bugPattern);
418: boolean unknownCategory = (null == i18n
419: .getBugCategory(category));
420: if (unknownCategory) {
421: i18n.registerBugCategory(category, new BugCategory(
422: category, category));
423: // no desc, but at least now it will appear in I18N.getBugCategories().
424: if (DEBUG)
425: System.out.println("Category " + category
426: + " (of BugPattern " + type
427: + ") has no description in messages*.xml");
428: //TODO report this even if !DEBUG
429: }
430: }
431:
432: // Create BugCodes
433: Set<String> definedBugCodes = new HashSet<String>();
434: for (Document messageCollection : messageCollectionList) {
435: List<Node> bugCodeNodeList = messageCollection
436: .selectNodes("/MessageCollection/BugCode");
437: for (Node bugCodeNode : bugCodeNodeList) {
438: String abbrev = bugCodeNode.valueOf("@abbrev");
439: if (abbrev.equals(""))
440: throw new PluginException(
441: "BugCode element with missing abbrev attribute");
442: if (definedBugCodes.contains(abbrev))
443: continue;
444: String description = bugCodeNode.getText();
445:
446: String query = "/FindbugsPlugin/BugCode[@abbrev='"
447: + abbrev + "']";
448: Node fbNode = pluginDescriptor.selectSingleNode(query);
449: int cweid = 0;
450: if (fbNode != null)
451: try {
452: cweid = Integer.parseInt(fbNode
453: .valueOf("@cweid"));
454: } catch (RuntimeException e) {
455: assert true; // ignore
456: }
457: BugCode bugCode = new BugCode(abbrev, description,
458: cweid);
459: plugin.addBugCode(bugCode);
460: definedBugCodes.add(abbrev);
461: }
462:
463: }
464:
465: // If an engine registrar is specified, make a note of its classname
466: Node node = pluginDescriptor
467: .selectSingleNode("/FindbugsPlugin/EngineRegistrar");
468: if (node != null) {
469: String engineClassName = node.valueOf("@class");
470: if (engineClassName == null) {
471: throw new PluginException(
472: "EngineRegistrar element with missing class attribute");
473: }
474:
475: try {
476: Class<?> engineRegistrarClass = classLoader
477: .loadClass(engineClassName);
478: if (!IAnalysisEngineRegistrar.class
479: .isAssignableFrom(engineRegistrarClass)) {
480: throw new PluginException(
481: engineRegistrarClass
482: + " does not implement IAnalysisEngineRegistrar");
483: }
484:
485: plugin
486: .setEngineRegistrarClass((Class<? extends IAnalysisEngineRegistrar>) engineRegistrarClass);
487: } catch (ClassNotFoundException e) {
488: throw new PluginException(
489: "Could not instantiate analysis engine registrar class: "
490: + e, e);
491: }
492:
493: }
494:
495: // Success!
496: // Assign to the plugin field, so getPlugin() can return the
497: // new Plugin object.
498: this .plugin = plugin;
499:
500: }
501:
502: private static DetectorFactorySelector getConstraintSelector(
503: Element constraintElement, Plugin plugin,
504: String singleDetectorElementName/*,
505: String detectorCategoryElementName*/) throws PluginException {
506: Node node = constraintElement.selectSingleNode("./"
507: + singleDetectorElementName);
508: if (node != null) {
509: String detectorClass = node.valueOf("@class");
510: return new SingleDetectorFactorySelector(plugin,
511: detectorClass);
512: }
513:
514: node = constraintElement.selectSingleNode("./"
515: + singleDetectorElementName + "Category");
516: if (node != null) {
517: boolean spanPlugins = Boolean.valueOf(
518: node.valueOf("@spanplugins")).booleanValue();
519:
520: String categoryName = node.valueOf("@name");
521: if (!categoryName.equals("")) {
522: if (categoryName.equals("reporting")) {
523: return new ReportingDetectorFactorySelector(
524: spanPlugins ? null : plugin);
525: } else if (categoryName.equals("training")) {
526: return new ByInterfaceDetectorFactorySelector(
527: spanPlugins ? null : plugin,
528: TrainingDetector.class);
529: } else if (categoryName.equals("interprocedural")) {
530: return new ByInterfaceDetectorFactorySelector(
531: spanPlugins ? null : plugin,
532: InterproceduralFirstPassDetector.class);
533: } else {
534: throw new PluginException("Invalid category name "
535: + categoryName
536: + " in constraint selector node");
537: }
538: }
539: }
540:
541: node = constraintElement.selectSingleNode("./"
542: + singleDetectorElementName + "Subtypes");
543: if (node != null) {
544: boolean spanPlugins = Boolean.valueOf(
545: node.valueOf("@spanplugins")).booleanValue();
546:
547: String super Name = node.valueOf("@super");
548: if (!super Name.equals("")) {
549: try {
550: Class<?> super Class = Class.forName(super Name);
551: return new ByInterfaceDetectorFactorySelector(
552: spanPlugins ? null : plugin, super Class);
553: } catch (ClassNotFoundException e) {
554: throw new PluginException("Unknown class "
555: + super Name
556: + " in constraint selector node");
557: }
558: }
559: }
560: throw new PluginException("Invalid constraint selector node");
561: }
562:
563: private String lookupDetectorClass(Plugin plugin, String name)
564: throws PluginException {
565: // If the detector name contains '.' characters, assume it is
566: // fully qualified already. Otherwise, assume it is a short
567: // name that resolves to another detector in the same plugin.
568:
569: if (name.indexOf('.') < 0) {
570: DetectorFactory factory = plugin
571: .getFactoryByShortName(name);
572: if (factory == null)
573: throw new PluginException(
574: "No detector found for short name '" + name
575: + "'");
576: name = factory.getFullName();
577: }
578: return name;
579: }
580:
581: private void addCollection(List<Document> messageCollectionList,
582: String filename) throws DocumentException {
583: URL messageURL = getResource(filename);
584: if (messageURL != null) {
585: SAXReader reader = new SAXReader();
586: Document messageCollection = reader.read(messageURL);
587: messageCollectionList.add(messageCollection);
588: }
589: }
590:
591: private static Node findMessageNode(
592: List<Document> messageCollectionList, String xpath,
593: String missingMsg) throws PluginException {
594:
595: for (Document document : messageCollectionList) {
596: Node node = document.selectSingleNode(xpath);
597: if (node != null)
598: return node;
599: }
600: throw new PluginException(missingMsg);
601: }
602:
603: private static @CheckForNull
604: Node findOptionalMessageNode(List<Document> messageCollectionList,
605: String xpath) throws PluginException {
606: for (Document document : messageCollectionList) {
607: Node node = document.selectSingleNode(xpath);
608: if (node != null)
609: return node;
610: }
611: return null;
612: }
613:
614: private static String getChildText(Node node, String childName)
615: throws PluginException {
616: Node child = node.selectSingleNode(childName);
617: if (child == null)
618: throw new PluginException("Could not find child \""
619: + childName + "\" for node");
620: return child.getText();
621: }
622:
623: }
624:
625: // vim:ts=4
|