/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2006, ThoughtWorks, Inc.
* 200 E. Randolph, 25th Floor
* Chicago, IL 60601 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.listeners;
import java.io.File;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Date;
import java.util.Iterator;
import java.util.List;
import net.sourceforge.cruisecontrol.CruiseControlException;
import net.sourceforge.cruisecontrol.Listener;
import net.sourceforge.cruisecontrol.ProjectEvent;
import net.sourceforge.cruisecontrol.ProjectState;
import net.sourceforge.cruisecontrol.util.CurrentBuildFileWriter;
import net.sourceforge.cruisecontrol.util.DateUtil;
import net.sourceforge.cruisecontrol.util.IO;
import net.sourceforge.cruisecontrol.util.ValidationHelper;
import org.apache.log4j.Logger;
/**
* Updates replaceable text in a pattern file each time the Project status changes. Can show full project status
* history. The following items will be replaced with their values each time they occur in the source file:
* <ul>
* <li>{Project} - Project Name.</li>
* <li>{State.Name} - Name of current project state.</li>
* <li>{State.Description} - Description of current project state.</li>
* <li>{State.Date} - Date/time the current state happened</li>
* <li>{State.Duration} - How long since this state was in effect. (Only useful in {History} line.)
* <li>{History} - Historical states. Must be first on line. This line will be processed and output once for each state
* the project has previously been in. The {History} tag will be deleted from the line.</li>
* </ul>
* <p>
* {@link DateUtil} for the dateformat
*
* @author John Lussmyer
*/
public class CurrentBuildStatusPageListener implements Listener {
private static final long serialVersionUID = -2710491917137221293L;
private static final Logger LOG = Logger.getLogger(CurrentBuildStatusPageListener.class);
/** Name of file to write/create */
private String dstFileName;
/** File to read pattern text from */
private File sourceFile = null;
/** Pattern text to use, contains String objects */
private List<String> sourceText = new ArrayList<String>();
/** Default text to use if no source file provided. */
private static final String DEFAULT_TEXT = "{Project}: {State.Date} - {State.Name}: {State.Description}";
/** Historical Status changes, contains HistoryItem objects */
private final List<HistoryItem> history = new ArrayList<HistoryItem>();
private static final String KEY_PROJECT = "{project}";
private static final String KEY_NAME = "{state.name}";
private static final String KEY_DESC = "{state.description}";
private static final String KEY_DATE = "{state.date}";
private static final String KEY_DURATION = "{state.duration}";
private static final String KEY_HISTORY = "{history}";
/**
* Holds info about a project state that has happened.
*
* @author jlussmyer Created on: Jan 12, 2006
*/
private class HistoryItem implements Serializable {
private static final long serialVersionUID = -5271600385796774883L;
public HistoryItem() {
// needed for serialization
}
public HistoryItem(ProjectState projstate) {
state = projstate.getName();
desc = projstate.getDescription();
when = System.currentTimeMillis();
}
public String state;
public String desc;
public long when;
}
/**
* Default constructor just used for initialization of local members.
*/
public CurrentBuildStatusPageListener() {
sourceText.add(DEFAULT_TEXT);
}
public void handleEvent(final ProjectEvent event) throws CruiseControlException {
if (!(event instanceof ProjectStateChangedEvent)) {
// ignore other ProjectEvents
LOG.debug("ignoring event " + event.getClass().getName() + " for project " + event.getProjectName());
return;
}
final ProjectStateChangedEvent stateChanged = (ProjectStateChangedEvent) event;
final ProjectState newState = stateChanged.getNewState();
LOG.debug("updating status to " + newState.getName() + " for project " + stateChanged.getProjectName());
final HistoryItem hist = new HistoryItem(newState);
final String result = substituteText(hist, stateChanged.getProjectName());
history.add(0, hist);
IO.write(dstFileName, result);
}
/**
* Perform all the needed text substitutions.
*
* @param current current history item
* @param projectName project name
* @return String resulting form substituting entries from sourceText.
*/
private String substituteText(final HistoryItem current, final String projectName) {
final StringBuilder result = new StringBuilder();
for (final String src : sourceText) {
// See if we need to output this line once for each historical state
if (src.toLowerCase().startsWith(KEY_HISTORY)) {
final String srcSub = src.substring(KEY_HISTORY.length());
Iterator histIter = history.iterator();
long prevtime = current.when;
while (histIter.hasNext()) {
HistoryItem hist = (HistoryItem) histIter.next();
result.append(substituteItems(srcSub, projectName, hist, prevtime));
result.append('\n');
prevtime = hist.when;
}
} else {
// Just do this line once
result.append(substituteItems(src, projectName, current, 0));
result.append('\n');
}
}
return result.toString();
}
/**
* Substitute values for any and all key items in a line of text.
*
* @param src
* Source line to have substitutions made
* @param projectName
* Name of project being processed
* @param current
* current project state information
* @param prevtime
* used to get duration
* @return altered string
*/
private String substituteItems(String src, final String projectName, final HistoryItem current,
final long prevtime) {
int idx;
final StringBuilder result = new StringBuilder();
// Find and substitute entries in this line
while ((idx = src.indexOf('{')) != -1) {
if (idx > 0) {
result.append(src.substring(0, idx));
}
src = src.substring(idx); // Trim off preceding text
int skiplen;
if (src.toLowerCase().startsWith(KEY_PROJECT)) {
result.append(projectName);
skiplen = KEY_PROJECT.length();
} else if (src.toLowerCase().startsWith(KEY_NAME)) {
result.append(current.state);
skiplen = KEY_NAME.length();
} else if (src.toLowerCase().startsWith(KEY_DESC)) {
result.append(current.desc);
skiplen = KEY_DESC.length();
} else if (src.toLowerCase().startsWith(KEY_DATE)) {
result.append(DateUtil.formatIso8601(new Date(current.when)));
skiplen = KEY_DATE.length();
} else if (src.toLowerCase().startsWith(KEY_DURATION)) {
result.append(formatDuration(prevtime - current.when));
skiplen = KEY_DURATION.length();
} else {
result.append('{');
skiplen = 1;
}
if (skiplen > 0) {
src = src.substring(skiplen); // remove KEY text
}
}
result.append(src); // Pick up any leftover text
return result.toString();
}
/**
* formats the given number of milliseconds as HH:MM:SS.sss
*
* @param msecs
* number of milliseconds
* @return String of the form HH:MM:SS.sss representing the given milliseconds
*/
public static String formatDuration(long msecs) {
StringBuilder buf = new StringBuilder();
long hours = msecs / (60 * 60 * 1000);
msecs %= (60 * 60 * 1000);
long mins = msecs / (60 * 1000);
msecs %= (60 * 1000);
long secs = msecs / 1000;
msecs %= 1000;
if (hours > 0) {
buf.append(hours);
buf.append(':');
}
if ((mins > 0) || (hours > 0)) {
if (mins < 10) {
buf.append('0');
}
buf.append(mins);
buf.append(':');
}
if ((secs < 10) && ((mins > 0) || (hours > 0))) {
buf.append('0');
}
buf.append(secs);
buf.append('.');
if (msecs < 100) {
buf.append('0');
}
if (msecs < 10) {
buf.append('0');
}
buf.append(msecs);
return buf.toString();
}
public void validate() throws CruiseControlException {
ValidationHelper.assertIsSet(dstFileName, "file", this.getClass());
CurrentBuildFileWriter.validate(dstFileName);
if (sourceFile != null) {
ValidationHelper.assertTrue(sourceFile.exists(), "'sourceFile' does not exist: "
+ sourceFile.getAbsolutePath());
ValidationHelper.assertTrue(sourceFile.isFile(), "'sourceFile' must be a file: "
+ sourceFile.getAbsolutePath());
sourceText = IO.readLines(sourceFile);
}
}
public void setFile(String fileName) {
this.dstFileName = fileName.trim();
LOG.debug("set fileName = " + fileName);
}
public void setSourceFile(String fileName) {
sourceFile = new File(fileName);
LOG.debug("set sourceFile = " + fileName);
}
}