/*
* 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.File;
import java.io.UnsupportedEncodingException;
import java.net.URL;
import java.net.URLDecoder;
import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import javax.annotation.CheckForNull;
import javax.annotation.Nonnull;
import javax.swing.JOptionPane;
import edu.umd.cs.findbugs.cloud.CloudPlugin;
import edu.umd.cs.findbugs.updates.PluginUpdateListener;
import edu.umd.cs.findbugs.updates.UpdateCheckCallback;
import edu.umd.cs.findbugs.updates.UpdateChecker;
import edu.umd.cs.findbugs.util.ClassPathUtil;
import edu.umd.cs.findbugs.util.FutureValue;
/**
* The DetectorFactoryCollection stores all of the DetectorFactory objects used
* to create the Detectors which implement the various analyses. It is a
* singleton class.
*
* @author David Hovemeyer
* @see DetectorFactory
*/
public class DetectorFactoryCollection implements UpdateCheckCallback {
private static final Logger LOGGER = Logger.getLogger(DetectorFactoryCollection.class.getName());
private static final boolean DEBUG_JAWS = SystemProperties.getBoolean("findbugs.jaws.debug");
// private static final boolean DEBUG = Boolean.getBoolean("dfc.debug");
private static DetectorFactoryCollection theInstance;
private static final Object lock = new Object();
private final Map<String, Plugin> pluginByIdMap = new LinkedHashMap<String, Plugin>();
private Plugin corePlugin;
private final List<DetectorFactory> factoryList = new ArrayList<DetectorFactory>();
private final Map<String, DetectorFactory> factoriesByName = new HashMap<String, DetectorFactory>();
private final Map<String, DetectorFactory> factoriesByDetectorClassName = new HashMap<String, DetectorFactory>();
private final Map<String, CloudPlugin> registeredClouds = new LinkedHashMap<String, CloudPlugin>();
protected final Map<String, BugCategory> categoryDescriptionMap = new HashMap<String, BugCategory>();
protected final Map<String, BugPattern> bugPatternMap = new HashMap<String, BugPattern>();
protected final Map<String, BugCode> bugCodeMap = new HashMap<String, BugCode>();
private final UpdateChecker updateChecker;
private final CopyOnWriteArrayList<PluginUpdateListener> pluginUpdateListeners
= new CopyOnWriteArrayList<PluginUpdateListener>();
private volatile List<UpdateChecker.PluginUpdate> updates;
private boolean updatesForced;
private final Collection<Plugin> pluginsToUpdate;
final Map<String, String> globalOptions = new HashMap<String,String>();
final Map<String, Plugin> globalOptionsSetter = new HashMap<String,Plugin>();
protected DetectorFactoryCollection() {
this(true, false, Plugin.getAllPlugins(), new ArrayList<Plugin>());
}
protected DetectorFactoryCollection(Plugin onlyPlugin) {
this(false, true, Collections.singleton(onlyPlugin), new ArrayList<Plugin>());
}
protected DetectorFactoryCollection(Collection<Plugin> enabled) {
this(true, true, enabled, enabled);
}
private DetectorFactoryCollection(boolean loadCore, boolean forceLoad,
@Nonnull Collection<Plugin> pluginsToLoad,
@Nonnull Collection<Plugin> enabledPlugins) {
if(loadCore) {
loadCorePlugin();
}
for(Plugin plugin : pluginsToLoad) {
if (forceLoad || plugin.isGloballyEnabled() && !plugin.isCorePlugin()) {
loadPlugin(plugin);
if(!enabledPlugins.contains(plugin)) {
enabledPlugins.add(plugin);
}
}
}
setGlobalOptions();
updateChecker = new UpdateChecker(this);
pluginsToUpdate = combine(enabledPlugins);
// There are too many places where the detector factory collection can be created,
// and in many cases it has nothing to do with update checks, like Plugin#addCustomPlugin(URI).
// Line below commented to allow custom plugins be loaded/registered BEFORE we
// start to update something. The reason is that custom plugins
// can disable update globally OR just will be NOT INCLUDED in the update check!
// updateChecker.checkForUpdates(pluginsToUpdate, false);
}
/**
* @param force whether the updates should be shown to the user no matter what - even if the updates have been
* seen before
*/
public void checkForUpdates(boolean force) {
updateChecker.checkForUpdates(pluginsToUpdate, force);
}
private Collection<Plugin> combine(Collection<Plugin> enabled) {
List<Plugin> result = new ArrayList<Plugin>(enabled);
if (corePlugin != null && !result.contains(corePlugin)) {
result.add(corePlugin);
}
return result;
}
/**
* Reset the factory singleton.
* <p>
* <b>Implementation note:</b> This method is public for tests only!
*
* @param instance
* use null to clear the instance
*/
public static void resetInstance(@CheckForNull DetectorFactoryCollection instance) {
synchronized (lock) {
theInstance = instance;
}
}
/**
* Get the single instance of DetectorFactoryCollection, which knows each and every
* loaded plugin, independently of it's enablement
*/
public static DetectorFactoryCollection instance() {
synchronized (lock) {
if (theInstance == null) {
theInstance = new DetectorFactoryCollection();
}
return theInstance;
}
}
private void setGlobalOptions() {
globalOptions.clear();
globalOptionsSetter.clear();
for(Plugin p : plugins()) {
if (p.isGloballyEnabled()) {
for(Map.Entry<String, String> e : p.getMyGlobalOptions().entrySet()) {
String key = e.getKey();
String value = e.getValue();
String oldValue = globalOptions.get(key);
if (oldValue != null) {
if (oldValue.equals(value)) {
continue;
}
Plugin oldP = globalOptionsSetter.get(key);
throw new RuntimeException(
"Incompatible global options for " + key + "; conflict between " + oldP.getPluginId() + " and " + p.getPluginId());
}
globalOptions.put(key, value);
globalOptionsSetter.put(key, p);
}
}
}
}
public @CheckForNull String getGlobalOption(String key) {
return globalOptions.get(key);
}
public @CheckForNull Plugin getGlobalOptionSetter(String key) {
return globalOptionsSetter.get(key);
}
/**
* Return an Iterator over all available Plugin objects.
*/
public Iterator<Plugin> pluginIterator() {
return pluginByIdMap.values().iterator();
}
/**
* Return an Collection of all available Plugin objects.
*/
public Collection<Plugin> plugins() {
return pluginByIdMap.values();
}
@Nonnull
public Plugin getCorePlugin() {
if (corePlugin == null)
throw new IllegalStateException("No core plugin");
return corePlugin;
}
/**
* Get a Plugin by its unique id.
*
* @param pluginId
* the unique id
* @return the Plugin with that id, or null if no such Plugin is found
*/
public Plugin getPluginById(String pluginId) {
return pluginByIdMap.get(pluginId);
}
/**
* Return an Iterator over the DetectorFactory objects for all registered
* Detectors.
*/
public Iterator<DetectorFactory> factoryIterator() {
return factoryList.iterator();
}
/**
* Return an Iterable over the DetectorFactory objects for all registered
* Detectors.
*/
public Iterable<DetectorFactory> getFactories() {
return factoryList;
}
/**
* Look up a DetectorFactory by its short name.
*
* @param name
* the short name
* @return the DetectorFactory, or null if there is no factory with that
* short name
*/
public DetectorFactory getFactory(String name) {
return factoriesByName.get(name);
}
/**
* Look up a DetectorFactory by its class name.
*
* @param className
* the class name
* @return the DetectoryFactory, or null if there is no factory with that
* class name
*/
public DetectorFactory getFactoryByClassName(String className) {
return factoriesByDetectorClassName.get(className);
}
/**
* Register a DetectorFactory.
*/
void registerDetector(DetectorFactory factory) {
if (FindBugs.DEBUG)
System.out.println("Registering detector: " + factory.getFullName());
String detectorName = factory.getShortName();
if(!factoryList.contains(factory)) {
factoryList.add(factory);
} else {
LOGGER.log(Level.WARNING, "Trying to add already registered factory: " + factory +
", " + factory.getPlugin());
}
factoriesByName.put(detectorName, factory);
factoriesByDetectorClassName.put(factory.getFullName(), factory);
}
void unRegisterDetector(DetectorFactory factory) {
if (FindBugs.DEBUG)
System.out.println("Unregistering detector: " + factory.getFullName());
String detectorName = factory.getShortName();
factoryList.remove(factory);
factoriesByName.remove(detectorName);
factoriesByDetectorClassName.remove(factory.getFullName());
}
/**
* See if the location of ${findbugs.home} can be inferred from the location
* of findbugs.jar in the classpath.
*
* @return inferred ${findbugs.home}, or null if we can't figure it out
*/
private static String inferFindBugsHome() {
Pattern[] findbugsJarNames = { Pattern.compile("findbugs\\.jar$"), };
for (Pattern jarNamePattern : findbugsJarNames) {
String findbugsJarCodeBase = ClassPathUtil.findCodeBaseInClassPath(jarNamePattern,
SystemProperties.getProperty("java.class.path"));
if (findbugsJarCodeBase != null) {
File findbugsJar = new File(findbugsJarCodeBase);
File libDir = findbugsJar.getParentFile();
if (libDir.getName().equals("lib")) {
String fbHome = libDir.getParent();
FindBugs.setHome(fbHome);
return fbHome;
}
}
}
String classFilePath = FindBugs.class.getName().replaceAll("\\.", "/") + ".class";
URL resource = FindBugs.class.getClassLoader().getResource(classFilePath);
if (resource != null && resource.getProtocol().equals("file")) {
try {
String classfile = URLDecoder.decode(resource.getPath(), Charset.defaultCharset().name());
Matcher m = Pattern.compile("(.*)/.*?/edu/umd.*").matcher(classfile);
if (m.matches()) {
String home = m.group(1);
if (new File(home + "/etc/findbugs.xml").exists()) {
FindBugs.setHome(home);
return home;
}
}
} catch (UnsupportedEncodingException e) {
}
}
return null;
}
public static String getFindBugsHome() {
String homeDir = FindBugs.getHome();
if (homeDir == null) {
// Attempt to infer findbugs.home from the observed
// location of findbugs.jar.
homeDir = inferFindBugsHome();
}
return homeDir;
}
@CheckForNull
public static URL getCoreResource(String name) {
return PluginLoader.getCoreResource(name);
}
private void loadCorePlugin() {
Plugin plugin = PluginLoader.getCorePluginLoader().getPlugin();
loadPlugin(plugin);
corePlugin = plugin;
}
public static void jawsDebugMessage(String message) {
if (DEBUG_JAWS)
JOptionPane.showMessageDialog(null, message);
else if (FindBugs.DEBUG)
System.err.println(message);
}
void loadPlugin(Plugin plugin) {
if (FindBugs.DEBUG) {
System.out.println("Loading " + plugin.getPluginId());
}
pluginByIdMap.put(plugin.getPluginId(), plugin);
setGlobalOptions();
// Register all of the detectors that this plugin contains
for (DetectorFactory factory : plugin.getDetectorFactories()) {
registerDetector(factory);
}
for (BugCategory bugCategory : plugin.getBugCategories()) {
registerBugCategory(bugCategory);
}
// Register the BugPatterns
for (BugPattern bugPattern : plugin.getBugPatterns()) {
registerBugPattern(bugPattern);
}
// Register the BugCodes
for (BugCode bugCode : plugin.getBugCodes()) {
registerBugCode(bugCode);
}
for (CloudPlugin cloud : plugin.getCloudPlugins()) {
registerCloud(cloud);
}
}
void unLoadPlugin(Plugin plugin) {
pluginByIdMap.remove(plugin.getPluginId());
setGlobalOptions();
for (DetectorFactory factory : plugin.getDetectorFactories()) {
unRegisterDetector(factory);
}
for (BugCategory bugCategory : plugin.getBugCategories()) {
unRegisterBugCategory(bugCategory);
}
for (BugPattern bugPattern : plugin.getBugPatterns()) {
unRegisterBugPattern(bugPattern);
}
for (BugCode bugCode : plugin.getBugCodes()) {
unRegisterBugCode(bugCode);
}
for (CloudPlugin cloud : plugin.getCloudPlugins()) {
unRegisterCloud(cloud);
}
}
@SuppressWarnings({"ConstantConditions"})
public void pluginUpdateCheckComplete(List<UpdateChecker.PluginUpdate> newUpdates, boolean force) {
this.updates = newUpdates;
this.updatesForced = force;
for (PluginUpdateListener listener : pluginUpdateListeners) {
try {
listener.pluginUpdateCheckComplete(newUpdates, force);
} catch (Throwable e) {
LOGGER.log(Level.INFO, "Error during update check callback", e);
}
}
}
public void addPluginUpdateListener(PluginUpdateListener listener) {
pluginUpdateListeners.add(listener);
if (updates != null) {
listener.pluginUpdateCheckComplete(updates, updatesForced);
} else if(!updateChecker.updateChecksGloballyDisabled()){
checkForUpdates(false);
}
}
public FutureValue<Collection<UpdateChecker.PluginUpdate>> getUpdates() {
final FutureValue<Collection<UpdateChecker.PluginUpdate>> results = new FutureValue<Collection<UpdateChecker.PluginUpdate>>();
addPluginUpdateListener(new PluginUpdateListener() {
public void pluginUpdateCheckComplete(Collection<UpdateChecker.PluginUpdate> u, boolean force) {
results.set(u);
}
});
return results;
}
public Map<String, CloudPlugin> getRegisteredClouds() {
return Collections.unmodifiableMap(registeredClouds);
}
void registerCloud(CloudPlugin cloudPlugin) {
LOGGER.log(Level.FINE, "Registering " + cloudPlugin.getId());
registeredClouds.put(cloudPlugin.getId(), cloudPlugin);
}
void unRegisterCloud(CloudPlugin cloudPlugin) {
LOGGER.log(Level.FINE, "Unregistering " + cloudPlugin.getId());
registeredClouds.remove(cloudPlugin.getId());
}
/**
* Set the metadata for a bug category. If the category's metadata has
* already been set, this does nothing.
* @param bc
* the BugCategory object holding the metadata for the category
*
* @return false if the category's metadata has already been set, true
* otherwise
*/
public boolean registerBugCategory(BugCategory bc) {
String category = bc.getCategory();
if (categoryDescriptionMap.get(category) != null)
return false;
categoryDescriptionMap.put(category, bc);
return true;
}
protected boolean unRegisterBugCategory(BugCategory bc) {
String category = bc.getCategory();
categoryDescriptionMap.remove(category);
return true;
}
/**
* Register a BugPattern.
*
* @param bugPattern
* the BugPattern
*/
public void registerBugPattern(BugPattern bugPattern) {
bugPatternMap.put(bugPattern.getType(), bugPattern);
}
protected void unRegisterBugPattern(BugPattern bugPattern) {
bugPatternMap.remove(bugPattern.getType());
}
/**
* Get an Iterator over all registered bug patterns.
*/
public Iterator<BugPattern> bugPatternIterator() {
return bugPatternMap.values().iterator();
}
/**
* Get an Iterator over all registered bug patterns.
*/
public Collection<BugPattern> getBugPatterns() {
return bugPatternMap.values();
}
/**
* Look up bug pattern.
*
* @param bugType
* the bug type for the bug pattern
* @return the BugPattern, or null if it can't be found
*/
public @CheckForNull BugPattern lookupBugPattern(String bugType) {
return bugPatternMap.get(bugType);
}
public void registerBugCode(BugCode bugCode) {
bugCodeMap.put(bugCode.getAbbrev(), bugCode);
}
protected void unRegisterBugCode(BugCode bugCode) {
bugCodeMap.remove(bugCode.getAbbrev());
}
public Collection<BugCode> getBugCodes() {
return bugCodeMap.values();
}
/**
* Get a description for given "bug type". FIXME: this is referred to
* elsewhere as the "bug code" or "bug abbrev". Should make the terminology
* consistent everywhere. In this case, the bug type refers to the short
* prefix code prepended to the long and short bug messages.
*
* @param shortBugType
* the short bug type code
* @return the description of that short bug type code means
*/
public @Nonnull BugCode getBugCode(String shortBugType) {
BugCode bugCode = lookupBugCode(shortBugType);
if (bugCode == null)
throw new IllegalArgumentException("Error: missing bug code for key" + shortBugType);
return bugCode;
}
/**
* @param shortBugType the short bug type code
* @return the description of that short bug type code means
*/
public @CheckForNull BugCode lookupBugCode(String shortBugType) {
return bugCodeMap.get(shortBugType);
}
/**
* Get the BugCategory object for a category key. Returns null if no
* BugCategory object can be found.
*
* @param category
* the category key
* @return the BugCategory object (may be null)
*/
public BugCategory getBugCategory(String category) {
return categoryDescriptionMap.get(category);
}
/**
* Get a Collection containing all known bug category keys. E.g.,
* "CORRECTNESS", "MT_CORRECTNESS", "PERFORMANCE", etc.
*
* Excludes hidden bug categories
*
* @return Collection of bug category keys.
*/
public Collection<String> getBugCategories() {
ArrayList<String> result = new ArrayList<String>(categoryDescriptionMap.size());
for(BugCategory c : categoryDescriptionMap.values())
if (!c.isHidden())
result.add(c.getCategory());
return result;
}
public Collection<BugCategory> getBugCategoryObjects() {
return categoryDescriptionMap.values(); // backed by the Map
}
public UpdateChecker getUpdateChecker() {
return updateChecker;
}
}