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}