1/*
2 * Copyright (C) 2015 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package com.android.internal.util;
18
19import java.io.PrintWriter;
20import java.io.Writer;
21import java.util.Arrays;
22
23/**
24 * A writer that breaks up its output into chunks before writing to its out writer,
25 * and which is linebreak aware, i.e., chunks will created along line breaks, if
26 * possible.
27 *
28 * Note: this class is not thread-safe.
29 */
30public class LineBreakBufferedWriter extends PrintWriter {
31
32    /**
33     * A buffer to collect data until the buffer size is reached.
34     *
35     * Note: we manage a char[] ourselves to avoid an allocation when printing to the
36     *       out writer. Otherwise a StringBuilder would have been simpler to use.
37     */
38    private char[] buffer;
39
40    /**
41     * The index of the first free element in the buffer.
42     */
43    private int bufferIndex;
44
45    /**
46     * The chunk size (=maximum buffer size) to use for this writer.
47     */
48    private final int bufferSize;
49
50
51    /**
52     * Index of the last newline character discovered in the buffer. The writer will try
53     * to split there.
54     */
55    private int lastNewline = -1;
56
57    /**
58     * The line separator for println().
59     */
60    private final String lineSeparator;
61
62    /**
63     * Create a new linebreak-aware buffered writer with the given output and buffer
64     * size. The initial capacity will be a default value.
65     * @param out The writer to write to.
66     * @param bufferSize The maximum buffer size.
67     */
68    public LineBreakBufferedWriter(Writer out, int bufferSize) {
69        this(out, bufferSize, 16);  // 16 is the default size of a StringBuilder buffer.
70    }
71
72    /**
73     * Create a new linebreak-aware buffered writer with the given output, buffer
74     * size and initial capacity.
75     * @param out The writer to write to.
76     * @param bufferSize The maximum buffer size.
77     * @param initialCapacity The initial capacity of the internal buffer.
78     */
79    public LineBreakBufferedWriter(Writer out, int bufferSize, int initialCapacity) {
80        super(out);
81        this.buffer = new char[Math.min(initialCapacity, bufferSize)];
82        this.bufferIndex = 0;
83        this.bufferSize = bufferSize;
84        this.lineSeparator = System.getProperty("line.separator");
85    }
86
87    /**
88     * Flush the current buffer. This will ignore line breaks.
89     */
90    @Override
91    public void flush() {
92        writeBuffer(bufferIndex);
93        bufferIndex = 0;
94        super.flush();
95    }
96
97    @Override
98    public void write(int c) {
99        if (bufferIndex < buffer.length) {
100            buffer[bufferIndex] = (char)c;
101            bufferIndex++;
102            if ((char)c == '\n') {
103                lastNewline = bufferIndex;
104            }
105        } else {
106            // This should be an uncommon case, we mostly expect char[] and String. So
107            // let the chunking be handled by the char[] case.
108            write(new char[] { (char)c }, 0 ,1);
109        }
110    }
111
112    @Override
113    public void println() {
114        write(lineSeparator);
115    }
116
117    @Override
118    public void write(char[] buf, int off, int len) {
119        while (bufferIndex + len > bufferSize) {
120            // Find the next newline in the buffer, see if that's below the limit.
121            // Repeat.
122            int nextNewLine = -1;
123            int maxLength = bufferSize - bufferIndex;
124            for (int i = 0; i < maxLength; i++) {
125                if (buf[off + i] == '\n') {
126                    if (bufferIndex + i < bufferSize) {
127                        nextNewLine = i;
128                    } else {
129                        break;
130                    }
131                }
132            }
133
134            if (nextNewLine != -1) {
135                // We can add some more data.
136                appendToBuffer(buf, off, nextNewLine);
137                writeBuffer(bufferIndex);
138                bufferIndex = 0;
139                lastNewline = -1;
140                off += nextNewLine + 1;
141                len -= nextNewLine + 1;
142            } else if (lastNewline != -1) {
143                // Use the last newline.
144                writeBuffer(lastNewline);
145                removeFromBuffer(lastNewline + 1);
146                lastNewline = -1;
147            } else {
148                // OK, there was no newline, break at a full buffer.
149                int rest = bufferSize - bufferIndex;
150                appendToBuffer(buf, off, rest);
151                writeBuffer(bufferIndex);
152                bufferIndex = 0;
153                off += rest;
154                len -= rest;
155            }
156        }
157
158        // Add to the buffer, this will fit.
159        if (len > 0) {
160            // Add the chars, find the last newline.
161            appendToBuffer(buf, off, len);
162            for (int i = len - 1; i >= 0; i--) {
163                if (buf[off + i] == '\n') {
164                    lastNewline = bufferIndex - len + i;
165                    break;
166                }
167            }
168        }
169    }
170
171    @Override
172    public void write(String s, int off, int len) {
173        while (bufferIndex + len > bufferSize) {
174            // Find the next newline in the buffer, see if that's below the limit.
175            // Repeat.
176            int nextNewLine = -1;
177            int maxLength = bufferSize - bufferIndex;
178            for (int i = 0; i < maxLength; i++) {
179                if (s.charAt(off + i) == '\n') {
180                    if (bufferIndex + i < bufferSize) {
181                        nextNewLine = i;
182                    } else {
183                        break;
184                    }
185                }
186            }
187
188            if (nextNewLine != -1) {
189                // We can add some more data.
190                appendToBuffer(s, off, nextNewLine);
191                writeBuffer(bufferIndex);
192                bufferIndex = 0;
193                lastNewline = -1;
194                off += nextNewLine + 1;
195                len -= nextNewLine + 1;
196            } else if (lastNewline != -1) {
197                // Use the last newline.
198                writeBuffer(lastNewline);
199                removeFromBuffer(lastNewline + 1);
200                lastNewline = -1;
201            } else {
202                // OK, there was no newline, break at a full buffer.
203                int rest = bufferSize - bufferIndex;
204                appendToBuffer(s, off, rest);
205                writeBuffer(bufferIndex);
206                bufferIndex = 0;
207                off += rest;
208                len -= rest;
209            }
210        }
211
212        // Add to the buffer, this will fit.
213        if (len > 0) {
214            // Add the chars, find the last newline.
215            appendToBuffer(s, off, len);
216            for (int i = len - 1; i >= 0; i--) {
217                if (s.charAt(off + i) == '\n') {
218                    lastNewline = bufferIndex - len + i;
219                    break;
220                }
221            }
222        }
223    }
224
225    /**
226     * Append the characters to the buffer. This will potentially resize the buffer,
227     * and move the index along.
228     * @param buf The char[] containing the data.
229     * @param off The start index to copy from.
230     * @param len The number of characters to copy.
231     */
232    private void appendToBuffer(char[] buf, int off, int len) {
233        if (bufferIndex + len > buffer.length) {
234            ensureCapacity(bufferIndex + len);
235        }
236        System.arraycopy(buf, off, buffer, bufferIndex, len);
237        bufferIndex += len;
238    }
239
240    /**
241     * Append the characters from the given string to the buffer. This will potentially
242     * resize the buffer, and move the index along.
243     * @param s The string supplying the characters.
244     * @param off The start index to copy from.
245     * @param len The number of characters to copy.
246     */
247    private void appendToBuffer(String s, int off, int len) {
248        if (bufferIndex + len > buffer.length) {
249            ensureCapacity(bufferIndex + len);
250        }
251        s.getChars(off, off + len, buffer, bufferIndex);
252        bufferIndex += len;
253    }
254
255    /**
256     * Resize the buffer. We use the usual double-the-size plus constant scheme for
257     * amortized O(1) insert. Note: we expect small buffers, so this won't check for
258     * overflow.
259     * @param capacity The size to be ensured.
260     */
261    private void ensureCapacity(int capacity) {
262        int newSize = buffer.length * 2 + 2;
263        if (newSize < capacity) {
264            newSize = capacity;
265        }
266        buffer = Arrays.copyOf(buffer, newSize);
267    }
268
269    /**
270     * Remove the characters up to (and excluding) index i from the buffer. This will
271     * not resize the buffer, but will update bufferIndex.
272     * @param i The number of characters to remove from the front.
273     */
274    private void removeFromBuffer(int i) {
275        int rest = bufferIndex - i;
276        if (rest > 0) {
277            System.arraycopy(buffer, bufferIndex - rest, buffer, 0, rest);
278            bufferIndex = rest;
279        } else {
280            bufferIndex = 0;
281        }
282    }
283
284    /**
285     * Helper method, write the given part of the buffer, [start,length), to the output.
286     * @param length The number of characters to flush.
287     */
288    private void writeBuffer(int length) {
289        if (length > 0) {
290            super.write(buffer, 0, length);
291        }
292    }
293}
294