Restructure use of static field m_bundle to improve the odds that multiple instances...
[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.HtmlReport;
25 import net.jaekl.cfb.analyze.MessageMap;
26 import net.jaekl.cfb.analyze.Notifier;
27 import net.jaekl.cfb.db.CfbSchema;
28 import net.jaekl.cfb.db.TypeMismatchException;
29 import net.jaekl.cfb.db.driver.DbDriver;
30 import net.jaekl.cfb.db.driver.PostgresqlDriver;
31 import net.jaekl.cfb.store.DbStore;
32 import net.jaekl.cfb.store.StoreException;
33 import net.jaekl.qd.xml.XmlParseException;
34
35 import org.apache.commons.cli.CommandLine;
36 import org.apache.commons.cli.GnuParser;
37 import org.apache.commons.cli.HelpFormatter;
38 import org.apache.commons.cli.Options;
39 import org.apache.commons.cli.ParseException;
40 import org.xml.sax.SAXException;
41
42 public class CFB {
43         DbDriver m_driver;
44         CfbSchema m_schema;
45         volatile static CfbBundle m_bundle = null;      
46         Locale m_locale;
47         
48         Config m_config;
49         
50         // Command-line parameters
51         File m_configFile;
52         File m_fbp;             // FindBugsProject file
53         File m_fbDir;   // Directory where FindBugs is installed
54         String m_projName; // project (module) name
55         String m_buildNum; // build number (version)
56         boolean m_removeSchema; // purge DB schema
57         File m_output;  // File to which we should write our output (report)
58         
59         CFB(Locale locale) {
60                 m_driver = new PostgresqlDriver();
61                 m_schema = new CfbSchema(m_driver);
62                 m_locale = locale;
63                 m_config = new Config();
64                 
65                 m_configFile = new File("config.properties");
66                 m_fbp    = null;
67                 m_fbDir  = null;
68                 m_projName = null;
69                 m_buildNum = null;
70                 m_removeSchema = false;
71                 m_output = null;
72         }
73         
74         static CfbBundle getBundle(Locale locale) {
75                 CfbBundle bundle = m_bundle;
76                 if (null == bundle) {
77                         synchronized(CFB.class) {
78                                 if (null == m_bundle) {
79                                         m_bundle = bundle = CfbBundle.getInst(locale);
80                                 }
81                         }
82                 }
83                 return bundle;
84         }
85         
86         Options createOptions() {
87                 Options opt = new Options();
88                 
89                 opt.addOption("c",  "config",      true,  "Properties configuration file");
90                 opt.addOption("d",  "dbname",      true,  "DB name");
91                 opt.addOption(null, "drop-tables", false, "Remove database schema (drop all data)");
92                 opt.addOption("f",  "fbp",         true,  "FindBugsProject file");
93                 opt.addOption("h",  "host",        true,  "DB hostname");
94                 opt.addOption("j",  "project",     true,  "proJect name");
95                 opt.addOption("n",  "number",      true,  "Build number (version)");
96                 opt.addOption("o",  "outfile",     true,  "Output report filename");
97                 opt.addOption("p",  "pass",        true,  "DB password");
98                 opt.addOption("t",  "port",        true,  "DB port");
99                 opt.addOption("u",  "user",        true,  "DB username");
100                 
101                 return opt;
102         }
103         
104         boolean parseArgs(PrintWriter pw, String[] args) {
105                 Options opt = createOptions();
106                 
107                 try {
108                         CommandLine line = new GnuParser().parse(opt, args);
109                         if (line.hasOption("c")) {
110                                 m_configFile = new File(line.getOptionValue("c"));
111                         }
112                         if (line.hasOption("d")) {
113                                 m_config.setDbName(line.getOptionValue("d"));
114                         }
115                         if (line.hasOption("f")) {
116                                 m_fbp = new File(line.getOptionValue("f"));
117                         }
118                         if (line.hasOption("h")) {
119                                 m_config.setDbHost(line.getOptionValue("h"));
120                         }
121                         if (line.hasOption("j")) {
122                                 m_projName = line.getOptionValue("j");
123                         }
124                         if (line.hasOption("n")) {
125                                 m_buildNum = line.getOptionValue("n");
126                         }
127                         if (line.hasOption("o")) {
128                                 m_output = new File(line.getOptionValue("o"));
129                         }
130                         if (line.hasOption("p")) {
131                                 m_config.setDbPass(line.getOptionValue("p"));
132                         }
133                         m_removeSchema = line.hasOption("drop-tables");
134                         if (line.hasOption("t")) {
135                                 m_config.setDbPort(Integer.parseInt(line.getOptionValue("t")));
136                         }
137                         if (line.hasOption("u")) {
138                                 m_config.setDbUser(line.getOptionValue("u"));
139                         }
140                 } 
141                 catch (ParseException exc) {
142                         usage(pw, opt);
143                         return false;
144                 }
145                 
146                 return true;
147         }
148         
149         void usage(PrintWriter pw, Options opt) {
150                 HelpFormatter help = new HelpFormatter();
151                 help.printHelp(pw, 80, getClass().getName(), "", opt, 0, 0, "", true);
152         }
153         
154         String trans(String key) {
155                 return getBundle(m_locale).get(key);
156         }
157         
158         String getenv(String varName) {
159                 // This is a separate function so that we can override it at unit test time
160                 return System.getenv(varName);
161         }
162         
163         String getProperty(String propName) {
164                 // This is a separate function so that we can override it at unit test time
165                 return System.getProperty(propName);
166         }
167         
168         File getFindBugsDir() {
169                 return (null != m_fbDir) ? m_fbDir : new File(".");
170         }
171         
172         void initArgs() {
173                 String findBugsDir = getenv("FINDBUGS_HOME");
174                 if (null != findBugsDir) {
175                         m_fbDir = new File(findBugsDir);
176                 }
177                 findBugsDir = getProperty("findbugs.home");
178                 if (null != findBugsDir) {
179                         m_fbDir = new File(findBugsDir);
180                 }
181         }
182         
183         void readConfig() throws IOException {
184                 if (null != m_configFile) {
185                         m_config.readFile(m_configFile);
186                 }
187         }
188         
189         void doMain(PrintWriter pw, String[] args) throws SQLException, IOException, XmlParseException, SAXException, TypeMismatchException {
190                 initArgs();     // read environment and system properties
191                 if ( ! parseArgs(pw, args) ) {
192                         return;
193                 }
194                 readConfig();
195
196                 File findBugsDir = getFindBugsDir();
197                 File workDir = new File(".");
198                 MessageMap messageMap = new MessageMap();
199                 messageMap.load(findBugsDir, Locale.getDefault(Category.DISPLAY));
200                 
201                 if (!ensureDbInitialized(pw, messageMap)) {
202                         return;
203                 }
204                 
205                 Analyzer analyzer = new Analyzer(messageMap);
206                 Analysis analysis = analyzer.analyze(pw, workDir, m_fbp, m_projName, m_buildNum);
207                 if (null == analysis) {
208                         pw.println(trans(CfbBundle.ANALYSIS_FAILED));
209                         return;
210                 }
211                 
212                 storeAndReport(pw, messageMap, analysis);
213         }
214
215         boolean ensureDbInitialized(PrintWriter pw, MessageMap messageMap)
216                         throws TypeMismatchException 
217         {
218                 try (Connection con = m_driver.connect(
219                                         m_config.getDbHost(), m_config.getDbPort(), 
220                                         m_config.getDbName(), 
221                                         m_config.getDbUser(), m_config.getDbPass())
222                         ) 
223                 {
224                         m_schema.setMessageMap(messageMap);
225                         
226                         if (m_removeSchema) {
227                                 m_schema.purge(con);
228                                 return false;   // do not continue execution
229                         }
230                         m_schema.ensureDbInitialized(con);
231                         messageMap.loadIds(con, m_driver);
232                 }
233                 catch (SQLException exc) {
234                         reportUnableToConnect(pw, exc);
235                         return false;   // do not continue execution
236                 }
237                 
238                 return true;    // all OK; continue execution
239         }
240
241         void storeAndReport(PrintWriter pw, MessageMap messageMap, Analysis analysis) 
242                         throws TypeMismatchException, IOException 
243         {
244                 try (
245                                 Connection con = m_driver.connect(
246                                                 m_config.getDbHost(), m_config.getDbPort(), 
247                                                 m_config.getDbName(),
248                                                 m_config.getDbUser(), m_config.getDbPass()
249                                         )
250                         )
251                 {
252                         DbStore store = new DbStore(con, m_driver, messageMap.getColl());
253                         
254                         store.put(analysis);
255                         Analysis prior = store.getPrior(analysis);
256                         Delta delta = new Delta(prior, analysis);
257
258                         HtmlReport report = new HtmlReport(getBundle(m_locale), messageMap.getColl(), delta);
259                         if (null != m_output) {
260                                 report.write(m_output);
261                         }
262                         
263                         Notifier notifier = new Notifier(getBundle(m_locale), m_config);
264                         notifier.sendEmailIfNeeded(pw, report);
265                 }
266                 catch (StoreException exc) {
267                         exc.printStackTrace(pw);
268                 }
269                 catch (SQLException exc) {
270                         reportUnableToConnect(pw, exc);
271                 }
272         }
273
274         private void reportUnableToConnect(PrintWriter pw, SQLException exc) {
275                 String cannotConnectFormat = trans(CfbBundle.CANNOT_CONNECT);
276                 String cannotConnect = MessageFormat.format(cannotConnectFormat, 
277                                                                                         m_config.getDbHost(), 
278                                                                                         ""+m_config.getDbPort(), 
279                                                                                         m_config.getDbName(), 
280                                                                                         m_config.getDbUser()
281                                                                                 );
282                 exc.printStackTrace(pw);
283                 SQLException next = exc.getNextException();
284                 while (null != next) {
285                         next.printStackTrace(pw);
286                         next = next.getNextException();
287                 }
288                 pw.println(cannotConnect);
289         }
290         
291         public static void main(String[] args) {
292                 CFB cfb = new CFB(Locale.getDefault());
293                 
294                 try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset()))) {
295                         cfb.doMain(pw, args);
296                         pw.flush();
297                 } catch (SQLException | IOException | XmlParseException | SAXException | TypeMismatchException exc) {
298                         exc.printStackTrace();
299                 }
300         }
301
302 }