3 // Comparative FindBugs
5 // Tool to compare successive runs of FindBugs,
6 // flagging the change from one run to the next.
8 // Copyright (C) 2015 Christian Jaekl
11 import java.io.IOException;
12 import java.io.OutputStreamWriter;
13 import java.io.PrintWriter;
14 import java.nio.charset.Charset;
15 import java.sql.Connection;
16 import java.sql.SQLException;
17 import java.text.MessageFormat;
18 import java.util.Locale;
19 import java.util.Locale.Category;
21 import net.jaekl.cfb.analyze.Analysis;
22 import net.jaekl.cfb.analyze.Analyzer;
23 import net.jaekl.cfb.analyze.Delta;
24 import net.jaekl.cfb.analyze.FBMsgFileNotFoundException;
25 import net.jaekl.cfb.analyze.HtmlReport;
26 import net.jaekl.cfb.analyze.MessageMap;
27 import net.jaekl.cfb.analyze.Notifier;
28 import net.jaekl.cfb.db.CfbSchema;
29 import net.jaekl.cfb.db.TypeMismatchException;
30 import net.jaekl.cfb.db.driver.DbDriver;
31 import net.jaekl.cfb.db.driver.PostgresqlDriver;
32 import net.jaekl.cfb.store.DbStore;
33 import net.jaekl.cfb.store.StoreException;
34 import net.jaekl.cfb.util.Env;
35 import net.jaekl.qd.xml.XmlParseException;
37 import org.apache.commons.cli.CommandLine;
38 import org.apache.commons.cli.GnuParser;
39 import org.apache.commons.cli.HelpFormatter;
40 import org.apache.commons.cli.Options;
41 import org.apache.commons.cli.ParseException;
42 import org.xml.sax.SAXException;
45 public static final String FINDBUGS_HOME = "FINDBUGS_HOME"; // name of the FINDBUGS_HOME environment variable
49 volatile static CfbBundle m_bundle = null;
54 // Command-line parameters
56 File m_fbp; // FindBugsProject file
57 File m_fbDir; // Directory where FindBugs is installed
58 String m_projName; // project (module) name
59 String m_buildNum; // build number (version)
60 boolean m_removeSchema; // purge DB schema
61 File m_output; // File to which we should write our output (report)
64 m_driver = new PostgresqlDriver();
65 m_schema = new CfbSchema(m_driver);
67 m_config = new Config();
69 m_configFile = new File("config.properties");
74 m_removeSchema = false;
78 static CfbBundle getBundle(Locale locale) {
79 CfbBundle bundle = m_bundle;
81 synchronized(CFB.class) {
82 if (null == m_bundle) {
83 m_bundle = bundle = CfbBundle.getInst(locale);
90 Options createOptions() {
91 Options opt = new Options();
93 opt.addOption("c", "config", true, "Properties configuration file");
94 opt.addOption("d", "dbname", true, "DB name");
95 opt.addOption(null, "drop-tables", false, "Remove database schema (drop all data)");
96 opt.addOption("f", "fbp", true, "FindBugsProject file");
97 opt.addOption(null, "help", false, "Display help message and exit");
98 opt.addOption("h", "host", true, "DB hostname");
99 opt.addOption("j", "project", true, "proJect name");
100 opt.addOption("n", "number", true, "Build number (version)");
101 opt.addOption("o", "outfile", true, "Output report filename");
102 opt.addOption("p", "pass", true, "DB password");
103 opt.addOption("t", "port", true, "DB port");
104 opt.addOption("u", "user", true, "DB username");
109 boolean parseArgs(PrintWriter pw, String[] args) {
110 Options opt = createOptions();
113 CommandLine line = new GnuParser().parse(opt, args);
115 if (line.hasOption("c")) {
116 m_configFile = new File(line.getOptionValue("c"));
118 if (line.hasOption("d")) {
119 m_config.setDbName(line.getOptionValue("d"));
121 if (line.hasOption("f")) {
122 m_fbp = new File(line.getOptionValue("f"));
124 if (line.hasOption("h")) {
125 m_config.setDbHost(line.getOptionValue("h"));
127 if (line.hasOption("help")) {
131 if (line.hasOption("j")) {
132 m_projName = line.getOptionValue("j");
134 if (line.hasOption("n")) {
135 m_buildNum = line.getOptionValue("n");
137 if (line.hasOption("o")) {
138 m_output = new File(line.getOptionValue("o"));
140 if (line.hasOption("p")) {
141 m_config.setDbPass(line.getOptionValue("p"));
143 m_removeSchema = line.hasOption("drop-tables");
144 if (line.hasOption("t")) {
145 m_config.setDbPort(Integer.parseInt(line.getOptionValue("t")));
147 if (line.hasOption("u")) {
148 m_config.setDbUser(line.getOptionValue("u"));
151 catch (ParseException exc) {
156 // Check for required parameters
157 if (m_removeSchema) {
158 // No other parameters required
162 pw.println(trans(CfbBundle.MUST_SPECIFY_FBP_FILE));
163 pw.println(trans(CfbBundle.INVOKE_WITH_HELP_FOR_HELP));
166 if (null == m_projName) {
167 m_projName = m_fbp.getName();
168 if (m_projName.endsWith(".fbp")) {
169 m_projName = m_projName.substring(0, m_projName.length() - 4);
176 // Note that this leverages commons-cli's HelpFormatter to
177 // generate the usage message. It will always be in English.
178 // If we want to localize that, we'd need to recode this,
179 // and also translate the parameter descriptions in
181 void usage(PrintWriter pw, Options opt) {
182 HelpFormatter help = new HelpFormatter();
183 help.printHelp(pw, 80, getClass().getName(), "", opt, 0, 0, "", true);
186 String trans(String key, Object... params) {
187 return getBundle(m_locale).get(key, params);
190 String getProperty(String propName) {
191 // This is a separate function so that we can override it at unit test time
192 return System.getProperty(propName);
195 File getFindBugsDir() {
196 return (null != m_fbDir) ? m_fbDir : new File(".");
200 String findBugsDir = Env.get("FINDBUGS_HOME");
201 if (null != findBugsDir) {
202 m_fbDir = new File(findBugsDir);
204 findBugsDir = getProperty("findbugs.home");
205 if (null != findBugsDir) {
206 m_fbDir = new File(findBugsDir);
210 void readConfig() throws IOException {
211 if (null != m_configFile) {
212 m_config.readFile(m_configFile);
216 void doMain(PrintWriter pw, String[] args) throws SQLException, IOException, XmlParseException, SAXException, TypeMismatchException {
217 initArgs(); // read environment and system properties
218 if ( ! parseArgs(pw, args) ) {
223 File findBugsDir = getFindBugsDir();
224 File workDir = new File(".");
225 MessageMap messageMap = new MessageMap();
227 messageMap.load(findBugsDir, Locale.getDefault(Category.DISPLAY));
229 catch (FBMsgFileNotFoundException exc) {
230 reportException(pw, exc);
234 if (!ensureDbInitialized(pw, messageMap)) {
238 Analyzer analyzer = new Analyzer(messageMap);
239 Analysis analysis = analyzer.analyze(pw, workDir, m_fbp, m_projName, m_buildNum);
240 if (null == analysis) {
241 pw.println(trans(CfbBundle.ANALYSIS_FAILED));
245 storeAndReport(pw, messageMap, analysis);
248 boolean ensureDbInitialized(PrintWriter pw, MessageMap messageMap)
249 throws TypeMismatchException
251 try (Connection con = m_driver.connect(
252 m_config.getDbHost(), m_config.getDbPort(),
253 m_config.getDbName(),
254 m_config.getDbUser(), m_config.getDbPass())
257 m_schema.setMessageMap(messageMap);
259 if (m_removeSchema) {
261 return false; // do not continue execution
263 m_schema.ensureDbInitialized(con);
264 messageMap.loadIds(con, m_driver);
266 catch (SQLException exc) {
267 reportUnableToConnect(pw, exc);
268 return false; // do not continue execution
271 return true; // all OK; continue execution
274 void storeAndReport(PrintWriter pw, MessageMap messageMap, Analysis analysis)
275 throws TypeMismatchException, IOException
278 Connection con = m_driver.connect(
279 m_config.getDbHost(), m_config.getDbPort(),
280 m_config.getDbName(),
281 m_config.getDbUser(), m_config.getDbPass()
285 DbStore store = new DbStore(con, m_driver, messageMap.getColl());
288 Analysis prior = store.getPrior(analysis);
289 Delta delta = new Delta(prior, analysis);
291 HtmlReport report = new HtmlReport(getBundle(m_locale), messageMap.getColl(), delta);
292 if (null != m_output) {
293 report.write(m_output);
296 Notifier notifier = new Notifier(getBundle(m_locale), m_config);
297 notifier.sendEmailIfNeeded(pw, report);
299 catch (StoreException exc) {
300 exc.printStackTrace(pw);
302 catch (SQLException exc) {
303 reportUnableToConnect(pw, exc);
307 void reportException(PrintWriter pw, FBMsgFileNotFoundException exc) {
308 exc.printStackTrace(pw);
310 pw.println(trans(CfbBundle.CANNOT_LOAD_FBMSG_FILE, exc.getFilename()));
312 String fbHome = Env.get(FINDBUGS_HOME);
313 if (null == fbHome) {
314 pw.println(trans(CfbBundle.FINDBUGS_HOME_IS_NOT_SET, FINDBUGS_HOME));
317 pw.println(trans(CfbBundle.FINDBUGS_HOME_IS_SET_TO, FINDBUGS_HOME, fbHome));
322 private void reportUnableToConnect(PrintWriter pw, SQLException exc) {
323 String cannotConnectFormat = trans(CfbBundle.CANNOT_CONNECT);
324 String cannotConnect = MessageFormat.format(cannotConnectFormat,
325 m_config.getDbHost(),
326 ""+m_config.getDbPort(),
327 m_config.getDbName(),
330 exc.printStackTrace(pw);
331 SQLException next = exc.getNextException();
332 while (null != next) {
333 next.printStackTrace(pw);
334 next = next.getNextException();
336 pw.println(cannotConnect);
339 public static void main(String[] args) {
340 CFB cfb = new CFB(Locale.getDefault());
342 try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset()))) {
343 cfb.doMain(pw, args);
345 } catch (SQLException | IOException | XmlParseException | SAXException | TypeMismatchException exc) {
346 exc.printStackTrace();