Address some edge cases related to bootstrapping a fresh system.
[cfb.git] / prod / net / jaekl / cfb / CFB.java
1 package net.jaekl.cfb;
2
3 // Comparative FindBugs
4 // 
5 // Tool to compare successive runs of FindBugs, 
6 // flagging the change from one run to the next.
7 // 
8 // Copyright (C) 2015 Christian Jaekl
9
10 import java.io.File;
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;
20
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;
36
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;
43
44 public class CFB {
45         public static final String FINDBUGS_HOME = "FINDBUGS_HOME";     // name of the FINDBUGS_HOME environment variable
46         
47         DbDriver m_driver;
48         CfbSchema m_schema;
49         volatile static CfbBundle m_bundle = null;      
50         Locale m_locale;
51         
52         Config m_config;
53         
54         // Command-line parameters
55         File m_configFile;
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)
62         
63         CFB(Locale locale) {
64                 m_driver = new PostgresqlDriver();
65                 m_schema = new CfbSchema(m_driver);
66                 m_locale = locale;
67                 m_config = new Config();
68                 
69                 m_configFile = new File("config.properties");
70                 m_fbp    = null;
71                 m_fbDir  = null;
72                 m_projName = null;
73                 m_buildNum = null;
74                 m_removeSchema = false;
75                 m_output = null;
76         }
77         
78         static CfbBundle getBundle(Locale locale) {
79                 CfbBundle bundle = m_bundle;
80                 if (null == bundle) {
81                         synchronized(CFB.class) {
82                                 if (null == m_bundle) {
83                                         m_bundle = bundle = CfbBundle.getInst(locale);
84                                 }
85                         }
86                 }
87                 return bundle;
88         }
89         
90         Options createOptions() {
91                 Options opt = new Options();
92                 
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");
105                 
106                 return opt;
107         }
108         
109         boolean parseArgs(PrintWriter pw, String[] args) {
110                 Options opt = createOptions();
111                 
112                 try {
113                         CommandLine line = new GnuParser().parse(opt, args);
114                         
115                         if (line.hasOption("c")) {
116                                 m_configFile = new File(line.getOptionValue("c"));
117                         }
118                         if (line.hasOption("d")) {
119                                 m_config.setDbName(line.getOptionValue("d"));
120                         }
121                         if (line.hasOption("f")) {
122                                 m_fbp = new File(line.getOptionValue("f"));
123                         }
124                         if (line.hasOption("h")) {
125                                 m_config.setDbHost(line.getOptionValue("h"));
126                         }
127                         if (line.hasOption("help")) {
128                                 usage(pw, opt);
129                                 return false;
130                         }
131                         if (line.hasOption("j")) {
132                                 m_projName = line.getOptionValue("j");
133                         }
134                         if (line.hasOption("n")) {
135                                 m_buildNum = line.getOptionValue("n");
136                         }
137                         if (line.hasOption("o")) {
138                                 m_output = new File(line.getOptionValue("o"));
139                         }
140                         if (line.hasOption("p")) {
141                                 m_config.setDbPass(line.getOptionValue("p"));
142                         }
143                         m_removeSchema = line.hasOption("drop-tables");
144                         if (line.hasOption("t")) {
145                                 m_config.setDbPort(Integer.parseInt(line.getOptionValue("t")));
146                         }
147                         if (line.hasOption("u")) {
148                                 m_config.setDbUser(line.getOptionValue("u"));
149                         }
150                 } 
151                 catch (ParseException exc) {
152                         usage(pw, opt);
153                         return false;
154                 }
155                 
156                 // Check for required parameters
157                 if (m_removeSchema) {
158                         // No other parameters required
159                         return true;
160                 }
161                 if (null == m_fbp) {
162                         pw.println(trans(CfbBundle.MUST_SPECIFY_FBP_FILE));
163                         pw.println(trans(CfbBundle.INVOKE_WITH_HELP_FOR_HELP));
164                         return false;
165                 }
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);
170                         }
171                 }
172                 
173                 return true;
174         }
175         
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 
180         // createOptions().
181         void usage(PrintWriter pw, Options opt) {
182                 HelpFormatter help = new HelpFormatter();
183                 help.printHelp(pw, 80, getClass().getName(), "", opt, 0, 0, "", true);
184         }
185         
186         String trans(String key, Object... params) {
187                 return getBundle(m_locale).get(key, params);
188         }
189         
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);
193         }
194         
195         File getFindBugsDir() {
196                 return (null != m_fbDir) ? m_fbDir : new File(".");
197         }
198         
199         void initArgs() {
200                 String findBugsDir = Env.get("FINDBUGS_HOME");
201                 if (null != findBugsDir) {
202                         m_fbDir = new File(findBugsDir);
203                 }
204                 findBugsDir = getProperty("findbugs.home");
205                 if (null != findBugsDir) {
206                         m_fbDir = new File(findBugsDir);
207                 }
208         }
209         
210         void readConfig() throws IOException {
211                 if (null != m_configFile) {
212                         m_config.readFile(m_configFile);
213                 }
214         }
215         
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) ) {
219                         return;
220                 }
221                 readConfig();
222
223                 File findBugsDir = getFindBugsDir();
224                 File workDir = new File(".");
225                 MessageMap messageMap = new MessageMap();
226                 try {
227                         messageMap.load(findBugsDir, Locale.getDefault(Category.DISPLAY));
228                 }
229                 catch (FBMsgFileNotFoundException exc) {
230                         reportException(pw, exc);
231                         return;
232                 }
233                 
234                 if (!ensureDbInitialized(pw, messageMap)) {
235                         return;
236                 }
237                 
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));
242                         return;
243                 }
244                 
245                 storeAndReport(pw, messageMap, analysis);
246         }
247
248         boolean ensureDbInitialized(PrintWriter pw, MessageMap messageMap)
249                         throws TypeMismatchException 
250         {
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())
255                         ) 
256                 {
257                         m_schema.setMessageMap(messageMap);
258                         
259                         if (m_removeSchema) {
260                                 m_schema.purge(con);
261                                 return false;   // do not continue execution
262                         }
263                         m_schema.ensureDbInitialized(con);
264                         messageMap.loadIds(con, m_driver);
265                 }
266                 catch (SQLException exc) {
267                         reportUnableToConnect(pw, exc);
268                         return false;   // do not continue execution
269                 }
270                 
271                 return true;    // all OK; continue execution
272         }
273
274         void storeAndReport(PrintWriter pw, MessageMap messageMap, Analysis analysis) 
275                         throws TypeMismatchException, IOException 
276         {
277                 try (
278                                 Connection con = m_driver.connect(
279                                                 m_config.getDbHost(), m_config.getDbPort(), 
280                                                 m_config.getDbName(),
281                                                 m_config.getDbUser(), m_config.getDbPass()
282                                         )
283                         )
284                 {
285                         DbStore store = new DbStore(con, m_driver, messageMap.getColl());
286                         
287                         store.put(analysis);
288                         Analysis prior = store.getPrior(analysis);
289                         Delta delta = new Delta(prior, analysis);
290
291                         HtmlReport report = new HtmlReport(getBundle(m_locale), messageMap.getColl(), delta);
292                         if (null != m_output) {
293                                 report.write(m_output);
294                         }
295                         
296                         Notifier notifier = new Notifier(getBundle(m_locale), m_config);
297                         notifier.sendEmailIfNeeded(pw, report);
298                 }
299                 catch (StoreException exc) {
300                         exc.printStackTrace(pw);
301                 }
302                 catch (SQLException exc) {
303                         reportUnableToConnect(pw, exc);
304                 }
305         }
306         
307         void reportException(PrintWriter pw, FBMsgFileNotFoundException exc) {
308                 exc.printStackTrace(pw);
309                 
310                 pw.println(trans(CfbBundle.CANNOT_LOAD_FBMSG_FILE, exc.getFilename()));
311                 
312                 String fbHome = Env.get(FINDBUGS_HOME);
313                 if (null == fbHome) {
314                         pw.println(trans(CfbBundle.FINDBUGS_HOME_IS_NOT_SET, FINDBUGS_HOME));
315                 }
316                 else {
317                         pw.println(trans(CfbBundle.FINDBUGS_HOME_IS_SET_TO, FINDBUGS_HOME, fbHome));
318                 }
319
320         }
321
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(), 
328                                                                                         m_config.getDbUser()
329                                                                                 );
330                 exc.printStackTrace(pw);
331                 SQLException next = exc.getNextException();
332                 while (null != next) {
333                         next.printStackTrace(pw);
334                         next = next.getNextException();
335                 }
336                 pw.println(cannotConnect);
337         }
338         
339         public static void main(String[] args) {
340                 CFB cfb = new CFB(Locale.getDefault());
341                 
342                 try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset()))) {
343                         cfb.doMain(pw, args);
344                         pw.flush();
345                 } catch (SQLException | IOException | XmlParseException | SAXException | TypeMismatchException exc) {
346                         exc.printStackTrace();
347                 }
348         }
349
350 }