/******************************************************************************** * CruiseControl, a Continuous Integration Toolkit * Copyright (c) 2001-2003, ThoughtWorks, Inc. * 651 W Washington Ave. Suite 600 * Chicago, IL 60661 USA * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * + Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * + Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following * disclaimer in the documentation and/or other materials provided * with the distribution. * * + Neither the name of ThoughtWorks, Inc., CruiseControl, nor the * names of its contributors may be used to endorse or promote * products derived from this software without specific prior * written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE REGENTS OR * CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, * EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR * PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING * NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS * SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ********************************************************************************/ package net.sourceforge.cruisecontrol.publishers; import java.io.File; import java.io.IOException; import java.io.StringWriter; import java.net.MalformedURLException; import java.net.URL; import java.util.Calendar; import java.util.Collection; import java.util.HashMap; import java.util.Hashtable; import java.util.Iterator; import java.util.Map; import java.util.StringTokenizer; import java.util.Vector; import javax.xml.transform.Transformer; import javax.xml.transform.TransformerException; import javax.xml.transform.TransformerFactory; import javax.xml.transform.stream.StreamResult; import javax.xml.transform.stream.StreamSource; import net.sourceforge.cruisecontrol.CruiseControlException; import net.sourceforge.cruisecontrol.Publisher; import net.sourceforge.cruisecontrol.util.XMLLogHelper; import org.apache.log4j.Logger; import org.apache.xmlrpc.XmlRpcClient; import org.jdom.Element; /** *
* Used to publish a blog entry based on the build report using the Blogger API, * MetaWeblog API or the LiveJournal API. *
** Here's a sample of the publisher element to put into your config.xml: *
* ** <weblog blogurl="http://yourblogserver:port/blog/xmlrpc" * api="metaweblog" * blogid="yourblog" * username="user1" * password="secret" * category="cruisecontrol" * reportsuccess="fixes" * subjectprefix="[CC]" * buildresultsurl="http://yourbuildserver:port/cc/buildresults" * logdir="/var/cruisecontrol/logs/YourProject" * xsldir="/opt/cruisecontrol/reporting/jsp/xsl" * css="/opt/cruisecontrol/reporting/jsp/css/cruisecontrol.css" * /> ** *
* And you also need to register the 'weblog' task with the following entry if * you're using this task with an older version of CruiseControl which doesn't * have the WeblogPublisher registered by default. * *
* * <project name="foo"> * <plugin name="weblog" * classname="net.sourceforge.cruisecontrol.publishers.WeblogPublisher"/> * ... * </project> * ** * @author Lasse Koskela */ public class WeblogPublisher implements Publisher { private static final Logger LOG = Logger.getLogger(WeblogPublisher.class); private static final String APP_KEY = "CruiseControl Blog Publisher"; private static final String DEFAULT_API = "metaweblog"; private static final String DEFAULT_REPORTSUCCESS = "always"; private static final boolean DEFAULT_SPAMWHILEBROKEN = true; // blogging configurations private String blogId; private String api = DEFAULT_API; private String username; private String password; private String category = ""; private String blogUrl; private String buildResultsURL; private String reportSuccess = DEFAULT_REPORTSUCCESS; private boolean spamWhileBroken = DEFAULT_SPAMWHILEBROKEN; private String subjectPrefix; // transformation resources private String xslFile; private String xslDir; private String css; private String logDir; private String[] xslFileNames = { "header.xsl", "maven.xsl", "checkstyle.xsl", "compile.xsl", "javadoc.xsl", "unittests.xsl", "modifications.xsl", "distributables.xsl" }; private static final Map API_CLIENTS = new HashMap(); static { API_CLIENTS.put("metaweblog", MetaWeblogApiClient.class); API_CLIENTS.put("blogger", BloggerApiClient.class); API_CLIENTS.put("livejournal", LiveJournalApiClient.class); } // --- ACCESSORS --- /** * If xslFile is set then both xslDir and css are ignored. Specified xslFile * must take care of entire document -- html open/close, body tags, styles, * etc. */ public void setXSLFile(String fullPathToXslFile) { xslFile = fullPathToXslFile; } /** * Directory where xsl files are located. */ public void setXSLDir(String xslDirectory) { xslDir = xslDirectory; } /** * Method to override the default list of file names that will be looked for * in the directory specified by xslDir. By default these are the standard * CruseControl xsl files:
Publisher interface.
*
* @param cruisecontrolLog
* The build results XML
*/
public void publish(Element cruisecontrolLog) {
XMLLogHelper helper = new XMLLogHelper(cruisecontrolLog);
try {
if (shouldSend(helper)) {
postBlogEntry(createSubject(helper), createMessage(helper
.getProjectName(), helper.getLogFileName()));
} else {
LOG.debug("shouldSend() indicated we should not"
+ " post a blog entry at this time");
}
} catch (CruiseControlException e) {
LOG.error("", e);
}
}
/**
* The interface for abstracting away the specific blogging API being used.
*
* @author Lasse Koskela
*/
interface BloggingApi {
/**
* Post a new blog entry.
*
* @param subject
* The blog entry's subject.
* @param content
* The blog entry's content.
* @return The newly created blog entry's identifier.
*/
public Object newPost(String subject, String content);
}
/**
* A BloggingApi implementation for the Blogger API.
*
* @author Lasse Koskela
*/
class BloggerApiClient implements BloggingApi {
public Object newPost(String subject, String content) {
// the Blogger API doesn't support titles for blog entries so
// we're using the common (de facto standard) workaround to embed
// the title into the content and let the weblog software parse
// the title from there, if supported.
content = "XMLLogHelper wrapper for the build log.
* @return whether or not the mail message should be sent.
*/
boolean shouldSend(XMLLogHelper logHelper) throws CruiseControlException {
if (logHelper.isBuildSuccessful()) {
return shouldSendForSuccessfulBuild(logHelper);
} else {
return shouldSendForFailedBuild(logHelper);
}
}
/**
* Determines if the conditions are right for the blog entry to be posted.
*
* @param logHelper
* XMLLogHelper wrapper for the build log.
* @return whether or not the mail message should be sent.
*/
boolean shouldSendForFailedBuild(XMLLogHelper logHelper)
throws CruiseControlException {
if (!logHelper.wasPreviousBuildSuccessful()
&& logHelper.isBuildNecessary() && !spamWhileBroken) {
LOG.debug("spamWhileBroken is false, not sending email");
return false;
} else {
return true;
}
}
/**
* Determines if the conditions are right for the blog entry to be posted.
*
* @param logHelper
* XMLLogHelper wrapper for the build log.
* @return whether or not the mail message should be sent.
*/
boolean shouldSendForSuccessfulBuild(XMLLogHelper logHelper)
throws CruiseControlException {
if (reportSuccess.equalsIgnoreCase(DEFAULT_REPORTSUCCESS)) {
return true;
} else if (reportSuccess.equalsIgnoreCase("never")) {
return false;
} else if (reportSuccess.equalsIgnoreCase("fixes")) {
if (logHelper.wasPreviousBuildSuccessful()) {
LOG.debug("reportSuccess is set to 'fixes', "
+ "not sending emails for repeated "
+ "successful builds.");
return false;
}
}
return true;
}
/**
* Creates the subject for the blog entry.
*
* @param logHelper
* XMLLogHelper wrapper for the build log.
* @return String containing the subject line.
*/
String createSubject(XMLLogHelper logHelper) throws CruiseControlException {
String projectName = logHelper.getProjectName();
String label = logHelper.getLabel();
boolean buildSuccessful = logHelper.isBuildSuccessful();
boolean isFix = logHelper.isBuildFix();
return createSubject(projectName, label, buildSuccessful, isFix);
}
/**
* Creates the subject for the blog entry.
*
* @param logHelper
* XMLLogHelper wrapper for the build log.
* @return String containing the subject line.
*/
String createSubject(String projectName, String label,
boolean buildSuccessful, boolean isFix)
throws CruiseControlException {
StringBuffer subject = new StringBuffer();
if (subjectPrefix != null && subjectPrefix.trim().length() > 0) {
subject.append(subjectPrefix).append(" ");
}
subject.append(projectName);
if (buildSuccessful) {
if (label.length() > 0) {
subject.append(" ").append(label);
}
subject.append(isFix ? " - Build Fixed" : " - Build Successful");
} else {
subject.append(" - Build Failed");
}
return subject.toString();
}
/**
* Create the text to be blogged.
*
* @param logHelper
* utility object that has parsed the log files
* @return created message; empty string if logDir not set
*/
String createMessage(String projectName, String logFileName) {
String message = "";
File inFile = null;
try {
if (logDir == null) {
logDir = getDefaultLogDir(projectName);
}
inFile = new File(logDir, logFileName);
message = transform(inFile);
} catch (Exception ex) {
LOG.error("error transforming " + inFile.getAbsolutePath(), ex);
message = createLinkLine(logFileName);
}
return message;
}
String getDefaultLogDir(String projectName) throws CruiseControlException {
// TODO: extract this duplication with ProjectXMLHelper.getLog()
// into a single method somewhere
return "logs" + File.separator + projectName;
}
String transform(File xml) throws TransformerException, IOException {
StringBuffer messageBuffer = new StringBuffer();
if (xslFile != null) {
transformWithSingleStylesheet(xml, messageBuffer);
} else {
messageBuffer.append(createLinkLine(xml.getName()));
transformWithMultipleStylesheets(xml, messageBuffer);
}
return messageBuffer.toString();
}
void transformWithMultipleStylesheets(File inFile,
StringBuffer messageBuffer) throws IOException,
TransformerException {
TransformerFactory tFactory = TransformerFactory.newInstance();
File xslDirectory = new File(xslDir);
String[] fileNames = getXslFileNames();
for (int i = 0; i < fileNames.length; i++) {
String fileName = fileNames[i];
File xsl = new File(xslDirectory, fileName);
messageBuffer.append("\n"); appendTransform(inFile, xsl, messageBuffer, tFactory); } } void transformWithSingleStylesheet(File inFile, StringBuffer messageBuffer) throws IOException, TransformerException { TransformerFactory tFactory = TransformerFactory.newInstance(); appendTransform(inFile, new File(xslFile), messageBuffer, tFactory); } void appendTransform(File xml, File xsl, StringBuffer messageBuffer, TransformerFactory tFactory) throws TransformerException { LOG.debug("Transforming file " + xml.getName() + " with " + xsl.getName() + " ..."); Transformer tformer = tFactory.newTransformer(new StreamSource(xsl)); StringWriter sw = new StringWriter(); try { tformer.transform(new StreamSource(xml), new StreamResult(sw)); LOG.debug("Transformed file " + xml.getName() + " with " + xsl.getName() + " ..."); } catch (Exception e) { LOG.error("error transforming with xslFile " + xsl.getName(), e); return; } messageBuffer.append(sw.toString()); } String createLinkLine(String logFileName) { if (buildResultsURL == null) { return ""; } String url = createBuildResultsUrl(logFileName); StringBuffer linkLine = new StringBuffer(); linkLine.append("
View results here -> "); linkLine.append(url); linkLine.append("
"); return linkLine.toString(); } String createBuildResultsUrl(String logFileName) { int startName = logFileName.lastIndexOf(File.separator) + 1; int endName = logFileName.lastIndexOf("."); String baseLogFileName = logFileName.substring(startName, endName); StringBuffer url = new StringBuffer(buildResultsURL); if (buildResultsURL.indexOf("?") == -1) { url.append("?"); } else { url.append("&"); } url.append("log="); url.append(baseLogFileName); return url.toString(); } }