/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2001-2003, 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;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FilenameFilter;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import net.sourceforge.cruisecontrol.gendoc.annotations.SkipDoc;
import net.sourceforge.cruisecontrol.util.BuildOutputLogger;
import net.sourceforge.cruisecontrol.util.DateUtil;
import net.sourceforge.cruisecontrol.util.IO;
import net.sourceforge.cruisecontrol.util.Util;
import net.sourceforge.cruisecontrol.util.XMLLogHelper;
import org.apache.log4j.Logger;
import org.jdom.Content;
import org.jdom.Document;
import org.jdom.Element;
import org.jdom.output.Format;
import org.jdom.output.XMLOutputter;
/**
* Handles the Log element, and subelements, of the CruiseControl configuration file. Also represents the Build Log used
* by the CruiseControl build process.
*/
public class Log implements Serializable {
private static final long serialVersionUID = -5727569770074024691L;
private static final Logger LOG = Logger.getLogger(Log.class);
public static final int BEFORE_LENGTH = "logYYYYMMDDhhmmssL".length();
private static final int AFTER_LENGTH = ".xml".length();
private transient String logDir;
private transient String logXmlEncoding;
private transient boolean isTrimWhitespace;
private transient Element buildLog;
private final transient List<BuildLogger> loggers = new ArrayList<BuildLogger>();
private final transient List<Manipulator> manipulators = new ArrayList<Manipulator>();
private transient String projectName;
static final String MSG_PREFIX_INVALID_LABEL = "Invalid log label: ";
/**
* Log instances created this way must have their projectName set.
*/
public Log() {
reset();
}
/**
* Although this property is required, it is implicitly defined by the project and doesn't map to a config file
* attribute.
*
* @param projectName project name
* @throws IllegalArgumentException
* is projectName is null
*/
void setProjectName(final String projectName) {
if (projectName == null) {
throw new IllegalArgumentException("projectName can't be null");
}
this.projectName = projectName;
if (logDir == null) {
logDir = "logs" + File.separatorChar + projectName;
}
}
/**
* Validate the log. Also creates the log directory if it doesn't exist.
*
* @throws CruiseControlException
* if projectName wasn't set
*/
public void validate() throws CruiseControlException {
if (projectName == null) {
// Not using ValidationHelper because projectName should be set
// implictly by the project, not as an attribute.
throw new CruiseControlException("projectName must be set");
}
if (logDir != null) {
checkLogDirectory(logDir);
}
for (final BuildLogger logger : loggers) {
logger.validate();
}
for (final Manipulator manipulator : manipulators) {
manipulator.validate();
}
}
/**
* Adds a BuildLogger that will be called to manipulate the project log just prior to writing the log.
* @param logger a BuildLogger that will be called to manipulate the project log just prior to writing the log
*/
public void add(final BuildLogger logger) {
loggers.add(logger);
}
/**
* Adds a Manipulator that will handle old log-files
* @param manipulator a Manipulator that will handle old log-files
*/
public void add(final Manipulator manipulator) {
manipulators.add(manipulator);
}
public BuildLogger[] getLoggers() {
return loggers.toArray(new BuildLogger[loggers.size()]);
}
public String getLogXmlEncoding() {
return logXmlEncoding;
}
public String getProjectName() {
return projectName;
}
public void setDir(final String logDir) {
this.logDir = logDir;
}
public String getLogDir() {
return logDir;
}
public void setEncoding(final String logXmlEncoding) {
this.logXmlEncoding = logXmlEncoding;
}
/**
* @param trimWhitespace if true, trim whitespace from start and end of log lines.
* Primarily intended only to force historic "trim" behavior.
* Defaults to false.
*/
public void setTrimWhitespace(final boolean trimWhitespace) {
isTrimWhitespace = trimWhitespace;
}
/**
* creates log directory if it doesn't already exist
* @param logDir log directory to create if it doesn't already exist
* @throws CruiseControlException
* if directory can't be created or there is a file of the same name
*/
private void checkLogDirectory(final String logDir) throws CruiseControlException {
final File logDirectory = new File(logDir);
if (!logDirectory.exists()) {
LOG.info("log directory specified in config file does not exist; creating: "
+ logDirectory.getAbsolutePath());
if (!Util.doMkDirs(logDirectory)) {
throw new CruiseControlException("Can't create log directory specified in config file: "
+ logDirectory.getAbsolutePath());
}
} else if (!logDirectory.isDirectory()) {
throw new CruiseControlException("Log directory specified in config file is not a directory: "
+ logDirectory.getAbsolutePath());
}
}
/**
* Writes the current build log to the appropriate directory and filename.
* @param now current build date
* @throws CruiseControlException if nextLogger.log throws CruiseControlException
*/
public void writeLogFile(final Date now) throws CruiseControlException {
// Call the Loggers to let them do their thing
for (final BuildLogger nextLogger : loggers) {
// The buildloggers get the "real" build log, not a clone. Therefore,
// call getContent() wouldn't be appropriate here.
nextLogger.log(buildLog);
}
final String logFilename = decideLogfileName(now);
// Add the logDir as an info element
final Element logDirElement = new Element("property");
logDirElement.setAttribute("name", "logdir");
logDirElement.setAttribute("value", new File(logDir).getAbsolutePath());
buildLog.getChild("info").addContent(logDirElement);
// Add the logFile as an info element
final Element logFileElement = new Element("property");
logFileElement.setAttribute("name", "logfile");
logFileElement.setAttribute("value", logFilename);
buildLog.getChild("info").addContent(logFileElement);
final File logfile = new File(logDir, logFilename);
LOG.debug("Project " + projectName + ": Writing log file [" + logfile.getAbsolutePath() + "]");
writeLogFile(logfile, buildLog);
callManipulators();
}
protected void writeLogFile(final File file, final Element element) throws CruiseControlException {
// Write the log file out, let jdom care about the encoding by using
// an OutputStream instead of a Writer.
try {
final Format format = Format.getPrettyFormat();
if (logXmlEncoding != null) {
format.setEncoding(logXmlEncoding);
}
if (!isTrimWhitespace) {
format.setTextMode(Format.TextMode.TRIM_FULL_WHITE);
}
final XMLOutputter outputter = new XMLOutputter(format);
final OutputStream logStream = new BufferedOutputStream(new FileOutputStream(file));
try {
outputter.output(new Document(element), logStream);
} finally {
IO.close(logStream);
}
} catch (IOException e) {
throw new CruiseControlException(e);
}
}
private String decideLogfileName(final Date now) throws CruiseControlException {
final XMLLogHelper helper = new XMLLogHelper(buildLog);
if (helper.isBuildSuccessful()) {
return formatLogFileName(now, helper.getLabel());
}
return formatLogFileName(now);
}
/**
* Calls all Manipulators to already existing logfiles.
*/
protected void callManipulators() {
for (final Manipulator manipulator : manipulators) {
manipulator.execute(getLogDir());
}
}
public static String formatLogFileName(final Date date) {
return formatLogFileName(date, null);
}
public static String formatLogFileName(final Date date, final String label) {
final StringBuilder logFileName = new StringBuilder();
logFileName.append("log");
logFileName.append(DateUtil.getFormattedTime(date));
if (label != null) {
logFileName.append("L");
logFileName.append(label);
}
logFileName.append(".xml");
return logFileName.toString();
}
@SkipDoc
public void addContent(final Content newContent) {
buildLog.addContent(newContent);
}
public Element getContent() {
return (Element) buildLog.clone();
}
public boolean wasBuildSuccessful() {
return new XMLLogHelper(buildLog).isBuildSuccessful();
}
/**
* Resets the build log. After calling this method a fresh build log will exist, ready for adding new content.
*/
public void reset() {
this.buildLog = new Element("cruisecontrol");
}
/**
* @return a list with the names of the available log files, that is the list
*/
public List<String> getLogLabels() {
final List<String> labels = new ArrayList<String>();
final File dir = new File(logDir);
if (dir.isDirectory()) {
final FilenameFilter xmlLogFilter = new FilenameFilter() {
public boolean accept(File dir, String name) {
return name.startsWith("log") && name.endsWith(".xml");
}
};
final String[] xmlLogFiles = dir.list(xmlLogFilter);
labels.addAll(Arrays.asList(xmlLogFiles));
}
return labels;
}
boolean isExistingLogLabel(final String filename) {
final List<String> logLabels = getLogLabels();
return logLabels.contains(filename);
}
File getFileFromLabel(final String filename) {
if (!isExistingLogLabel(filename)) {
throw new IllegalArgumentException(MSG_PREFIX_INVALID_LABEL + filename);
}
return new File(logDir, filename);
}
public String[] getLogLabelLines(final String logLabel, final int firstLine) {
final File logFile = getFileFromLabel(logLabel);
// reuse file reader features of BuildOutputLogger
final BuildOutputLogger buildOutputLogger = new BuildOutputLogger(logFile);
return buildOutputLogger.retrieveLines(firstLine);
}
public static boolean wasSuccessfulBuild(final String filename) {
if (filename == null) {
return false;
}
final boolean startsWithLog = filename.startsWith("log");
final boolean hasLabelSeparator = filename.indexOf('L') == BEFORE_LENGTH - 1;
final boolean isXmlFile = filename.endsWith(".xml");
return startsWithLog && hasLabelSeparator && isXmlFile;
}
public static Date parseDateFromLogFileName(final String filename) throws CruiseControlException {
return DateUtil.parseFormattedTime(filename.substring(3, BEFORE_LENGTH - 1), "date from logfile name");
}
public static String parseLabelFromLogFileName(final String filename) {
if (!Log.wasSuccessfulBuild(filename)) {
return "";
}
return filename.substring(BEFORE_LENGTH, filename.length() - AFTER_LENGTH);
}
}