001package biweekly.io.text;
002
003import java.io.IOException;
004import java.io.OutputStreamWriter;
005import java.io.Writer;
006import java.nio.charset.Charset;
007
008import biweekly.util.org.apache.commons.codec.EncoderException;
009import biweekly.util.org.apache.commons.codec.net.QuotedPrintableCodec;
010
011/*
012 Copyright (c) 2013-2015, Michael Angstadt
013 All rights reserved.
014
015 Redistribution and use in source and binary forms, with or without
016 modification, are permitted provided that the following conditions are met: 
017
018 1. Redistributions of source code must retain the above copyright notice, this
019 list of conditions and the following disclaimer. 
020 2. Redistributions in binary form must reproduce the above copyright notice,
021 this list of conditions and the following disclaimer in the documentation
022 and/or other materials provided with the distribution. 
023
024 THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
025 ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
026 WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
027 DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
028 ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
029 (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
030 LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
031 ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
032 (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
033 SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
034 */
035
036/**
037 * Automatically folds lines as they are written.
038 * @author Michael Angstadt
039 */
040public class FoldedLineWriter extends Writer {
041        private final Writer writer;
042        private int curLineLength = 0;
043        private Integer lineLength = 75;
044        private String indent = " ";
045        private String newline = "\r\n";
046
047        /**
048         * Creates a folded line writer.
049         * @param writer the writer object to wrap
050         */
051        public FoldedLineWriter(Writer writer) {
052                this.writer = writer;
053        }
054
055        /**
056         * Writes a string, followed by a newline.
057         * @param str the text to write
058         * @throws IOException if there's a problem writing to the output stream
059         */
060        public void writeln(String str) throws IOException {
061                write(str);
062                write(newline);
063        }
064
065        /**
066         * Writes a string.
067         * @param str the string to write
068         * @param quotedPrintable true to encode the string in quoted-printable
069         * encoding, false not to
070         * @param charset the character set to use when encoding into
071         * quoted-printable, or null to use the writer's character encoding (only
072         * applicable if "quotedPrintable" is set to true)
073         * @return this
074         * @throws IOException if there's a problem writing to the output stream
075         */
076        public FoldedLineWriter append(CharSequence str, boolean quotedPrintable, Charset charset) throws IOException {
077                write(str, quotedPrintable, charset);
078                return this;
079        }
080
081        /**
082         * Writes a string.
083         * @param str the string to write
084         * @param quotedPrintable true to encode the string in quoted-printable
085         * encoding, false not to
086         * @param charset the character set to use when encoding into
087         * quoted-printable, or null to use the writer's character encoding (only
088         * applicable if "quotedPrintable" is set to true)
089         * @throws IOException if there's a problem writing to the output stream
090         */
091        public void write(CharSequence str, boolean quotedPrintable, Charset charset) throws IOException {
092                write(str.toString().toCharArray(), 0, str.length(), quotedPrintable, charset);
093        }
094
095        @Override
096        public void write(char[] cbuf, int off, int len) throws IOException {
097                write(cbuf, off, len, false, null);
098        }
099
100        /**
101         * Writes a portion of an array of characters.
102         * @param cbuf the array of characters
103         * @param off the offset from which to start writing characters
104         * @param len the number of characters to write
105         * @param quotedPrintable true to encode the string in quoted-printable
106         * encoding, false not to
107         * @param charset the character set to use when encoding into
108         * quoted-printable, or null to use the writer's character encoding (only
109         * applicable if "quotedPrintable" is set to true)
110         * @throws IOException if there's a problem writing to the output stream
111         */
112        public void write(char[] cbuf, int off, int len, boolean quotedPrintable, Charset charset) throws IOException {
113                if (quotedPrintable) {
114                        if (charset == null) {
115                                charset = Charset.forName("UTF-8");
116                        }
117
118                        QuotedPrintableCodec codec = new QuotedPrintableCodec(charset.name());
119                        try {
120                                String str = new String(cbuf, off, len);
121                                String encoded = codec.encode(str);
122
123                                cbuf = encoded.toCharArray();
124                                off = 0;
125                                len = cbuf.length;
126                        } catch (EncoderException e) {
127                                /*
128                                 * Thrown if an unsupported charset is passed into the codec.
129                                 * This should never be thrown though, because we already know
130                                 * the charset is valid (a Charset object is passed into the
131                                 * method).
132                                 */
133                                throw new RuntimeException(e);
134                        }
135                }
136
137                if (lineLength == null) {
138                        /*
139                         * If line folding is disabled, then write directly to the Writer.
140                         */
141                        writer.write(cbuf, off, len);
142                        return;
143                }
144
145                int effectiveLineLength = lineLength;
146                if (quotedPrintable) {
147                        /*
148                         * Account for the "=" character that must be appended onto each
149                         * line.
150                         */
151                        effectiveLineLength -= 1;
152                }
153
154                int encodedCharPos = -1;
155                int start = off;
156                int end = off + len;
157                for (int i = start; i < end; i++) {
158                        char c = cbuf[i];
159
160                        /*
161                         * Keep track of the quoted-printable characters to prevent them
162                         * from being cut in two at a folding boundary.
163                         */
164                        if (encodedCharPos >= 0) {
165                                encodedCharPos++;
166                                if (encodedCharPos == 3) {
167                                        encodedCharPos = -1;
168                                }
169                        }
170
171                        if (c == '\n') {
172                                writer.write(cbuf, start, i - start + 1);
173                                curLineLength = 0;
174                                start = i + 1;
175                                continue;
176                        }
177
178                        if (c == '\r') {
179                                if (i == end - 1 || cbuf[i + 1] != '\n') {
180                                        writer.write(cbuf, start, i - start + 1);
181                                        curLineLength = 0;
182                                        start = i + 1;
183                                } else {
184                                        curLineLength++;
185                                }
186                                continue;
187                        }
188
189                        if (c == '=' && quotedPrintable) {
190                                encodedCharPos = 0;
191                        }
192
193                        if (curLineLength >= effectiveLineLength) {
194                                /*
195                                 * If the last characters on the line are whitespace, then
196                                 * exceed the max line length in order to include the whitespace
197                                 * on the same line. Otherwise, the whitespace will be lost
198                                 * because it will merge with the padding on the next, folded
199                                 * line.
200                                 */
201                                if (Character.isWhitespace(c)) {
202                                        while (Character.isWhitespace(c) && i < end - 1) {
203                                                i++;
204                                                c = cbuf[i];
205                                        }
206                                        if (i >= end - 1) {
207                                                /*
208                                                 * The rest of the char array is whitespace, so leave
209                                                 * the loop.
210                                                 */
211                                                break;
212                                        }
213                                }
214
215                                /*
216                                 * If we are in the middle of a quoted-printable encoded
217                                 * character, then exceed the max line length so the sequence
218                                 * doesn't get split up across multiple lines.
219                                 */
220                                if (encodedCharPos > 0) {
221                                        i += 3 - encodedCharPos;
222                                        if (i >= end - 1) {
223                                                /*
224                                                 * The rest of the char array was a quoted-printable
225                                                 * encoded char, so leave the loop.
226                                                 */
227                                                break;
228                                        }
229                                }
230
231                                writer.write(cbuf, start, i - start);
232                                if (quotedPrintable) {
233                                        writer.write('=');
234                                }
235                                writer.write(newline);
236                                writer.write(indent);
237                                curLineLength = indent.length() + 1;
238                                start = i;
239
240                                continue;
241                        }
242
243                        curLineLength++;
244                }
245
246                writer.write(cbuf, start, end - start);
247        }
248
249        /**
250         * Closes the writer.
251         */
252        @Override
253        public void close() throws IOException {
254                writer.close();
255        }
256
257        /**
258         * Flushes the writer.
259         */
260        @Override
261        public void flush() throws IOException {
262                writer.flush();
263        }
264
265        /**
266         * Gets the maximum length a line can be before it is folded (excluding the
267         * newline, defaults to 75).
268         * @return the line length or null if folding is disabled
269         */
270        public Integer getLineLength() {
271                return lineLength;
272        }
273
274        /**
275         * Sets the maximum length a line can be before it is folded (excluding the
276         * newline, defaults to 75).
277         * @param lineLength the line length or null to disable folding
278         * @throws IllegalArgumentException if the line length is less than or equal
279         * to zero
280         */
281        public void setLineLength(Integer lineLength) {
282                if (lineLength != null && lineLength <= 0) {
283                        throw new IllegalArgumentException("Line length must be greater than 0.");
284                }
285                this.lineLength = lineLength;
286        }
287
288        /**
289         * Gets the string that is prepended to each folded line (defaults to a
290         * single space character).
291         * @return the indent string
292         */
293        public String getIndent() {
294                return indent;
295        }
296
297        /**
298         * Sets the string that is prepended to each folded line (defaults to a
299         * single space character).
300         * @param indent the indent string
301         * @throws IllegalArgumentException if the length of the indent string is
302         * greater than the max line length
303         */
304        public void setIndent(String indent) {
305                if (lineLength != null && indent.length() >= lineLength) {
306                        throw new IllegalArgumentException("The length of the indent string must be less than the max line length.");
307                }
308                this.indent = indent;
309        }
310
311        /**
312         * Gets the newline sequence that is used to separate lines (defaults to
313         * CRLF).
314         * @return the newline sequence
315         */
316        public String getNewline() {
317                return newline;
318        }
319
320        /**
321         * Sets the newline sequence that is used to separate lines (defaults to
322         * CRLF).
323         * @param newline the newline sequence
324         */
325        public void setNewline(String newline) {
326                this.newline = newline;
327        }
328
329        /**
330         * Gets the wrapped {@link Writer} object.
331         * @return the wrapped writer
332         */
333        public Writer getWriter() {
334                return writer;
335        }
336
337        /**
338         * Gets the writer's character encoding.
339         * @return the writer's character encoding or null if undefined
340         */
341        public Charset getEncoding() {
342                if (!(writer instanceof OutputStreamWriter)) {
343                        return null;
344                }
345
346                OutputStreamWriter osw = (OutputStreamWriter) writer;
347                String charsetStr = osw.getEncoding();
348                return (charsetStr == null) ? null : Charset.forName(charsetStr);
349        }
350}