package de.stephannoske.hudson.tools; import hudson.Extension; import hudson.Launcher; import hudson.ProxyConfiguration; import hudson.model.AbstractBuild; import hudson.model.AbstractProject; import hudson.model.BuildListener; import hudson.model.Result; import hudson.tasks.BuildStepDescriptor; import hudson.tasks.BuildStepMonitor; import hudson.tasks.Notifier; import hudson.tasks.Publisher; import hudson.util.FormValidation; import java.io.IOException; import java.io.InputStream; import java.io.StringReader; import java.io.UnsupportedEncodingException; import java.net.MalformedURLException; import java.net.URL; import java.net.URLConnection; import java.net.URLEncoder; import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.logging.Level; import javax.servlet.ServletException; import javax.xml.stream.FactoryConfigurationError; import javax.xml.stream.XMLInputFactory; import javax.xml.stream.XMLStreamConstants; import javax.xml.stream.XMLStreamException; import javax.xml.stream.XMLStreamReader; import net.sf.json.JSONObject; import org.apache.commons.io.IOUtils; import org.apache.commons.lang.StringUtils; import org.kohsuke.stapler.DataBoundConstructor; import org.kohsuke.stapler.QueryParameter; import org.kohsuke.stapler.StaplerRequest; /** * @author snoske * @author eric.lemerdy */ public class NabatzagPublisher extends Notifier { public static final class DescriptorImpl extends BuildStepDescriptor<Publisher> { public String nabatzagToken = ""; public String nabatzagSerial = ""; public String nabatzagUrl = "http://api.nabaztag.com/vl/FR/api.jsp"; public String nabatzagVoice = "UK-Penelope"; public String nabatzagFAILDpos = "posright=8&posleft=8&ears=ok"; public String nabatzagSUSSCEEDpos = "posright=0&posleft=0&ears=ok"; public String nabatzagBUILDEDpos = "posright=4&posleft=12&ears=ok"; public String nabatzagFailTTS = "Failure of build \"${buildNumber}\" in project \"${projectName}\"."; public String nabatzagSuccessTTS = "Success of build \"${buildNumber}\" in project \"${projectName}\"."; public String nabatzagRecoverTTS = "Project \"${projectName}\" recovered at build \"${buildNumber}\"."; public String nabatzagBuildTTS = "Build \"${buildNumber}\" of project \"${projectName}\" has started."; public boolean reportOnSucess = false; public boolean notifyOnBuildStart = false; protected DescriptorImpl() { super(NabatzagPublisher.class); load(); } @Override public boolean isApplicable(Class<? extends AbstractProject> jobType) { return true; } @Override public boolean configure(final StaplerRequest req, JSONObject json) throws FormException { nabatzagVoice = req.getParameter("nabatzagVoice"); nabatzagSerial = req.getParameter("nabatzagSerial"); nabatzagUrl = req.getParameter("nabatzagUrl"); nabatzagToken = req.getParameter("nabatzagToken"); reportOnSucess = "on".equals(req.getParameter("reportOnSucess")); nabatzagFailTTS = req.getParameter("nabatzagFailTTS"); nabatzagSuccessTTS = req.getParameter("nabatzagSuccessTTS"); nabatzagRecoverTTS = req.getParameter("nabatzagRecoverTTS"); nabatzagBuildTTS = req.getParameter("nabatzagBuildTTS"); notifyOnBuildStart = "on".equals(req.getParameter("nabatzagNotifyOnBuildStart")); save(); return super.configure(req, json); } @Override public String getDisplayName() { return "Nabatzag Publisher "; } public String getNabatzagFAILDpos() { return nabatzagFAILDpos; } public String getNabatzagFailTTS() { return nabatzagFailTTS; } public String getNabatzagRecoverTTS() { return nabatzagRecoverTTS; } public String getNabatzagSerial() { return nabatzagSerial; } public String getNabatzagSuccessTTS() { return nabatzagSuccessTTS; } public String getNabatzagSUSSCEEDpos() { return nabatzagSUSSCEEDpos; } public String getNabatzagBuildTTS() { return nabatzagBuildTTS; } public String getNabatzagBUILDEDpos() { return nabatzagBUILDEDpos; } public String getNabatzagToken() { return nabatzagToken; } public String getNabatzagUrl() { return nabatzagUrl; } public String getNabatzagVoice() { return nabatzagVoice; } public boolean isReportOnSucess() { return reportOnSucess; } public void setNabatzagFAILDpos(final String nabatzagFAILDpos) { this.nabatzagFAILDpos = nabatzagFAILDpos; } public void setNabatzagFailTTS(final String nabatzagFailTTS) { this.nabatzagFailTTS = nabatzagFailTTS; } public void setNabatzagRecoverTTS(final String nabatzagRecoverTTS) { this.nabatzagRecoverTTS = nabatzagRecoverTTS; } public void setNabatzagSerial(final String nabatzagSerial) { this.nabatzagSerial = nabatzagSerial; } public void setNabatzagSuccessTTS(final String nabatzagSuccessTTS) { this.nabatzagSuccessTTS = nabatzagSuccessTTS; } public void setNabatzagSUSSCEEDpos(final String nabatzagSUSSCEEDpos) { this.nabatzagSUSSCEEDpos = nabatzagSUSSCEEDpos; } public void setNabatzagBuildTTS(final String nabatzagBuildTTS) { this.nabatzagBuildTTS = nabatzagBuildTTS; } public void setNabatzagBUILDEDpos(final String nabatzagBUILDEDpos) { this.nabatzagBUILDEDpos = nabatzagBUILDEDpos; } public void setNabatzagToken(final String nabatzagToken) { this.nabatzagToken = nabatzagToken; } public void setNabatzagUrl(final String nabatzagUrl) { this.nabatzagUrl = nabatzagUrl; } public void setNabatzagVoice(final String nabatzagVoice) { this.nabatzagVoice = nabatzagVoice; } public void setReportOnSucess(final boolean reportOnSucess) { this.reportOnSucess = reportOnSucess; } public boolean isNotifyOnBuildStart() { return notifyOnBuildStart; } public void setNotifyOnBuildStart(boolean notifyOnBuildStart) { this.notifyOnBuildStart = notifyOnBuildStart; } public FormValidation doTestCredentials(@QueryParameter String nabatzagSerial, @QueryParameter String nabatzagToken) throws IOException, ServletException { String requestString = buildRequestWithAWakeUpAction(nabatzagSerial, nabatzagToken); log.finest(" sending nabatztag request : " + requestString); URLConnection cnx = ProxyConfiguration.open(new URL(requestString)); cnx.connect(); InputStream inputStream = cnx.getInputStream(); String result = IOUtils.toString(inputStream); log.finest("API call result : " + result); Map<String, String> messages = parseAndExtractMessages(result); if (messages.containsKey("COMMANDSENT")) { return FormValidation.ok("Credentials are valid and your rabbit is awake."); } if (messages.containsKey("NOGOODTOKENORSERIAL")) { return FormValidation.error(messages.get("NOGOODTOKENORSERIAL")); } if (messages.containsKey("NOGOODSERIAL")) { return FormValidation.error(messages.get("NOGOODSERIAL")); } return FormValidation.error("Unexpected API result: " + messages.toString()); } } /** * the DESCRIPTOR */ @Extension public static final DescriptorImpl DESCRIPTOR = new DescriptorImpl(); /** the Logger */ private static java.util.logging.Logger log = java.util.logging.Logger.getLogger(NabatzagPublisher.class.getName()); @DataBoundConstructor public NabatzagPublisher() { super(); } private String buildRequestWithAMessageAVoiceAndSomeEarPosition(final String message, final String earpos) { final StringBuilder buf = buildFirstCommonRequest(DESCRIPTOR.getNabatzagSerial(), DESCRIPTOR.getNabatzagToken()); buf.append("tts=").append(message); buf.append("&"); buf.append("voice=").append(DESCRIPTOR.getNabatzagVoice()); buf.append("&"); buf.append(StringUtils.defaultString(earpos)); return buf.toString(); } private static StringBuilder buildFirstCommonRequest(String serialNumber, String token) { final StringBuilder buf = new StringBuilder(); buf.append(DESCRIPTOR.getNabatzagUrl()).append("?"); buf.append("sn=").append(serialNumber); buf.append("&"); buf.append("token=").append(token); buf.append("&"); return buf; } protected static String buildRequestWithAWakeUpAction(String serialNumber, String token) { final StringBuilder buf = buildFirstCommonRequest(serialNumber, token); buf.append("action=14"); return buf.toString(); } @Override public boolean prebuild(final AbstractBuild<?, ?> build, BuildListener listener) { if (DESCRIPTOR.isNotifyOnBuildStart()) { String msg = DESCRIPTOR.getNabatzagBuildTTS(); log.finest("Nabaztag Build BEGIN"); sendRequest(msg, DESCRIPTOR.getNabatzagBUILDEDpos(), build, listener); } return true; } @Override public boolean perform(final AbstractBuild<?, ?> build, final Launcher launcher, final BuildListener listener) throws InterruptedException, IOException { if (!isSNAndTokenDefined()) { listener.getLogger().println("Nabaztag Serial Number or Token are not defined, notification has not been sent."); return false; } String msg; // Build FAILURE if ((build.getResult() == Result.FAILURE) || (build.getResult() == Result.UNSTABLE)) { msg = DESCRIPTOR.getNabatzagFailTTS(); log.finest("Nabaztag Build FAILURE"); sendRequest(msg, DESCRIPTOR.getNabatzagFAILDpos(), build, listener); } else if (build.getResult() == Result.SUCCESS) { // Build RECOVERY if (build.getPreviousBuild() != null && build.getPreviousBuild().getResult() == Result.FAILURE) { msg = DESCRIPTOR.getNabatzagRecoverTTS(); log.finest("Nabaztag Build RECOVERY"); sendRequest(msg, DESCRIPTOR.getNabatzagSUSSCEEDpos(), build, listener); } // Build SUCCESS else if (DESCRIPTOR.reportOnSucess) { msg = DESCRIPTOR.getNabatzagSuccessTTS(); log.finest("Nabaztag Build SUCCESS"); sendRequest(msg, DESCRIPTOR.getNabatzagSUSSCEEDpos(), build, listener); } else { listener.getLogger().println("User has choosen not to be notified of success, notification has not been sent."); } } else { listener.getLogger().println("Build result not handled by Nabaztag notifier, notification has not been sent."); } return true; } public BuildStepMonitor getRequiredMonitorService() { return BuildStepMonitor.BUILD; } private boolean isSNAndTokenDefined() { return StringUtils.isNotBlank(DESCRIPTOR.nabatzagSerial) && StringUtils.isNotBlank(DESCRIPTOR.nabatzagSerial); } /** * @param message * @param earpos * @param build * @param listener * @param build */ private void sendRequest(final String message, final String earpos, AbstractBuild<?,?> build, BuildListener listener) { String substituedMessage = StringUtils.replaceEach( message, new String[]{"${projectName}", "${buildNumber}"}, new String[]{build.getProject().getName(), String.valueOf(build.getNumber())} ); String urlEncodedMessage = null; URLConnection cnx = null; InputStream inputStream = null; try { urlEncodedMessage = URLEncoder.encode(substituedMessage, "UTF-8"); String requestString = buildRequestWithAMessageAVoiceAndSomeEarPosition(urlEncodedMessage, earpos); log.finest(" sending nabatztag request : " + requestString); cnx = ProxyConfiguration.open(new URL(requestString)); cnx.connect(); inputStream = cnx.getInputStream(); String result = IOUtils.toString(inputStream); log.finest("API call result : " + result.toString()); analyseResult(result.toString(), listener, new ArrayList<String>(Arrays.asList(new String[]{"EARPOSITIONSENT","POSITIONEAR","TTSSENT"}))); } catch (UnsupportedEncodingException notFatal) { log.log(Level.WARNING, "URL is malformed.", notFatal); listener.error("Unable to url encode the Nabaztag message."); } catch (MalformedURLException dontCare) { log.log(Level.WARNING, "URL is malformed.", dontCare); listener.error("Unable to build a valid Nabaztag API call."); } catch (IOException notImportant) { log.log(Level.WARNING, "IOException while reading API call result.", notImportant); listener.error("Nabaztag has not been successfully notified."); } finally { IOUtils.closeQuietly(inputStream); } } protected void analyseResult(String contentResult, BuildListener listener, List<String> expectedCommands) { List<String> unExpectedCommands = new ArrayList<String>(); for (String message : parseAndExtractMessages(contentResult).keySet()) { if (expectedCommands.contains(message)) { expectedCommands.remove(message); } else { unExpectedCommands.add(message); } } boolean success = true; StringBuilder out = new StringBuilder(); if (!expectedCommands.isEmpty()) { success = false; out.append("Following expected confirmations has not been received: "); out.append(expectedCommands); out.append("\n"); } if (!unExpectedCommands.isEmpty()) { success = false; out.append("Following unexpected messages has been received: "); out.append(unExpectedCommands); out.append(". "); } if (success) { listener.getLogger().println("Nabaztag has been successfully notified."); } else { listener.getLogger().println("Nabaztag has not been successfully notified: "); listener.getLogger().println(out); } } private static Map<String, String> parseAndExtractMessages(String contentResult) { Map<String, String> messages = new HashMap<String, String>(); String currentElementText = null; XMLStreamReader xmlStreamReader; try { xmlStreamReader = XMLInputFactory.newInstance().createXMLStreamReader(new StringReader(contentResult)); while (xmlStreamReader.hasNext()) { int next = xmlStreamReader.next(); if (next == XMLStreamConstants.START_ELEMENT) { String currentElement = xmlStreamReader.getName().getLocalPart(); if (currentElement.equals("message")) { currentElementText = xmlStreamReader.getElementText(); messages.put(currentElementText, ""); } else if (currentElement.equals("comment")) { messages.put(currentElementText, xmlStreamReader.getElementText()); } } } } catch (XMLStreamException e) { log.log(Level.WARNING, "Unable to read xml result.", e); } catch (FactoryConfigurationError e) { log.log(Level.WARNING, "Unable to create xml parser to read xml result.", e); } return messages; } }