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                //encode to quoted-printable
114                if (quotedPrintable) {
115                        if (charset == null) {
116                                charset = Charset.forName("UTF-8");
117                        }
118
119                        QuotedPrintableCodec codec = new QuotedPrintableCodec(charset.name());
120                        try {
121                                String str = new String(cbuf, off, len);
122                                String encoded = codec.encode(str);
123
124                                cbuf = encoded.toCharArray();
125                                off = 0;
126                                len = cbuf.length;
127                        } catch (EncoderException e) {
128                                //thrown if an unsupported charset is passed into the codec
129                                //this should never be thrown because we already know the charset is valid (Charset object is passed in)
130                                throw new RuntimeException(e);
131                        }
132                }
133
134                if (lineLength == null) {
135                        //if line folding is disabled, then write directly to the Writer
136                        writer.write(cbuf, off, len);
137                        return;
138                }
139
140                int effectiveLineLength = lineLength;
141                if (quotedPrintable) {
142                        //"=" must be appended onto each line
143                        effectiveLineLength -= 1;
144                }
145
146                int encodedCharPos = -1;
147                int start = off;
148                int end = off + len;
149                for (int i = start; i < end; i++) {
150                        char c = cbuf[i];
151
152                        //keep track of the quoted-printable characters to prevent them from being cut in two at a folding boundary
153                        if (encodedCharPos >= 0) {
154                                encodedCharPos++;
155                                if (encodedCharPos == 3) {
156                                        encodedCharPos = -1;
157                                }
158                        }
159
160                        if (c == '\n') {
161                                writer.write(cbuf, start, i - start + 1);
162                                curLineLength = 0;
163                                start = i + 1;
164                                continue;
165                        }
166
167                        if (c == '\r') {
168                                if (i == end - 1 || cbuf[i + 1] != '\n') {
169                                        writer.write(cbuf, start, i - start + 1);
170                                        curLineLength = 0;
171                                        start = i + 1;
172                                } else {
173                                        curLineLength++;
174                                }
175                                continue;
176                        }
177
178                        if (c == '=' && quotedPrintable) {
179                                encodedCharPos = 0;
180                        }
181
182                        if (curLineLength >= effectiveLineLength) {
183                                //if the last characters on the line are whitespace, then exceed the max line length in order to include the whitespace on the same line
184                                //otherwise it will be lost because it will merge with the padding on the next line
185                                if (Character.isWhitespace(c)) {
186                                        while (Character.isWhitespace(c) && i < end - 1) {
187                                                i++;
188                                                c = cbuf[i];
189                                        }
190                                        if (i >= end - 1) {
191                                                //the rest of the char array is whitespace, so leave the loop
192                                                break;
193                                        }
194                                }
195
196                                //if we are in the middle of a quoted-printable encoded char, then exceed the max line length in order to print out the rest of the char
197                                if (encodedCharPos > 0) {
198                                        i += 3 - encodedCharPos;
199                                        if (i >= end - 1) {
200                                                //the rest of the char array was an encoded char, so leave the loop
201                                                break;
202                                        }
203                                }
204
205                                writer.write(cbuf, start, i - start);
206                                if (quotedPrintable) {
207                                        writer.write('=');
208                                }
209                                writer.write(newline);
210                                writer.write(indent);
211                                curLineLength = indent.length() + 1;
212                                start = i;
213
214                                continue;
215                        }
216
217                        curLineLength++;
218                }
219
220                writer.write(cbuf, start, end - start);
221        }
222
223        /**
224         * Closes the writer.
225         */
226        @Override
227        public void close() throws IOException {
228                writer.close();
229        }
230
231        /**
232         * Flushes the writer.
233         */
234        @Override
235        public void flush() throws IOException {
236                writer.flush();
237        }
238
239        /**
240         * Gets the maximum length a line can be before it is folded (excluding the
241         * newline, defaults to 75).
242         * @return the line length or null if folding is disabled
243         */
244        public Integer getLineLength() {
245                return lineLength;
246        }
247
248        /**
249         * Sets the maximum length a line can be before it is folded (excluding the
250         * newline, defaults to 75).
251         * @param lineLength the line length or null to disable folding
252         * @throws IllegalArgumentException if the line length is less than or equal
253         * to zero
254         */
255        public void setLineLength(Integer lineLength) {
256                if (lineLength != null && lineLength <= 0) {
257                        throw new IllegalArgumentException("Line length must be greater than 0.");
258                }
259                this.lineLength = lineLength;
260        }
261
262        /**
263         * Gets the string that is prepended to each folded line (defaults to a
264         * single space character).
265         * @return the indent string
266         */
267        public String getIndent() {
268                return indent;
269        }
270
271        /**
272         * Sets the string that is prepended to each folded line (defaults to a
273         * single space character).
274         * @param indent the indent string
275         * @throws IllegalArgumentException if the length of the indent string is
276         * greater than the max line length
277         */
278        public void setIndent(String indent) {
279                if (lineLength != null && indent.length() >= lineLength) {
280                        throw new IllegalArgumentException("The length of the indent string must be less than the max line length.");
281                }
282                this.indent = indent;
283        }
284
285        /**
286         * Gets the newline sequence that is used to separate lines (defaults to
287         * CRLF).
288         * @return the newline sequence
289         */
290        public String getNewline() {
291                return newline;
292        }
293
294        /**
295         * Sets the newline sequence that is used to separate lines (defaults to
296         * CRLF).
297         * @param newline the newline sequence
298         */
299        public void setNewline(String newline) {
300                this.newline = newline;
301        }
302
303        /**
304         * Gets the wrapped {@link Writer} object.
305         * @return the wrapped writer
306         */
307        public Writer getWriter() {
308                return writer;
309        }
310
311        /**
312         * Gets the writer's character encoding.
313         * @return the writer's character encoding or null if undefined
314         */
315        public Charset getEncoding() {
316                if (!(writer instanceof OutputStreamWriter)) {
317                        return null;
318                }
319
320                OutputStreamWriter osw = (OutputStreamWriter) writer;
321                String charsetStr = osw.getEncoding();
322                return (charsetStr == null) ? null : Charset.forName(charsetStr);
323        }
324}