/* * The MIT License * * Copyright (c) 2016 Steven G. Brown * * Permission is hereby granted, free of charge, to any person obtaining a copy * of this software and associated documentation files (the "Software"), to deal * in the Software without restriction, including without limitation the rights * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell * copies of the Software, and to permit persons to whom the Software is * furnished to do so, subject to the following conditions: * * The above copyright notice and this permission notice shall be included in * all copies or substantial portions of the Software. * * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN * THE SOFTWARE. */ package hudson.plugins.timestamper.action; import static com.google.common.base.Preconditions.checkNotNull; import java.io.BufferedReader; import java.io.IOException; import java.io.Reader; import java.io.StringReader; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.List; import java.util.TimeZone; import org.apache.commons.lang.time.DurationFormatUtils; import com.google.common.base.Function; import com.google.common.base.Joiner; import com.google.common.base.Optional; import hudson.model.Run; import hudson.plugins.timestamper.Timestamp; import hudson.plugins.timestamper.io.LogFileReader; import hudson.plugins.timestamper.io.LogFileReader.Line; import hudson.plugins.timestamper.io.TimestampsReader; /** * Generate a page of time-stamps on behalf of {@link TimestampsAction}. * <p> * Each line contains time-stamps for the equivalent line in the console log, * and optionally includes the console log text. * <p> * By default, the elapsed time will be displayed in seconds, with three places * after the decimal point. The output can be configured by providing the * following query parameters. The output can include multiple time-stamp * formats by providing multiple parameters. * <ul> * <li>"precision": Display the elapsed time in seconds, with a certain number * of places after the decimal point. Accepts a number of decimal places or * values such as "seconds" and "milliseconds".</li> * <li>"time": Display the system clock time. Accepts the * {@link SimpleDateFormat} format.</li> * <li>"timeZone": Time zone used when displaying the system clock time. Accepts * the {@link TimeZone} ID format.</li> * <li>"elapsed": Display the elapsed time since the start of the build. Accepts * the {@link DurationFormatUtils} format.</li> * <li>"appendLog": Display the console log line after the time-stamp.</li> * <li>"startLine": Display the time-stamps starting from a certain line. * Accepts a positive integer to start at that line, or a negative integer to * start that many lines back from the end.</li> * <li>"endLine": Display the time-stamps ending at a certain line. Accepts a * positive integer to finish at that line, or a negative integer to finish that * many lines back from the end.</li> * <li>"locale": Select the locale to use when displaying the system clock time. * </li> * <li>"currentTime": Display the current time instead of reading time-stamps * from the build. * <li> * </ul> * * @author Steven G. Brown */ public class TimestampsActionOutput { /** * Open a reader which provides the page of time-stamps. * * @param build * @param query * @return a {@link BufferedReader} */ public static BufferedReader open(Run<?, ?> build, TimestampsActionQuery query) { TimestampsReader timestampsReader = new TimestampsReader(build); LogFileReader logFileReader = new LogFileReader(build); long buildStartTime = build.getStartTimeInMillis(); long millisSinceEpoch = System.currentTimeMillis(); Timestamp currentTimestamp = new Timestamp(millisSinceEpoch - buildStartTime, millisSinceEpoch); return open(timestampsReader, logFileReader, query, currentTimestamp); } static BufferedReader open(final TimestampsReader timestampsReader, final LogFileReader logFileReader, final TimestampsActionQuery query, Timestamp currentTimestamp) { if (query.currentTime) { List<String> parts = new ArrayList<String>(); for (Function<Timestamp, String> format : query.timestampFormats) { parts.add(format.apply(currentTimestamp)); } String result = Joiner.on(' ').join(parts) + "\n"; return new BufferedReader(new StringReader(result)); } final StringBuilder buffer = new StringBuilder(); Reader reader = new Reader() { int linesRead; Optional<Integer> endLine = Optional.absent(); boolean started; @Override public int read(char[] cbuf, int off, int len) throws IOException { if (!started) { LineCountSupplier lineCount = new LineCountSupplier(logFileReader); linesRead = readToStartLine(query, lineCount); endLine = resolveEndLine(query, lineCount); started = true; } while (buffer.length() < len) { Optional<String> nextLine = readNextLine(query); if (!nextLine.isPresent()) { break; } linesRead++; if (endLine.isPresent() && linesRead > endLine.get()) { break; } buffer.append(nextLine.get()); buffer.append("\n"); } int numRead = new StringReader(buffer.toString()).read(cbuf, off, len); buffer.delete(0, (numRead >= 0 ? numRead : buffer.length())); return numRead; } private int readToStartLine(TimestampsActionQuery query, LineCountSupplier lineCount) throws IOException { int linesToSkip = Math.max(query.startLine - 1, 0); if (query.startLine < 0) { linesToSkip = lineCount.get() + query.startLine; } for (int line = 0; line < linesToSkip; line++) { timestampsReader.read(); logFileReader.nextLine(); } return linesToSkip; } private Optional<Integer> resolveEndLine(TimestampsActionQuery query, LineCountSupplier lineCount) throws IOException { if (query.endLine.isPresent() && query.endLine.get() < 0) { return Optional.of(lineCount.get() + query.endLine.get() + 1); } return query.endLine; } private Optional<String> readNextLine(TimestampsActionQuery query) throws IOException { Optional<Timestamp> timestamp = timestampsReader.read(); Optional<Line> logFileLine = logFileReader.nextLine(); if (logFileLine.isPresent() && !timestamp.isPresent()) { timestamp = logFileLine.get().readTimestamp(); } String result = ""; if (timestamp.isPresent()) { List<String> parts = new ArrayList<String>(); for (Function<Timestamp, String> format : query.timestampFormats) { parts.add(format.apply(timestamp.get())); } result = Joiner.on(' ').join(parts); } if (query.appendLogLine) { result += " "; if (logFileLine.isPresent()) { result += logFileLine.get().getText(); } } if (result.trim().isEmpty()) { return Optional.absent(); } return Optional.of(result); } @Override public void close() throws IOException { timestampsReader.close(); logFileReader.close(); } }; return new BufferedReader(reader); } private static class LineCountSupplier { private final LogFileReader logFileReader; private Optional<Integer> lineCount = Optional.absent(); LineCountSupplier(LogFileReader logFileReader) { this.logFileReader = checkNotNull(logFileReader); } int get() throws IOException { if (!lineCount.isPresent()) { lineCount = Optional.of(logFileReader.lineCount()); } return lineCount.get(); } } private TimestampsActionOutput() { } }