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 *      http://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}