    001/*
    002 * Licensed to the Apache Software Foundation (ASF) under one
    003 * or more contributor license agreements.  See the NOTICE file
    004 * distributed with this work for additional information
    005 * regarding copyright ownership.  The ASF licenses this file
    006 * to you under the Apache License, Version 2.0 (the
    007 * "License"); you may not use this file except in compliance
    008 * with the License.  You may obtain a copy of the License at
    009 *
    010 *   https://www.apache.org/licenses/LICENSE-2.0
    011 *
    012 * Unless required by applicable law or agreed to in writing,
    013 * software distributed under the License is distributed on an
    014 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
    015 * KIND, either express or implied.  See the License for the
    016 * specific language governing permissions and limitations
    017 * under the License.
    018 */
    019
    020package org.apache.commons.exec;
    021
    022import java.io.ByteArrayOutputStream;
    023import java.io.IOException;
    024import java.io.OutputStream;
    025import java.nio.charset.Charset;
    026
    027/**
    028 * Base class to connect a logging system to the output and/or error stream of then external process. The implementation parses the incoming data to construct a
    029 * line and passes the complete line to a user-defined implementation.
    030 */
    031public abstract class LogOutputStream extends OutputStream {
    032
    033    private static final class ByteArrayOutputStreamX extends ByteArrayOutputStream {
    034        private ByteArrayOutputStreamX(final int size) {
    035            super(size);
    036        }
    037
    038        public synchronized String toString(final Charset charset) {
    039            return new String(buf, 0, count, charset);
    040        }
    041    }
    042
    043    /** Initial buffer size. */
    044    private static final int INITIAL_SIZE = 132;
    045
    046    /** Carriage return. */
    047    private static final int CR = 0x0d;
    048
    049    /** Line-feed. */
    050    private static final int LF = 0x0a;
    051
    052    /** The internal buffer. */
    053    private final ByteArrayOutputStreamX buffer = new ByteArrayOutputStreamX(INITIAL_SIZE);
    054
    055    /**
    056     * Last written char was a CR.
    057     */
    058    private boolean skip;
    059
    060    /**
    061     * Level used to log data written to this stream.
    062     */
    063    private final int level;
    064
    065    /**
    066     * Character Set to use when processing lines.
    067     */
    068    private final Charset charset;
    069
    070    /**
    071     * Creates a new instance of this class. Uses the default level of 999.
    072     */
    073    public LogOutputStream() {
    074        this(999);
    075    }
    076
    077    /**
    078     * Creates a new instance of this class.
    079     *
    080     * @param level level used to log data written to this stream.
    081     */
    082    public LogOutputStream(final int level) {
    083        this(level, null);
    084    }
    085
    086    /**
    087     * Creates a new instance of this class, specifying the character set that should be used for outputting the string for each line
    088     *
    089     * @param level   level used to log data written to this stream.
    090     * @param charset Character Set to use when processing lines.
    091     */
    092    public LogOutputStream(final int level, final Charset charset) {
    093        this.level = level;
    094        this.charset = charset == null ? Charset.defaultCharset() : charset;
    095    }
    096
    097    /**
    098     * Writes all remaining data from the buffer.
    099     *
    100     * @see OutputStream#close()
    101     */
    102    @Override
    103    public void close() throws IOException {
    104        if (buffer.size() > 0) {
    105            processBuffer();
    106        }
    107        super.close();
    108    }
    109
    110    /**
    111     * Flushes this log stream.
    112     *
    113     * @see OutputStream#flush()
    114     */
    115    @Override
    116    public void flush() {
    117        if (buffer.size() > 0) {
    118            processBuffer();
    119        }
    120    }
    121
    122    /**
    123     * Gets the trace level of the log system.
    124     *
    125     * @return the trace level of the log system.
    126     */
    127    public int getMessageLevel() {
    128        return level;
    129    }
    130
    131    /**
    132     * Converts the buffer to a string and sends it to {@code processLine}.
    133     */
    134    protected void processBuffer() {
    135        processLine(buffer.toString(charset));
    136        buffer.reset();
    137    }
    138
    139    /**
    140     * Logs a line to the log system of the user.
    141     *
    142     * @param line the line to log.
    143     */
    144    protected void processLine(final String line) {
    145        processLine(line, level);
    146    }
    147
    148    /**
    149     * Logs a line to the log system of the user.
    150     *
    151     * @param line     the line to log.
    152     * @param logLevel the log level to use
    153     */
    154    protected abstract void processLine(String line, int logLevel);
    155
    156    /**
    157     * Writes a block of characters to the output stream.
    158     *
    159     * @param b   the array containing the data.
    160     * @param off the offset into the array where data starts.
    161     * @param len the length of block.
    162     * @throws IOException if the data cannot be written into the stream.
    163     * @see OutputStream#write(byte[], int, int)
    164     */
    165    @Override
    166    public void write(final byte[] b, final int off, final int len) throws IOException {
    167        // find the line breaks and pass other chars through in blocks
    168        int offset = off;
    169        int blockStartOffset = offset;
    170        int remaining = len;
    171        while (remaining > 0) {
    172            while (remaining > 0 && b[offset] != LF && b[offset] != CR) {
    173                offset++;
    174                remaining--;
    175            }
    176            // either end of buffer or a line separator char
    177            final int blockLength = offset - blockStartOffset;
    178            if (blockLength > 0) {
    179                buffer.write(b, blockStartOffset, blockLength);
    180            }
    181            while (remaining > 0 && (b[offset] == LF || b[offset] == CR)) {
    182                write(b[offset]);
    183                offset++;
    184                remaining--;
    185            }
    186            blockStartOffset = offset;
    187        }
    188    }
    189
    190    /**
    191     * Writes the data to the buffer and flush the buffer, if a line separator is detected.
    192     *
    193     * @param cc data to log (byte).
    194     * @see OutputStream#write(int)
    195     */
    196    @Override
    197    public void write(final int cc) throws IOException {
    198        final byte c = (byte) cc;
    199        if (c == '\n' || c == '\r') {
    200            if (!skip) {
    201                processBuffer();
    202            }
    203        } else {
    204            buffer.write(cc);
    205        }
    206        skip = c == '\r';
    207    }
    208}