From f829b23412e21d657d63a81897794e833ef162ab Mon Sep 17 00:00:00 2001 From: Chris Jaekl Date: Mon, 26 Oct 2015 22:37:13 +0900 Subject: [PATCH] Add email notifications. --- config.properties | 9 ++ prod/cfb.properties | 2 + prod/net/jaekl/cfb/CFB.java | 26 +++- prod/net/jaekl/cfb/CfbBundle.java | 2 + prod/net/jaekl/cfb/Config.java | 146 +++++++++++++++++++++ prod/net/jaekl/cfb/analyze/HtmlReport.java | 44 ++++--- prod/net/jaekl/cfb/analyze/Notifier.java | 87 ++++++++++++ setcp.sh | 10 +- test/net/jaekl/cfb/ConfigTest.java | 24 ++++ 9 files changed, 323 insertions(+), 27 deletions(-) create mode 100644 config.properties create mode 100644 prod/net/jaekl/cfb/Config.java create mode 100644 prod/net/jaekl/cfb/analyze/Notifier.java create mode 100644 test/net/jaekl/cfb/ConfigTest.java diff --git a/config.properties b/config.properties new file mode 100644 index 0000000..caf5e75 --- /dev/null +++ b/config.properties @@ -0,0 +1,9 @@ +; Path (relative or absolute) to the FINDBUGS_HOME, i.e., where FindBugs is installed +FindBugsHome=../findbugs-3.0.1/ + +; List (comma-separated) of email addresses to which notifications should be sent +notify=chris@localhost + +; Mail server setup +mail.smtp.host=localhost +mail.from=findbugs@localhost diff --git a/prod/cfb.properties b/prod/cfb.properties index e56f819..7ddb59d 100644 --- a/prod/cfb.properties +++ b/prod/cfb.properties @@ -2,7 +2,9 @@ analysis.failed=Attempt to analyze source code failed. Will now stop. analyzed.at=Analyzed at {0} cannot.connect.to.db=Unable to connect to, or to initialize, database {2} on {0}:{1} as user {3}. cannot.exec=Got result code {1} when attempting to execute command-line: {0} +cannot.send.mail=Attempt to send email to {0} failed: {1} cfb=Comparative FindBugs +cfb.mail.subject=CFB: {0} vs. {1} cfb.report=Comparative FindBugs Report fixed.bugs=Fixed Bugs new.bugs=New Bugs diff --git a/prod/net/jaekl/cfb/CFB.java b/prod/net/jaekl/cfb/CFB.java index 94ef08f..33a1ca0 100644 --- a/prod/net/jaekl/cfb/CFB.java +++ b/prod/net/jaekl/cfb/CFB.java @@ -21,6 +21,7 @@ import net.jaekl.cfb.analyze.Analyzer; import net.jaekl.cfb.analyze.Delta; 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; @@ -41,7 +42,10 @@ public class CFB { CfbBundle m_bundle; Locale m_locale; + Config m_config; + // Command-line parameters + File m_configFile; String m_dbName; // db name File m_fbp; // FindBugsProject file File m_fbDir; // Directory where FindBugs is installed @@ -58,7 +62,9 @@ public class CFB { m_schema = new CfbSchema(m_driver); m_locale = locale; m_bundle = CfbBundle.getInst(m_locale); + m_config = new Config(); + m_configFile = new File("config.properties"); m_dbName = "CFB"; m_fbp = null; m_fbDir = null; @@ -74,6 +80,7 @@ public class CFB { 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"); @@ -92,6 +99,9 @@ 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"); } @@ -158,13 +168,20 @@ 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("."); @@ -200,8 +217,11 @@ public class CFB { 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(m_bundle, messageMap.getColl(), delta); + report.write(m_output); + + Notifier notifier = new Notifier(m_bundle, m_config); + notifier.sendEmailIfNeeded(pw, report); } catch (SQLException exc) { reportUnableToConnect(pw, exc); diff --git a/prod/net/jaekl/cfb/CfbBundle.java b/prod/net/jaekl/cfb/CfbBundle.java index 8ea3c68..0cb7a40 100644 --- a/prod/net/jaekl/cfb/CfbBundle.java +++ b/prod/net/jaekl/cfb/CfbBundle.java @@ -15,7 +15,9 @@ public class CfbBundle { public static final String ANALYZED_AT = "analyzed.at"; public static final String CANNOT_CONNECT = "cannot.connect.to.db"; public static final String CANNOT_EXEC = "cannot.exec"; + public static final String CANNOT_SEND_MAIL = "cannot.send.mail"; public static final String CFB = "cfb"; + public static final String CFB_MAIL_SUBJECT = "cfb.mail.subject"; public static final String CFB_REPORT = "cfb.report"; public static final String COMPARING_RUNS = "comparing.runs"; public static final String COMPARING_VERSIONS = "comparing.versions"; diff --git a/prod/net/jaekl/cfb/Config.java b/prod/net/jaekl/cfb/Config.java new file mode 100644 index 0000000..8bb401d --- /dev/null +++ b/prod/net/jaekl/cfb/Config.java @@ -0,0 +1,146 @@ +package net.jaekl.cfb; + +import java.io.File; +import java.io.FileInputStream; +import java.io.IOException; +import java.io.PrintWriter; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.Properties; + +import org.apache.commons.cli.CommandLine; +import org.apache.commons.cli.GnuParser; +import org.apache.commons.cli.Options; +import org.apache.commons.cli.ParseException; + +public class Config { + private static final String FINDBUGS_HOME = "FindBugsHome"; + private static final String MAIL_FROM = "mail.from"; + private static final String MAIL_SMTP_HOST = "mail.smtp.host"; + private static final String NOTIFY = "notify"; + + File m_configProperties; + + String m_dbName; // db name + 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_buildNum; // build number (version) + boolean m_removeSchema; // purge DB schema + File m_output; // File to which we should write our output (report) + + String m_mailFrom; + String m_mailSmtpHost; + List m_notify; + + Options m_options; + + public Config() { + m_dbName = "CFB"; + m_fbp = null; + m_fbDir = null; + m_host = "localhost"; + m_port = 5432; + m_pass = ""; + m_user = "user"; + m_buildNum = null; + m_removeSchema = false; + m_output = null; + + m_mailFrom = "findbugs@localhost"; + m_mailSmtpHost = "localhost"; + m_notify = new ArrayList(); + + m_options = createOptions(); + } + + public String getMailFrom() { return m_mailFrom; } + public String getMailSmtpHost() { return m_mailSmtpHost; } + public ArrayList getNotify() { return new ArrayList(m_notify); } + + public void readFile(File configProperties) throws IOException + { + Properties props = new Properties(); + FileInputStream fis = null; + + try { + fis = new FileInputStream(configProperties); + props.load(fis); + } + finally { + if (null != fis) { + fis.close(); + } + } + + if (props.containsKey(FINDBUGS_HOME)) { + m_fbDir = new File(props.getProperty(FINDBUGS_HOME)); + } + if (props.containsKey(MAIL_FROM)) { + m_mailFrom = props.getProperty(MAIL_FROM); + } + if (props.containsKey(MAIL_SMTP_HOST)) { + m_mailSmtpHost = props.getProperty(MAIL_SMTP_HOST); + } + if (props.containsKey(NOTIFY)) { + String[] addresses = props.getProperty(NOTIFY).split(","); + m_notify = Arrays.asList(addresses); + } + } + + public 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("h", "host", true, "DB hostname"); + opt.addOption("n", "number", true, "Build number (version)"); + opt.addOption("o", "outfile", true, "Output report filename"); + opt.addOption("p", "pass", true, "DB password"); + opt.addOption("t", "port", true, "DB port"); + opt.addOption("u", "user", true, "DB username"); + + return opt; + } + + public void parseArgs(PrintWriter pw, String[] args) throws ParseException + { + assert(null != m_options); + + CommandLine line = new GnuParser().parse(m_options, args); + if (line.hasOption("c")) { + m_configProperties = new File(line.getOptionValue("c")); + } + if (line.hasOption("d")) { + m_dbName = line.getOptionValue("d"); + } + if (line.hasOption("f")) { + m_fbp = new File(line.getOptionValue("f")); + } + if (line.hasOption("h")) { + m_host = line.getOptionValue("h"); + } + if (line.hasOption("n")) { + m_buildNum = line.getOptionValue("n"); + } + if (line.hasOption("o")) { + m_output = new File(line.getOptionValue("o")); + } + if (line.hasOption("p")) { + m_pass = line.getOptionValue("p"); + } + m_removeSchema = line.hasOption("drop-tables"); + if (line.hasOption("t")) { + m_port = Integer.parseInt(line.getOptionValue("t")); + } + if (line.hasOption("u")) { + m_user = line.getOptionValue("u"); + } + } +} diff --git a/prod/net/jaekl/cfb/analyze/HtmlReport.java b/prod/net/jaekl/cfb/analyze/HtmlReport.java index 4482f0a..e2e76cb 100644 --- a/prod/net/jaekl/cfb/analyze/HtmlReport.java +++ b/prod/net/jaekl/cfb/analyze/HtmlReport.java @@ -13,33 +13,37 @@ import net.jaekl.cfb.xml.messages.MessageCollection; public class HtmlReport { CfbBundle m_bundle; + Delta m_delta; MessageCollection m_msgColl; - public HtmlReport(CfbBundle bundle, MessageCollection msgColl) + public HtmlReport(CfbBundle bundle, MessageCollection msgColl, Delta delta) { m_bundle = bundle; + m_delta = delta; m_msgColl = msgColl; } - public void write(File output, Delta delta) throws IOException + public Delta getDelta() { return m_delta; } + + public void write(File output) throws IOException { try ( FileOutputStream fos = new FileOutputStream(output); PrintWriter pw = new PrintWriter(fos); ) { - write(pw, delta); + write(pw); } } - void write(PrintWriter pw, Delta delta) + void write(PrintWriter pw) { - startPage(pw, delta); + startPage(pw); } - void startPage(PrintWriter pw, Delta delta) + void startPage(PrintWriter pw) { - writeHeader(pw, delta); + writeHeader(pw); pw.println(" "); - writeBody(pw, delta); + writeBody(pw); pw.println(" "); pw.println(""); } @@ -48,12 +52,12 @@ public class HtmlReport { return m_bundle.get(key, arguments); } - void writeBody(PrintWriter pw, Delta delta) + void writeBody(PrintWriter pw) { - writeSummary(pw, delta); - writeBugs(pw, CfbBundle.NEW_BUGS, delta.getNew()); - writeBugs(pw, CfbBundle.FIXED_BUGS, delta.getFixed()); - writeBugs(pw, CfbBundle.OLD_BUGS, delta.getCommon()); + writeSummary(pw); + writeBugs(pw, CfbBundle.NEW_BUGS, m_delta.getNew()); + writeBugs(pw, CfbBundle.FIXED_BUGS, m_delta.getFixed()); + writeBugs(pw, CfbBundle.OLD_BUGS, m_delta.getCommon()); } void writeBugLocations(PrintWriter pw, BugInstance bug) @@ -117,7 +121,7 @@ public class HtmlReport { } } - void writeHeader(PrintWriter pw, Delta delta) + void writeHeader(PrintWriter pw) { String title = trans(CfbBundle.CFB_REPORT); @@ -136,7 +140,7 @@ public class HtmlReport { pw.println("

"); } - void writeSummary(PrintWriter pw, Delta delta) + void writeSummary(PrintWriter pw) { final String SEP = ":  "; @@ -145,24 +149,24 @@ public class HtmlReport { pw.println(" "); pw.println(" "); pw.println(" "); - pw.println(" "); + pw.println(" "); pw.println(" "); pw.println(" "); pw.println(" "); - pw.println(" "); + pw.println(" "); pw.println(" "); pw.println(" "); pw.println(" "); pw.println(" "); - pw.println(" "); + pw.println(" "); pw.println(" "); pw.println(" "); pw.println(" "); - pw.println(" "); + pw.println(" "); pw.println(" "); pw.println(" "); pw.println(" "); - pw.println(" "); + pw.println(" "); pw.println(" "); pw.println("
" + trans(CfbBundle.NEW_VERSION) + SEP + "" + delta.getLater().constructVersionText(m_bundle) + "" + m_delta.getLater().constructVersionText(m_bundle) + "
" + trans(CfbBundle.OLD_VERSION) + SEP + "" + delta.getEarlier().constructVersionText(m_bundle) + "" + m_delta.getEarlier().constructVersionText(m_bundle) + "
 
" + trans(CfbBundle.NEW_BUGS) + SEP + "" + trans(CfbBundle.NUM_BUGS, delta.getNumNew()) + "" + trans(CfbBundle.NUM_BUGS, m_delta.getNumNew()) + "
" + trans(CfbBundle.FIXED_BUGS) + SEP + "" + trans(CfbBundle.NUM_BUGS, delta.getNumFixed()) + "" + trans(CfbBundle.NUM_BUGS, m_delta.getNumFixed()) + "
" + trans(CfbBundle.OLD_BUGS) + SEP + "" + trans(CfbBundle.NUM_BUGS_OLD, delta.getNumCommon()) + "" + trans(CfbBundle.NUM_BUGS_OLD, m_delta.getNumCommon()) + "
"); pw.println("

"); diff --git a/prod/net/jaekl/cfb/analyze/Notifier.java b/prod/net/jaekl/cfb/analyze/Notifier.java new file mode 100644 index 0000000..6203727 --- /dev/null +++ b/prod/net/jaekl/cfb/analyze/Notifier.java @@ -0,0 +1,87 @@ +package net.jaekl.cfb.analyze; + +import java.io.PrintWriter; +import java.io.StringWriter; +import java.util.ArrayList; +import java.util.Properties; + +import javax.mail.Message.RecipientType; +import javax.mail.MessagingException; +import javax.mail.Session; +import javax.mail.Transport; +import javax.mail.internet.InternetAddress; +import javax.mail.internet.MimeMessage; + +import net.jaekl.cfb.CfbBundle; +import net.jaekl.cfb.Config; + +public class Notifier { + private static final String MAIL_SMTP_HOST = "mail.smtp.host"; + private static final String TEXT_HTML = "text/html"; + + CfbBundle m_bundle; + Config m_config; + + public Notifier(CfbBundle bundle, Config config) { + m_bundle = bundle; + m_config = config; + } + + public void sendEmailIfNeeded(PrintWriter pw, HtmlReport report) { + Delta delta = report.getDelta(); + if ((delta.getNumNew() > 0) || (delta.getNumFixed() > 0)) { + sendEmail(pw, report); + } + } + + void sendEmail(PrintWriter pw, HtmlReport report) { + Properties props = System.getProperties(); + props.setProperty(MAIL_SMTP_HOST, m_config.getMailSmtpHost()); + Session sess = Session.getDefaultInstance(props); + + ArrayList recipients = m_config.getNotify(); + if (recipients.size() < 1) { + return; + } + + PrintWriter mailWriter = null; + + try { + MimeMessage msg = new MimeMessage(sess); + + String earlier = report.getDelta().getEarlier().constructVersionText(m_bundle); + String later = report.getDelta().getLater().constructVersionText(m_bundle); + + msg.setFrom(new InternetAddress(m_config.getMailFrom())); + msg.setSubject(m_bundle.get(CfbBundle.CFB_MAIL_SUBJECT, earlier, later)); + + for (String recipient : recipients) { + msg.addRecipient(RecipientType.TO, new InternetAddress(recipient)); + } + + StringWriter sw = new StringWriter(); + mailWriter = new PrintWriter(sw); + report.write(mailWriter); + mailWriter.flush(); + + msg.setContent(sw.toString(), TEXT_HTML); + Transport.send(msg); + } + catch (MessagingException exc) { + StringBuilder toList = new StringBuilder(); + for (String recipient : recipients) { + if (toList.length() > 0) { + toList.append(", "); + } + toList.append(recipient); + } + pw.println(m_bundle.get(CfbBundle.CANNOT_SEND_MAIL, toList.toString(), exc.toString())); + exc.printStackTrace(pw); + } + finally { + if (null != mailWriter) { + mailWriter.close(); + } + } + } +} diff --git a/setcp.sh b/setcp.sh index a27737a..3346766 100644 --- a/setcp.sh +++ b/setcp.sh @@ -4,12 +4,14 @@ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )" export CLASSPATH=${SCRIPT_DIR}/bin -for x in ${SCRIPT_DIR}/lib/*.jar -do - export CLASSPATH=${x}:${CLASSPATH} -done +#for x in ${SCRIPT_DIR}/lib/*.jar +#do +# export CLASSPATH=${x}:${CLASSPATH} +#done export CLASSPATH=/usr/share/java/commons-cli.jar:${CLASSPATH} export CLASSPATH=/usr/share/java/junit4.jar:${CLASSPATH} export CLASSPATH=/usr/share/java/postgresql.jar:${CLASSPATH} +export CLASSPATH=/usr/share/java/gnumail.jar:${CLASSPATH} +export CLASSPATH=/usr/share/java/activation.jar:${CLASSPATH} #export CLASSPATH=${SCRIPT_DIR}/jcov/jcov.jar:${CLASSPATH} diff --git a/test/net/jaekl/cfb/ConfigTest.java b/test/net/jaekl/cfb/ConfigTest.java new file mode 100644 index 0000000..3fafe87 --- /dev/null +++ b/test/net/jaekl/cfb/ConfigTest.java @@ -0,0 +1,24 @@ +package net.jaekl.cfb; + +import static org.junit.Assert.*; + +import org.junit.Test; + +public class ConfigTest { + + private static final String SAMPLE1 = + "; Path (relative or absolute) to the FINDBUGS_HOME, i.e., where FindBugs is installed\n" + + "FindBugsHome=../findbugs-3.0.1/\n" + + "; List (comma-separated) of email addresses to which notifications should be sent\n" + + "notify=chris@localhost\n" + + "\n" + + "; Mail server setup\n" + + "mail.smtp.host=localhost\n" + + "mail.from=findbugs@localhost\n"; + + @Test + public void testReadFile() { + // TODO: wrap file access so that we can test this + } + +} -- 2.39.2