Address some edge cases related to bootstrapping a fresh system.
[cfb.git] / prod / net / jaekl / cfb / CFB.java
index 94ef08f8a996cecf7ce681574b95bc3fd1ad62f9..eeb2a47afd7feca1e30a9dc7eb951e48578ef769 100644 (file)
@@ -9,7 +9,9 @@ package net.jaekl.cfb;
 
 import java.io.File;
 import java.io.IOException;
+import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
+import java.nio.charset.Charset;
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.text.MessageFormat;
@@ -19,13 +21,17 @@ import java.util.Locale.Category;
 import net.jaekl.cfb.analyze.Analysis;
 import net.jaekl.cfb.analyze.Analyzer;
 import net.jaekl.cfb.analyze.Delta;
+import net.jaekl.cfb.analyze.FBMsgFileNotFoundException;
 import net.jaekl.cfb.analyze.HtmlReport;
 import net.jaekl.cfb.analyze.MessageMap;
+import net.jaekl.cfb.analyze.Notifier;
 import net.jaekl.cfb.db.CfbSchema;
 import net.jaekl.cfb.db.TypeMismatchException;
 import net.jaekl.cfb.db.driver.DbDriver;
 import net.jaekl.cfb.db.driver.PostgresqlDriver;
 import net.jaekl.cfb.store.DbStore;
+import net.jaekl.cfb.store.StoreException;
+import net.jaekl.cfb.util.Env;
 import net.jaekl.qd.xml.XmlParseException;
 
 import org.apache.commons.cli.CommandLine;
@@ -36,19 +42,20 @@ import org.apache.commons.cli.ParseException;
 import org.xml.sax.SAXException;
 
 public class CFB {
+       public static final String FINDBUGS_HOME = "FINDBUGS_HOME";     // name of the FINDBUGS_HOME environment variable
+       
        DbDriver m_driver;
        CfbSchema m_schema;
-       CfbBundle m_bundle;     
+       volatile static CfbBundle m_bundle = null;      
        Locale m_locale;
        
+       Config m_config;
+       
        // Command-line parameters
-       String m_dbName; // db name
+       File m_configFile;
        File m_fbp;             // FindBugsProject file
        File m_fbDir;   // Directory where FindBugs is installed
-       String m_host;  // db host
-       int m_port;             // db port
-       String m_user;  // db user
-       String m_pass;  // db password
+       String m_projName; // project (module) name
        String m_buildNum; // build number (version)
        boolean m_removeSchema; // purge DB schema
        File m_output;  // File to which we should write our output (report)
@@ -57,27 +64,39 @@ public class CFB {
                m_driver = new PostgresqlDriver();
                m_schema = new CfbSchema(m_driver);
                m_locale = locale;
-               m_bundle = CfbBundle.getInst(m_locale);
+               m_config = new Config();
                
-               m_dbName = "CFB";
+               m_configFile = new File("config.properties");
                m_fbp    = null;
                m_fbDir  = null;
-               m_host = "localhost";
-               m_port = 5432;
-               m_pass = "";
-               m_user = "user";
+               m_projName = null;
                m_buildNum = null;
                m_removeSchema = false;
                m_output = null;
        }
        
+       static CfbBundle getBundle(Locale locale) {
+               CfbBundle bundle = m_bundle;
+               if (null == bundle) {
+                       synchronized(CFB.class) {
+                               if (null == m_bundle) {
+                                       m_bundle = bundle = CfbBundle.getInst(locale);
+                               }
+                       }
+               }
+               return bundle;
+       }
+       
        Options createOptions() {
                Options opt = new Options();
                
+               opt.addOption("c",  "config",      true,  "Properties configuration file");
                opt.addOption("d",  "dbname",      true,  "DB name");
                opt.addOption(null, "drop-tables", false, "Remove database schema (drop all data)");
                opt.addOption("f",  "fbp",         true,  "FindBugsProject file");
+               opt.addOption(null, "help",        false, "Display help message and exit");
                opt.addOption("h",  "host",        true,  "DB hostname");
+               opt.addOption("j",  "project",     true,  "proJect name");
                opt.addOption("n",  "number",      true,  "Build number (version)");
                opt.addOption("o",  "outfile",     true,  "Output report filename");
                opt.addOption("p",  "pass",        true,  "DB password");
@@ -92,14 +111,25 @@ public class CFB {
                
                try {
                        CommandLine line = new GnuParser().parse(opt, args);
+                       
+                       if (line.hasOption("c")) {
+                               m_configFile = new File(line.getOptionValue("c"));
+                       }
                        if (line.hasOption("d")) {
-                               m_dbName = line.getOptionValue("d");
+                               m_config.setDbName(line.getOptionValue("d"));
                        }
                        if (line.hasOption("f")) {
                                m_fbp = new File(line.getOptionValue("f"));
                        }
                        if (line.hasOption("h")) {
-                               m_host = line.getOptionValue("h");
+                               m_config.setDbHost(line.getOptionValue("h"));
+                       }
+                       if (line.hasOption("help")) {
+                               usage(pw, opt);
+                               return false;
+                       }
+                       if (line.hasOption("j")) {
+                               m_projName = line.getOptionValue("j");
                        }
                        if (line.hasOption("n")) {
                                m_buildNum = line.getOptionValue("n");
@@ -108,14 +138,14 @@ public class CFB {
                                m_output = new File(line.getOptionValue("o"));
                        }
                        if (line.hasOption("p")) {
-                               m_pass = line.getOptionValue("p");
+                               m_config.setDbPass(line.getOptionValue("p"));
                        }
                        m_removeSchema = line.hasOption("drop-tables");
                        if (line.hasOption("t")) {
-                               m_port = Integer.parseInt(line.getOptionValue("t"));
+                               m_config.setDbPort(Integer.parseInt(line.getOptionValue("t")));
                        }
                        if (line.hasOption("u")) {
-                               m_user = line.getOptionValue("u");
+                               m_config.setDbUser(line.getOptionValue("u"));
                        }
                } 
                catch (ParseException exc) {
@@ -123,21 +153,38 @@ public class CFB {
                        return false;
                }
                
+               // Check for required parameters
+               if (m_removeSchema) {
+                       // No other parameters required
+                       return true;
+               }
+               if (null == m_fbp) {
+                       pw.println(trans(CfbBundle.MUST_SPECIFY_FBP_FILE));
+                       pw.println(trans(CfbBundle.INVOKE_WITH_HELP_FOR_HELP));
+                       return false;
+               }
+               if (null == m_projName) {
+                       m_projName = m_fbp.getName();
+                       if (m_projName.endsWith(".fbp")) {
+                               m_projName = m_projName.substring(0, m_projName.length() - 4);
+                       }
+               }
+               
                return true;
        }
        
+       // Note that this leverages commons-cli's HelpFormatter to 
+       // generate the usage message.  It will always be in English.
+       // If we want to localize that, we'd need to recode this,
+       // and also translate the parameter descriptions in 
+       // createOptions().
        void usage(PrintWriter pw, Options opt) {
                HelpFormatter help = new HelpFormatter();
                help.printHelp(pw, 80, getClass().getName(), "", opt, 0, 0, "", true);
        }
        
-       String trans(String key) {
-               return m_bundle.get(key);
-       }
-       
-       String getenv(String varName) {
-               // This is a separate function so that we can override it at unit test time
-               return System.getenv(varName);
+       String trans(String key, Object... params) {
+               return getBundle(m_locale).get(key, params);
        }
        
        String getProperty(String propName) {
@@ -150,7 +197,7 @@ public class CFB {
        }
        
        void initArgs() {
-               String findBugsDir = getenv("FINDBUGS_HOME");
+               String findBugsDir = Env.get("FINDBUGS_HOME");
                if (null != findBugsDir) {
                        m_fbDir = new File(findBugsDir);
                }
@@ -158,60 +205,128 @@ public class CFB {
                if (null != findBugsDir) {
                        m_fbDir = new File(findBugsDir);
                }
-       } 
+       }
+       
+       void readConfig() throws IOException {
+               if (null != m_configFile) {
+                       m_config.readFile(m_configFile);
+               }
+       }
        
        void doMain(PrintWriter pw, String[] args) throws SQLException, IOException, XmlParseException, SAXException, TypeMismatchException {
                initArgs();     // read environment and system properties
                if ( ! parseArgs(pw, args) ) {
                        return;
                }
+               readConfig();
 
                File findBugsDir = getFindBugsDir();
                File workDir = new File(".");
                MessageMap messageMap = new MessageMap();
-               messageMap.load(findBugsDir, Locale.getDefault(Category.DISPLAY));
+               try {
+                       messageMap.load(findBugsDir, Locale.getDefault(Category.DISPLAY));
+               }
+               catch (FBMsgFileNotFoundException exc) {
+                       reportException(pw, exc);
+                       return;
+               }
+               
+               if (!ensureDbInitialized(pw, messageMap)) {
+                       return;
+               }
                
-               try (Connection con = m_driver.connect(m_host, m_port, m_dbName, m_user, m_pass)) {
+               Analyzer analyzer = new Analyzer(messageMap);
+               Analysis analysis = analyzer.analyze(pw, workDir, m_fbp, m_projName, m_buildNum);
+               if (null == analysis) {
+                       pw.println(trans(CfbBundle.ANALYSIS_FAILED));
+                       return;
+               }
+               
+               storeAndReport(pw, messageMap, analysis);
+       }
+
+       boolean ensureDbInitialized(PrintWriter pw, MessageMap messageMap)
+                       throws TypeMismatchException 
+       {
+               try (Connection con = m_driver.connect(
+                                       m_config.getDbHost(), m_config.getDbPort(), 
+                                       m_config.getDbName(), 
+                                       m_config.getDbUser(), m_config.getDbPass())
+                       ) 
+               {
                        m_schema.setMessageMap(messageMap);
                        
                        if (m_removeSchema) {
                                m_schema.purge(con);
-                               return;
+                               return false;   // do not continue execution
                        }
                        m_schema.ensureDbInitialized(con);
                        messageMap.loadIds(con, m_driver);
                }
                catch (SQLException exc) {
                        reportUnableToConnect(pw, exc);
-                       return;
-               }
-               
-               Analyzer analyzer = new Analyzer(messageMap);
-               Analysis analysis = analyzer.analyze(pw, workDir, m_fbp, m_buildNum);
-               if (null == analysis) {
-                       pw.println(trans(CfbBundle.ANALYSIS_FAILED));
-                       return;
+                       return false;   // do not continue execution
                }
                
-               try (Connection con = m_driver.connect(m_host, m_port, m_dbName, m_user, m_pass)) {
+               return true;    // all OK; continue execution
+       }
+
+       void storeAndReport(PrintWriter pw, MessageMap messageMap, Analysis analysis) 
+                       throws TypeMismatchException, IOException 
+       {
+               try (
+                               Connection con = m_driver.connect(
+                                               m_config.getDbHost(), m_config.getDbPort(), 
+                                               m_config.getDbName(),
+                                               m_config.getDbUser(), m_config.getDbPass()
+                                       )
+                       )
+               {
                        DbStore store = new DbStore(con, m_driver, messageMap.getColl());
                        
                        store.put(analysis);
                        Analysis prior = store.getPrior(analysis);
                        Delta delta = new Delta(prior, analysis);
 
-                       HtmlReport report = new HtmlReport(m_bundle, messageMap.getColl());
-                       report.write(m_output, delta);
+                       HtmlReport report = new HtmlReport(getBundle(m_locale), messageMap.getColl(), delta);
+                       if (null != m_output) {
+                               report.write(m_output);
+                       }
+                       
+                       Notifier notifier = new Notifier(getBundle(m_locale), m_config);
+                       notifier.sendEmailIfNeeded(pw, report);
+               }
+               catch (StoreException exc) {
+                       exc.printStackTrace(pw);
                }
                catch (SQLException exc) {
                        reportUnableToConnect(pw, exc);
-                       return;
                }
        }
+       
+       void reportException(PrintWriter pw, FBMsgFileNotFoundException exc) {
+               exc.printStackTrace(pw);
+               
+               pw.println(trans(CfbBundle.CANNOT_LOAD_FBMSG_FILE, exc.getFilename()));
+               
+               String fbHome = Env.get(FINDBUGS_HOME);
+               if (null == fbHome) {
+                       pw.println(trans(CfbBundle.FINDBUGS_HOME_IS_NOT_SET, FINDBUGS_HOME));
+               }
+               else {
+                       pw.println(trans(CfbBundle.FINDBUGS_HOME_IS_SET_TO, FINDBUGS_HOME, fbHome));
+               }
+
+       }
 
        private void reportUnableToConnect(PrintWriter pw, SQLException exc) {
                String cannotConnectFormat = trans(CfbBundle.CANNOT_CONNECT);
-               String cannotConnect = MessageFormat.format(cannotConnectFormat, m_host, ""+m_port, m_dbName, m_user);
+               String cannotConnect = MessageFormat.format(cannotConnectFormat, 
+                                                                                       m_config.getDbHost(), 
+                                                                                       ""+m_config.getDbPort(), 
+                                                                                       m_config.getDbName(), 
+                                                                                       m_config.getDbUser()
+                                                                               );
                exc.printStackTrace(pw);
                SQLException next = exc.getNextException();
                while (null != next) {
@@ -224,7 +339,7 @@ public class CFB {
        public static void main(String[] args) {
                CFB cfb = new CFB(Locale.getDefault());
                
-               try (PrintWriter pw = new PrintWriter(System.out)){
+               try (PrintWriter pw = new PrintWriter(new OutputStreamWriter(System.out, Charset.defaultCharset()))) {
                        cfb.doMain(pw, args);
                        pw.flush();
                } catch (SQLException | IOException | XmlParseException | SAXException | TypeMismatchException exc) {