1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one
3 * or more contributor license agreements. See the NOTICE file
4 * distributed with this work for additional information
5 * regarding copyright ownership. The ASF licenses this file
6 * to you under the Apache License, Version 2.0 (the
7 * "License"); you may not use this file except in compliance
8 * with the License. You may obtain a copy of the License at
9 *
10 * https://www.apache.org/licenses/LICENSE-2.0
11 *
12 * Unless required by applicable law or agreed to in writing,
13 * software distributed under the License is distributed on an
14 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 * KIND, either express or implied. See the License for the
16 * specific language governing permissions and limitations
17 * under the License.
18 */
19
20 package org.apache.commons.exec;
21
22 import java.io.ByteArrayOutputStream;
23 import java.io.IOException;
24 import java.io.OutputStream;
25 import java.nio.charset.Charset;
26
27 /**
28 * 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
29 * line and passes the complete line to a user-defined implementation.
30 */
31 public abstract class LogOutputStream extends OutputStream {
32
33 private static final class ByteArrayOutputStreamX extends ByteArrayOutputStream {
34 private ByteArrayOutputStreamX(final int size) {
35 super(size);
36 }
37
38 public synchronized String toString(final Charset charset) {
39 return new String(buf, 0, count, charset);
40 }
41 }
42
43 /** Initial buffer size. */
44 private static final int INITIAL_SIZE = 132;
45
46 /** Carriage return. */
47 private static final int CR = 0x0d;
48
49 /** Line-feed. */
50 private static final int LF = 0x0a;
51
52 /** The internal buffer. */
53 private final ByteArrayOutputStreamX buffer = new ByteArrayOutputStreamX(INITIAL_SIZE);
54
55 /**
56 * Last written char was a CR.
57 */
58 private boolean skip;
59
60 /**
61 * Level used to log data written to this stream.
62 */
63 private final int level;
64
65 /**
66 * Character Set to use when processing lines.
67 */
68 private final Charset charset;
69
70 /**
71 * Creates a new instance of this class. Uses the default level of 999.
72 */
73 public LogOutputStream() {
74 this(999);
75 }
76
77 /**
78 * Creates a new instance of this class.
79 *
80 * @param level level used to log data written to this stream.
81 */
82 public LogOutputStream(final int level) {
83 this(level, null);
84 }
85
86 /**
87 * Creates a new instance of this class, specifying the character set that should be used for outputting the string for each line
88 *
89 * @param level level used to log data written to this stream.
90 * @param charset Character Set to use when processing lines.
91 */
92 public LogOutputStream(final int level, final Charset charset) {
93 this.level = level;
94 this.charset = charset == null ? Charset.defaultCharset() : charset;
95 }
96
97 /**
98 * Writes all remaining data from the buffer.
99 *
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 }