c6c995d41cf70e66d9ecdb54329fca321b18cd3c
[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("h",  "host",        true,  "DB hostname");
98                 opt.addOption("j",  "project",     true,  "proJect name");
99                 opt.addOption("n",  "number",      true,  "Build number (version)");
100                 opt.addOption("o",  "outfile",     true,  "Output report filename");
101                 opt.addOption("p",  "pass",        true,  "DB password");
102                 opt.addOption("t",  "port",        true,  "DB port");
103                 opt.addOption("u",  "user",        true,  "DB username");
104                 
105                 return opt;
106         }
107         
108         boolean parseArgs(PrintWriter pw, String[] args) {
109                 Options opt = createOptions();
110                 
111                 try {
112                         CommandLine line = new GnuParser().parse(opt, args);
113                         if (line.hasOption("c")) {
114                                 m_configFile = new File(line.getOptionValue("c"));
115                         }
116                         if (line.hasOption("d")) {
117                                 m_config.setDbName(line.getOptionValue("d"));
118                         }
119                         if (line.hasOption("f")) {
120                                 m_fbp = new File(line.getOptionValue("f"));
121                         }
122                         if (line.hasOption("h")) {
123                                 m_config.setDbHost(line.getOptionValue("h"));
124                         }
125                         if (line.hasOption("j")) {
126                                 m_projName = line.getOptionValue("j");
127                         }
128                         if (line.hasOption("n")) {
129                                 m_buildNum = line.getOptionValue("n");
130                         }
131                         if (line.hasOption("o")) {
132                                 m_output = new File(line.getOptionValue("o"));
133                         }
134                         if (line.hasOption("p")) {
135                                 m_config.setDbPass(line.getOptionValue("p"));
136                         }
137                         m_removeSchema = line.hasOption("drop-tables");
138                         if (line.hasOption("t")) {
139                                 m_config.setDbPort(Integer.parseInt(line.getOptionValue("t")));
140                         }
141                         if (line.hasOption("u")) {
142                                 m_config.setDbUser(line.getOptionValue("u"));
143                         }
144                 } 
145                 catch (ParseException exc) {
146                         usage(pw, opt);
147                         return false;
148                 }
149                 
150                 return true;
151         }
152         
153         void usage(PrintWriter pw, Options opt) {
154                 HelpFormatter help = new HelpFormatter();
155                 help.printHelp(pw, 80, getClass().getName(), "", opt, 0, 0, "", true);
156         }
157         
158         String trans(String key, Object... params) {
159                 return getBundle(m_locale).get(key, params);
160         }
161         
162         String getenv(String varName) {
163                 // This is a separate function so that we can override it at unit test time
164                 return System.getenv(varName);
165         }
166         
167         String getProperty(String propName) {
168                 // This is a separate function so that we can override it at unit test time
169                 return System.getProperty(propName);
170         }
171         
172         File getFindBugsDir() {
173                 return (null != m_fbDir) ? m_fbDir : new File(".");
174         }
175         
176         void initArgs() {
177                 String findBugsDir = getenv("FINDBUGS_HOME");
178                 if (null != findBugsDir) {
179                         m_fbDir = new File(findBugsDir);
180                 }
181                 findBugsDir = getProperty("findbugs.home");
182                 if (null != findBugsDir) {
183                         m_fbDir = new File(findBugsDir);
184                 }
185         }
186         
187         void readConfig() throws IOException {
188                 if (null != m_configFile) {
189                         m_config.readFile(m_configFile);
190                 }
191         }
192         
193         void doMain(PrintWriter pw, String[] args) throws SQLException, IOException, XmlParseException, SAXException, TypeMismatchException {
194                 initArgs();     // read environment and system properties
195                 if ( ! parseArgs(pw, args) ) {
196                         return;
197                 }
198                 readConfig();
199
200                 File findBugsDir = getFindBugsDir();
201                 File workDir = new File(".");
202                 MessageMap messageMap = new MessageMap();
203                 try {
204                         messageMap.load(findBugsDir, Locale.getDefault(Category.DISPLAY));
205                 }
206                 catch (FBMsgFileNotFoundException exc) {
207                         reportException(pw, exc);
208                         return;
209                 }
210                 
211                 if (!ensureDbInitialized(pw, messageMap)) {
212                         return;
213                 }
214                 
215                 Analyzer analyzer = new Analyzer(messageMap);
216                 Analysis analysis = analyzer.analyze(pw, workDir, m_fbp, m_projName, m_buildNum);
217                 if (null == analysis) {
218                         pw.println(trans(CfbBundle.ANALYSIS_FAILED));
219                         return;
220                 }
221                 
222                 storeAndReport(pw, messageMap, analysis);
223         }
224
225         boolean ensureDbInitialized(PrintWriter pw, MessageMap messageMap)
226                         throws TypeMismatchException 
227         {
228                 try (Connection con = m_driver.connect(
229                                         m_config.getDbHost(), m_config.getDbPort(), 
230                                         m_config.getDbName(), 
231                                         m_config.getDbUser(), m_config.getDbPass())
232                         ) 
233                 {
234                         m_schema.setMessageMap(messageMap);
235                         
236                         if (m_removeSchema) {
237                                 m_schema.purge(con);
238                                 return false;   // do not continue execution
239                         }
240                         m_schema.ensureDbInitialized(con);
241                         messageMap.loadIds(con, m_driver);
242                 }
243                 catch (SQLException exc) {
244                         reportUnableToConnect(pw, exc);
245                         return false;   // do not continue execution
246                 }
247                 
248                 return true;    // all OK; continue execution
249         }
250
251         void storeAndReport(PrintWriter pw, MessageMap messageMap, Analysis analysis) 
252                         throws TypeMismatchException, IOException 
253         {
254                 try (
255                                 Connection con = m_driver.connect(
256                                                 m_config.getDbHost(), m_config.getDbPort(), 
257                                                 m_config.getDbName(),
258                                                 m_config.getDbUser(), m_config.getDbPass()
259                                         )
260                         )
261                 {
262                         DbStore store = new DbStore(con, m_driver, messageMap.getColl());
263                         
264                         store.put(analysis);
265                         Analysis prior = store.getPrior(analysis);
266                         Delta delta = new Delta(prior, analysis);
267
268                         HtmlReport report = new HtmlReport(getBundle(m_locale), messageMap.getColl(), delta);
269                         if (null != m_output) {
270                                 report.write(m_output);
271                         }
272                         
273                         Notifier notifier = new Notifier(getBundle(m_locale), m_config);
274                         notifier.sendEmailIfNeeded(pw, report);
275                 }
276                 catch (StoreException exc) {
277                         exc.printStackTrace(pw);
278                 }
279                 catch (SQLException exc) {
280                         reportUnableToConnect(pw, exc);
281                 }
282         }
283         
284         void reportException(PrintWriter pw, FBMsgFileNotFoundException exc) {
285                 exc.printStackTrace(pw);
286                 
287                 pw.println(trans(CfbBundle.CANNOT_LOAD_FBMSG_FILE, exc.getFilename()));
288                 
289                 String fbHome = Env.get(FINDBUGS_HOME);
290                 if (null == fbHome) {
291                         pw.println(trans(CfbBundle.FINDBUGS_HOME_IS_NOT_SET, FINDBUGS_HOME));
292                 }
293                 else {
294                         pw.println(trans(CfbBundle.FINDBUGS_HOME_IS_SET_TO, FINDBUGS_HOME, fbHome));
295                 }
296
297         }
298
299         private void reportUnableToConnect(PrintWriter pw, SQLException exc) {
300                 String cannotConnectFormat = trans(CfbBundle.CANNOT_CONNECT);
301                 String cannotConnect = MessageFormat.format(cannotConnectFormat, 
302                                                                                         m_config.getDbHost(), 
303                                                                                         ""+m_config.getDbPort(), 
304                                                                                         m_config.getDbName(), 
305                                                                                         m_config.getDbUser()
306                                                                                 );
307                 exc.printStackTrace(pw);
308                 SQLException next = exc.getNextException();
309                 while (null != next) {
310                         next.printStackTrace(pw);
311                         next = next.getNextException();
312                 }
313                 pw.println(cannotConnect);
314         }
315         
316         public static void main(String[] args) {
317                 CFB cfb = new CFB(Locale.getDefault());
318                 
319                 try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset()))) {
320                         cfb.doMain(pw, args);
321                         pw.flush();
322                 } catch (SQLException | IOException | XmlParseException | SAXException | TypeMismatchException exc) {
323                         exc.printStackTrace();
324                 }
325         }
326
327 }