/*
* FindBugs - Find bugs in Java programs
* Copyright (C) 2004,2005 Dave Brosius <dbrosius@qis.net>
* Copyright (C) 2004,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
*/
/*
* UserPreferences.java
*
* Created on May 26, 2004, 11:55 PM
*/
package edu.umd.cs.findbugs.config;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.Map.Entry;
import java.util.Properties;
import java.util.Set;
import java.util.TreeSet;
import javax.annotation.WillClose;
import edu.umd.cs.findbugs.DetectorFactory;
import edu.umd.cs.findbugs.DetectorFactoryCollection;
import edu.umd.cs.findbugs.FindBugs;
import edu.umd.cs.findbugs.IFindBugsEngine;
import edu.umd.cs.findbugs.Plugin;
import edu.umd.cs.findbugs.SystemProperties;
/**
* User Preferences outside of any one Project. This consists of a class to
* manage the findbugs.prop file found in the user.home.
*
* @author Dave Brosius
*/
public class UserPreferences implements Cloneable {
// Public constants
/**
* Separator string for values composed from a string and boolean
*/
private static final String BOOL_SEPARATOR = "|";
public static final String EFFORT_MIN = "min";
public static final String EFFORT_DEFAULT = "default";
public static final String EFFORT_MAX = "max";
// Private constants
private static final String PREF_FILE_NAME = ".Findbugs_prefs";
private static final int MAX_RECENT_FILES = 9;
private static final String DETECTOR_THRESHOLD_KEY = "detector_threshold";
private static final String FILTER_SETTINGS_KEY = "filter_settings";
private static final String FILTER_SETTINGS2_KEY = "filter_settings_neg";
private static final String RUN_AT_FULL_BUILD = "run_at_full_build";
private static final String EFFORT_KEY = "effort";
private static final String KEY_INCLUDE_FILTER = "includefilter";
private static final String KEY_EXCLUDE_FILTER = "excludefilter";
private static final String KEY_EXCLUDE_BUGS = "excludebugs";
private static final String KEY_PLUGIN = "plugin";
// Fields
private LinkedList<String> recentProjectsList;
private Map<String, Boolean> detectorEnablementMap;
private ProjectFilterSettings filterSettings;
private boolean runAtFullBuild;
private String effort;
private Map<String, Boolean> includeFilterFiles;
private Map<String, Boolean> excludeFilterFiles;
private Map<String, Boolean> excludeBugsFiles;
private Map<String, Boolean> customPlugins;
private UserPreferences() {
filterSettings = ProjectFilterSettings.createDefault();
recentProjectsList = new LinkedList<String>();
detectorEnablementMap = new HashMap<String, Boolean>();
runAtFullBuild = true;
effort = EFFORT_DEFAULT;
includeFilterFiles = new TreeMap<String, Boolean>();
excludeFilterFiles = new TreeMap<String, Boolean>();
excludeBugsFiles = new TreeMap<String, Boolean>();
customPlugins = new TreeMap<String, Boolean>();
}
/**
* Create default UserPreferences.
*
* @return default UserPreferences
*/
public static UserPreferences createDefaultUserPreferences() {
return new UserPreferences();
}
/**
* Read persistent global UserPreferences from file in the user's home
* directory.
*/
public void read() {
File prefFile = new File(SystemProperties.getProperty("user.home"), PREF_FILE_NAME);
if (!prefFile.exists() || !prefFile.isFile()) {
return;
}
try {
read(new FileInputStream(prefFile));
} catch (IOException e) {
// Ignore - just use default preferences
}
}
/**
* Read user preferences from given input stream. The InputStream is
* guaranteed to be closed by this method.
*
* @param in
* the InputStream
* @throws IOException
*/
public void read(@WillClose InputStream in) throws IOException {
BufferedInputStream prefStream = null;
Properties props = new Properties();
try {
prefStream = new BufferedInputStream(in);
props.load(prefStream);
} finally {
try {
if (prefStream != null) {
prefStream.close();
}
} catch (IOException ioe) {
// Ignore
}
}
if (props.size() == 0) {
return;
}
for (int i = 0; i < MAX_RECENT_FILES; i++) {
String key = "recent" + i;
String projectName = (String) props.get(key);
if (projectName != null) {
recentProjectsList.add(projectName);
}
}
for (Map.Entry<?, ?> e : props.entrySet()) {
String key = (String) e.getKey();
if (!key.startsWith("detector") || key.startsWith("detector_")) {
// it is not a detector enablement property
continue;
}
String detectorState = (String) e.getValue();
int pipePos = detectorState.indexOf(BOOL_SEPARATOR);
if (pipePos >= 0) {
String name = detectorState.substring(0, pipePos);
String enabled = detectorState.substring(pipePos + 1);
detectorEnablementMap.put(name, Boolean.valueOf(enabled));
}
}
if (props.get(FILTER_SETTINGS_KEY) != null) {
// Properties contain encoded project filter settings.
filterSettings = ProjectFilterSettings.fromEncodedString(props.getProperty(FILTER_SETTINGS_KEY));
} else {
// Properties contain only minimum warning priority threshold
// (probably).
// We will honor this threshold, and enable all bug categories.
String threshold = (String) props.get(DETECTOR_THRESHOLD_KEY);
if (threshold != null) {
try {
int detectorThreshold = Integer.parseInt(threshold);
setUserDetectorThreshold(detectorThreshold);
} catch (NumberFormatException nfe) {
// Ok to ignore
}
}
}
if (props.get(FILTER_SETTINGS2_KEY) != null) {
// populate the hidden bug categories in the project filter settings
ProjectFilterSettings.hiddenFromEncodedString(filterSettings, props.getProperty(FILTER_SETTINGS2_KEY));
}
if (props.get(RUN_AT_FULL_BUILD) != null) {
runAtFullBuild = Boolean.parseBoolean(props.getProperty(RUN_AT_FULL_BUILD));
}
effort = props.getProperty(EFFORT_KEY, EFFORT_DEFAULT);
includeFilterFiles = readProperties(props, KEY_INCLUDE_FILTER);
excludeFilterFiles = readProperties(props, KEY_EXCLUDE_FILTER);
excludeBugsFiles = readProperties(props, KEY_EXCLUDE_BUGS);
customPlugins = readProperties(props, KEY_PLUGIN);
}
/**
* Write persistent global UserPreferences to file in user's home directory.
*/
public void write() {
try {
File prefFile = new File(SystemProperties.getProperty("user.home"), PREF_FILE_NAME);
write(new FileOutputStream(prefFile));
} catch (IOException e) {
if (FindBugs.DEBUG) {
e.printStackTrace(); // Ignore
}
}
}
/**
* Write UserPreferences to given OutputStream. The OutputStream is
* guaranteed to be closed by this method.
*
* @param out
* the OutputStream
* @throws IOException
*/
public void write(@WillClose OutputStream out) throws IOException {
Properties props = new SortedProperties();
for (int i = 0; i < recentProjectsList.size(); i++) {
String projectName = recentProjectsList.get(i);
String key = "recent" + i;
props.put(key, projectName);
}
Iterator<Entry<String, Boolean>> it = detectorEnablementMap.entrySet().iterator();
while (it.hasNext()) {
Entry<String, Boolean> entry = it.next();
props.put("detector" + entry.getKey(), entry.getKey() + BOOL_SEPARATOR + String.valueOf(entry.getValue().booleanValue()));
}
// Save ProjectFilterSettings
props.put(FILTER_SETTINGS_KEY, filterSettings.toEncodedString());
props.put(FILTER_SETTINGS2_KEY, filterSettings.hiddenToEncodedString());
// Backwards-compatibility: save minimum warning priority as integer.
// This will allow the properties file to work with older versions
// of FindBugs.
props.put(DETECTOR_THRESHOLD_KEY, String.valueOf(filterSettings.getMinPriorityAsInt()));
props.put(RUN_AT_FULL_BUILD, String.valueOf(runAtFullBuild));
props.setProperty(EFFORT_KEY, effort);
writeProperties(props, KEY_INCLUDE_FILTER, includeFilterFiles);
writeProperties(props, KEY_EXCLUDE_FILTER, excludeFilterFiles);
writeProperties(props, KEY_EXCLUDE_BUGS, excludeBugsFiles);
writeProperties(props, KEY_PLUGIN, customPlugins);
OutputStream prefStream = null;
try {
prefStream = new BufferedOutputStream(out);
props.store(prefStream, "FindBugs User Preferences");
prefStream.flush();
} finally {
try {
if (prefStream != null) {
prefStream.close();
}
} catch (IOException ioe) {
}
}
}
/**
* Get List of recent project filenames.
*
* @return List of recent project filenames
*/
public List<String> getRecentProjects() {
return recentProjectsList;
}
/**
* Add given project filename to the front of the recently-used project
* list.
*
* @param projectName
* project filename
*/
public void useProject(String projectName) {
removeProject(projectName);
recentProjectsList.addFirst(projectName);
while (recentProjectsList.size() > MAX_RECENT_FILES) {
recentProjectsList.removeLast();
}
}
/**
* Remove project filename from the recently-used project list.
*
* @param projectName
* project filename
*/
public void removeProject(String projectName) {
// It should only be in list once (usually in slot 0) but check entire
// list...
Iterator<String> it = recentProjectsList.iterator();
while (it.hasNext()) {
// LinkedList, so remove() via iterator is faster than
// remove(index).
if (projectName.equals(it.next())) {
it.remove();
}
}
}
/**
* Set the enabled/disabled status of given Detector.
*
* @param factory
* the DetectorFactory for the Detector to be enabled/disabled
* @param enable
* true if the Detector should be enabled, false if it should be
* Disabled
*/
public void enableDetector(DetectorFactory factory, boolean enable) {
detectorEnablementMap.put(factory.getShortName(), enable);
}
/**
* Get the enabled/disabled status of given Detector.
*
* @param factory
* the DetectorFactory of the Detector
* @return true if the Detector is enabled, false if not
*/
public boolean isDetectorEnabled(DetectorFactory factory) {
String detectorName = factory.getShortName();
Boolean enabled = detectorEnablementMap.get(detectorName);
if (enabled == null) {
// No explicit preference has been specified for this detector,
// so use the default enablement specified by the
// DetectorFactory.
enabled = factory.isDefaultEnabled();
detectorEnablementMap.put(detectorName, enabled);
}
return enabled;
}
/**
* Enable or disable all known Detectors.
*
* @param enable
* true if all detectors should be enabled, false if they should
* all be disabled
*/
public void enableAllDetectors(boolean enable) {
detectorEnablementMap.clear();
Collection<Plugin> allPlugins = Plugin.getAllPlugins();
for (Plugin plugin : allPlugins) {
for (DetectorFactory factory : plugin.getDetectorFactories()) {
detectorEnablementMap.put(factory.getShortName(), enable);
}
}
}
/**
* Set the ProjectFilterSettings.
*
* @param filterSettings
* the ProjectFilterSettings
*/
public void setProjectFilterSettings(ProjectFilterSettings filterSettings) {
this.filterSettings = filterSettings;
}
/**
* Get ProjectFilterSettings.
*
* @return the ProjectFilterSettings
*/
public ProjectFilterSettings getFilterSettings() {
return this.filterSettings;
}
/**
* Get the detector threshold (min severity to report a warning).
*
* @return the detector threshold
*/
public int getUserDetectorThreshold() {
return filterSettings.getMinPriorityAsInt();
}
/**
* Set the detector threshold (min severity to report a warning).
*
* @param threshold
* the detector threshold
*/
public void setUserDetectorThreshold(int threshold) {
String minPriority = ProjectFilterSettings.getIntPriorityAsString(threshold);
filterSettings.setMinPriority(minPriority);
}
/**
* Set the enabled/disabled status of running findbugs automatically for
* full builds.
*
* @param enable
* true if running FindBugs at full builds should be enabled,
* false if it should be Disabled
*/
public void setRunAtFullBuild(boolean enable) {
this.runAtFullBuild = enable;
}
/**
* Get the enabled/disabled status of runAtFullBuild
*
* @return true if the running for full builds is enabled, false if not
*/
public boolean isRunAtFullBuild() {
return runAtFullBuild;
}
/**
* Set the detector threshold (min severity to report a warning).
*
* @param threshold
* the detector threshold
*/
public void setUserDetectorThreshold(String threshold) {
filterSettings.setMinPriority(threshold);
}
@Override
public boolean equals(Object obj) {
if (obj == null || obj.getClass() != this.getClass()) {
return false;
}
UserPreferences other = (UserPreferences) obj;
return runAtFullBuild == other.runAtFullBuild && recentProjectsList.equals(other.recentProjectsList)
&& detectorEnablementMap.equals(other.detectorEnablementMap) && filterSettings.equals(other.filterSettings)
&& effort.equals(other.effort) && includeFilterFiles.equals(other.includeFilterFiles)
&& excludeFilterFiles.equals(other.excludeFilterFiles) && excludeBugsFiles.equals(other.excludeBugsFiles)
&& customPlugins.equals(other.customPlugins);
}
@Override
public int hashCode() {
return recentProjectsList.hashCode() + detectorEnablementMap.hashCode() + filterSettings.hashCode() + effort.hashCode()
+ includeFilterFiles.hashCode() + excludeFilterFiles.hashCode() + (runAtFullBuild ? 1 : 0);
}
@Override
public Object clone() {
try {
UserPreferences dup = (UserPreferences) super.clone();
dup.recentProjectsList = new LinkedList<String>(recentProjectsList);
dup.detectorEnablementMap = new HashMap<String, Boolean>(detectorEnablementMap);
dup.filterSettings = (ProjectFilterSettings) this.filterSettings.clone();
dup.runAtFullBuild = runAtFullBuild;
dup.includeFilterFiles = new TreeMap<String, Boolean>(includeFilterFiles);
dup.excludeFilterFiles = new TreeMap<String, Boolean>(excludeFilterFiles);
dup.excludeBugsFiles = new TreeMap<String, Boolean>(excludeBugsFiles);
dup.customPlugins = new TreeMap<String, Boolean>(customPlugins);
dup.effort = effort;
return dup;
} catch (CloneNotSupportedException e) {
throw new AssertionError(e);
}
}
public String getEffort() {
return effort;
}
public void setEffort(String effort) {
if (!EFFORT_MIN.equals(effort) && !EFFORT_DEFAULT.equals(effort) && !EFFORT_MAX.equals(effort)) {
throw new IllegalArgumentException("Effort \"" + effort + "\" is not a valid effort value.");
}
this.effort = effort;
}
public Map<String, Boolean> getIncludeFilterFiles() {
return includeFilterFiles;
}
public void setIncludeFilterFiles(Map<String, Boolean> includeFilterFiles) {
if (includeFilterFiles == null) {
throw new IllegalArgumentException("includeFilterFiles may not be null.");
}
this.includeFilterFiles = includeFilterFiles;
}
public Map<String, Boolean> getExcludeBugsFiles() {
return excludeBugsFiles;
}
public void setExcludeBugsFiles(Map<String, Boolean> excludeBugsFiles) {
if (excludeBugsFiles == null) {
throw new IllegalArgumentException("excludeBugsFiles may not be null.");
}
this.excludeBugsFiles = excludeBugsFiles;
}
public void setExcludeFilterFiles(Map<String, Boolean> excludeFilterFiles) {
if (excludeFilterFiles == null) {
throw new IllegalArgumentException("excludeFilterFiles may not be null.");
}
this.excludeFilterFiles = excludeFilterFiles;
}
public Map<String, Boolean> getExcludeFilterFiles() {
return excludeFilterFiles;
}
/**
* Additional plugins which could be used by {@link IFindBugsEngine} (if
* enabled), or which shouldn't be used (if disabled). If a plugin is not
* included in the set, it's enablement depends on it's default settings.
*
* @param customPlugins
* map with additional third party plugin locations (as absolute
* paths), never null, but might be empty
* @see Plugin#isCorePlugin()
* @see Plugin#isGloballyEnabled()
*/
public void setCustomPlugins(Map<String, Boolean> customPlugins) {
if (customPlugins == null) {
throw new IllegalArgumentException("customPlugins may not be null.");
}
this.customPlugins = customPlugins;
}
/**
* Additional plugins which could be used by {@link IFindBugsEngine} (if
* enabled), or which shouldn't be used (if disabled). If a plugin is not
* included in the set, it's enablement depends on it's default settings.
*
* @return map with additional third party plugin locations (as absolute
* paths), never null, but might be empty. A value of a particular
* key can be null (same as disabled)
* @see Plugin#isCorePlugin()
* @see Plugin#isGloballyEnabled()
*/
public Map<String, Boolean> getCustomPlugins() {
return customPlugins;
}
/**
* Additional plugins which could be used or shouldn't be used (depending on
* given argument) by {@link IFindBugsEngine}. If a plugin is not included
* in the set, it's enablement depends on it's default settings.
*
* @return set with additional third party plugin locations (as absolute
* paths), never null, but might be empty
* @see Plugin#isCorePlugin()
* @see Plugin#isGloballyEnabled()
*/
public Set<String> getCustomPlugins(boolean enabled){
Set<Entry<String, Boolean>> entrySet = customPlugins.entrySet();
Set<String> result = new TreeSet<String>();
for (Entry<String, Boolean> entry : entrySet) {
if(enabled) {
if(entry.getValue() != null && entry.getValue().booleanValue()) {
result.add(entry.getKey());
}
} else {
if(entry.getValue() == null || !entry.getValue().booleanValue()) {
result.add(entry.getKey());
}
}
}
return result;
}
/**
* Helper method to read array of strings out of the properties file, using
* a Findbugs style format.
*
* @param props
* The properties file to read the array from.
* @param keyPrefix
* The key prefix of the array.
* @return The array of Strings, or an empty array if no values exist.
*/
private static Map<String, Boolean> readProperties(Properties props, String keyPrefix) {
Map<String, Boolean> filters = new TreeMap<String, Boolean>();
int counter = 0;
boolean keyFound = true;
while (keyFound) {
String property = props.getProperty(keyPrefix + counter);
if (property != null) {
int pipePos = property.indexOf(BOOL_SEPARATOR);
if (pipePos >= 0) {
String name = property.substring(0, pipePos);
String enabled = property.substring(pipePos + 1);
filters.put(name, Boolean.valueOf(enabled));
} else {
filters.put(property, Boolean.TRUE);
}
counter++;
} else {
keyFound = false;
}
}
return filters;
}
/**
* Helper method to write array of strings out of the properties file, using
* a Findbugs style format.
*
* @param props
* The properties file to write the array to.
* @param keyPrefix
* The key prefix of the array.
* @param filters
* The filters array to write to the properties.
*/
private static void writeProperties(Properties props, String keyPrefix, Map<String, Boolean> filters) {
int counter = 0;
Set<Entry<String, Boolean>> entrySet = filters.entrySet();
for (Entry<String, Boolean> entry : entrySet) {
props.setProperty(keyPrefix + counter, entry.getKey() + BOOL_SEPARATOR + entry.getValue());
counter++;
}
// remove obsolete keys from the properties file
boolean keyFound = true;
while (keyFound) {
String key = keyPrefix + counter;
String property = props.getProperty(key);
if (property == null) {
keyFound = false;
} else {
props.remove(key);
}
}
}
/**
* Returns the effort level as an array of feature settings as expected by
* FindBugs.
*
* @return The array of feature settings corresponding to the current effort
* setting.
*/
public AnalysisFeatureSetting[] getAnalysisFeatureSettings() {
if (effort.equals(EFFORT_DEFAULT)) {
return FindBugs.DEFAULT_EFFORT;
} else if (effort.equals(EFFORT_MIN)) {
return FindBugs.MIN_EFFORT;
}
return FindBugs.MAX_EFFORT;
}
}