/********************************************************************************
* CruiseControl, a Continuous Integration Toolkit
* Copyright (c) 2007, 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.util;
import net.sourceforge.cruisecontrol.LiveOutputReader;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.PrintStream;
import java.io.FileOutputStream;
import java.io.FileNotFoundException;
import java.io.Serializable;
import java.util.List;
import java.util.ArrayList;
/**
* Log all consumed lines to a file, and also provide methods to read lines from that file.
* Can be used to log all sysout and syserr to a file.
*/
public class BuildOutputLogger implements StreamConsumer, LiveOutputReader, Serializable {
private static final long serialVersionUID = -1594678930828433470L;
public static final int MAX_LINES = 1000;
private final File data;
/** A unique (for this VM) identifying string for this logger instance. */
private String id;
/** Counter used to change the id after data reset. */
private long resetCount;
public BuildOutputLogger(File outputFile) {
data = outputFile;
// use parent hashCode(), as this class overrides and is not unique per instance.
id = "" + super.hashCode();
}
public void clear() {
if (noDataFile()) { return; }
data.delete();
// reset ID after data file is cleared.
// Allows clients to read from beginning if readUptoMaxLines() was called before a reset.
id += "__" + resetCount++;
}
public synchronized void consumeLine(final String line) {
if (data == null) { throw new RuntimeException("No log file specified"); }
try {
final PrintStream out = new PrintStream(new FileOutputStream(data, true));
try {
out.println(line);
} finally {
out.close();
}
} catch (FileNotFoundException e) {
e.printStackTrace();
}
}
/**
* @return A unique (for this VM) identifying string for this logger instance.
* This is intended to allow reporting apps (eg: Dashboard) to check if the logger instance changes mid-build,
* causing the "live output" log file to reset (possibly due to a CompositeBuilder moving to a new Builder, etc.).
* If the logger instance changes (indicated by a new ID value), the client should start asking for output from
* the first line of the current output file.
* @see #retrieveLines(int)
*/
public String getID() { return id; }
/**
* @param firstLine line to skip to.
* @return All lines available from firstLine (inclusive) up to MAX_LINES.
* Before the first call to retrieveLines(), the client should call {@link #getID()}, and hold that id value.
* If a client later calls retrieveLines() with a non-zero 'firstLine' parameter, and receives an empty array
* as a result, that client should also call {@link #getID()}. If {@link #getID ()} returns a different value
* from the prior call to {@link #getID ()}, the client should make another call to retrieveLines() with the
* firstLine parameter set back to zero. This will allow the client to live output when the output logger is
* changed during a build.
*/
public String[] retrieveLines(final int firstLine) {
if (noDataFile()) { return new String[0]; }
final List<String> lines = loadFile(firstLine);
return lines.toArray(new String[lines.size()]);
}
/**
* @return true if a data output file has been specified.
*/
public boolean isDataFileSet() { return data != null; }
/**
* @param otherDataFile file to which to compare this logger's data file.
* @return true if the data file for this logger and the given file are the same.
*/
public boolean isDataFileEquals(final File otherDataFile) {
return dataEquals(data, otherDataFile);
}
private List<String> loadFile(final int firstLine) {
try {
final BufferedReader reader = new BufferedReader(new FileReader(data));
try {
skipLines(reader, firstLine);
return readUptoMaxLines(reader);
} finally {
reader.close();
}
} catch (IOException e) {
return new ArrayList<String>();
}
}
private List<String> readUptoMaxLines(BufferedReader reader) throws IOException {
List<String> result = new ArrayList<String>();
String line = reader.readLine();
while (line != null && result.size() < MAX_LINES) {
result.add(line);
line = reader.readLine();
}
return result;
}
private void skipLines(final BufferedReader inFile, final int numToSkip) throws IOException {
for (int i = 0; i < numToSkip; i++) { inFile.readLine(); }
}
private boolean noDataFile() {
return data == null || !data.exists();
}
public boolean equals(final Object other) {
if (this == other) { return true; }
if (other == null) { return false; }
if (this.getClass() != other.getClass()) { return false; }
return equals((BuildOutputLogger) other);
}
private boolean equals(final BuildOutputLogger other) {
return dataEquals(this.data, other.data);
}
private boolean dataEquals(final File mine, final File other) {
if (mine == null) { return other == null; }
final boolean pathSame = mine.getPath().equals(other.getPath());
final boolean nameSame = mine.getName().equals(other.getName());
return pathSame && nameSame;
}
public int hashCode() {
return (data != null ? data.hashCode() : 0);
}
public String toString() {
final String path = data == null ? "null" : (data.getAbsolutePath());
return "<BuildOutputLogger data=" + path + ">";
}
}