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