001/*
002 * MIT License
003 * 
004 * Copyright (c) 2016 Michael Angstadt
005 * 
006 * Permission is hereby granted, free of charge, to any person obtaining a copy
007 * of this software and associated documentation files (the "Software"), to deal
008 * in the Software without restriction, including without limitation the rights
009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
010 * copies of the Software, and to permit persons to whom the Software is
011 * furnished to do so, subject to the following conditions:
012 * 
013 * The above copyright notice and this permission notice shall be included in
014 * all copies or substantial portions of the Software.
015 * 
016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
022 * SOFTWARE.
023 */
024
025package com.github.mangstadt.vinnie.io;
026
027import java.io.IOException;
028import java.io.Writer;
029import java.nio.charset.Charset;
030
031import com.github.mangstadt.vinnie.SyntaxStyle;
032import com.github.mangstadt.vinnie.codec.EncoderException;
033import com.github.mangstadt.vinnie.codec.QuotedPrintableCodec;
034
035/**
036 * Automatically folds lines as they are written.
037 * @author Michael Angstadt
038 */
039public class FoldedLineWriter extends Writer {
040        private static final String CRLF = "\r\n";
041        private final Writer writer;
042
043        private Integer lineLength = 75;
044        private String indent = " ";
045
046        private int curLineLength = 0;
047
048        /**
049         * Creates a folded line writer.
050         * @param writer the writer object to wrap
051         */
052        public FoldedLineWriter(Writer writer) {
053                this.writer = writer;
054        }
055
056        /**
057         * Writes a newline.
058         * @throws IOException if there's a problem writing to the output stream
059         */
060        public void writeln() throws IOException {
061                write(CRLF);
062        }
063
064        /**
065         * Writes a string.
066         * @param str the string to write
067         * @param quotedPrintable true to encode the string in quoted-printable
068         * encoding, false not to
069         * @param charset the character set to use when encoding the string into
070         * quoted-printable
071         * @throws IOException if there's a problem writing to the output stream
072         */
073        public void write(CharSequence str, boolean quotedPrintable, Charset charset) throws IOException {
074                write(str.toString().toCharArray(), 0, str.length(), quotedPrintable, charset);
075        }
076
077        /**
078         * Writes a portion of an array of characters.
079         * @param cbuf the array of characters
080         * @param off the offset from which to start writing characters
081         * @param len the number of characters to write
082         * @throws IOException if there's a problem writing to the output stream
083         */
084        @Override
085        public void write(char[] cbuf, int off, int len) throws IOException {
086                write(cbuf, off, len, false, null);
087        }
088
089        /**
090         * Writes a portion of an array of characters.
091         * @param cbuf the array of characters
092         * @param off the offset from which to start writing characters
093         * @param len the number of characters to write
094         * @param quotedPrintable true to encode the string in quoted-printable
095         * encoding, false not to
096         * @param charset the character set to use when encoding the string into
097         * quoted-printable
098         * @throws IOException if there's a problem writing to the output stream
099         */
100        public void write(char[] cbuf, int off, int len, boolean quotedPrintable, Charset charset) throws IOException {
101                if (quotedPrintable) {
102                        String str = new String(cbuf, off, len);
103                        QuotedPrintableCodec codec = new QuotedPrintableCodec(charset.name());
104
105                        String encoded;
106                        try {
107                                encoded = codec.encode(str);
108                        } catch (EncoderException e) {
109                                /*
110                                 * Thrown if an unsupported charset is passed into the codec.
111                                 * This should never happen because we already know the charset
112                                 * is valid (a Charset object is passed into the method).
113                                 */
114                                throw new IOException(e);
115                        }
116
117                        cbuf = encoded.toCharArray();
118                        off = 0;
119                        len = cbuf.length;
120                }
121
122                if (lineLength == null) {
123                        /*
124                         * If line folding is disabled, then write directly to the Writer.
125                         */
126                        writer.write(cbuf, off, len);
127                        return;
128                }
129
130                int effectiveLineLength = lineLength;
131                if (quotedPrintable) {
132                        /*
133                         * Account for the "=" character that must be appended onto each
134                         * line.
135                         */
136                        effectiveLineLength -= 1;
137                }
138
139                int encodedCharPos = -1;
140                int start = off;
141                int end = off + len;
142                for (int i = start; i < end; i++) {
143                        char c = cbuf[i];
144
145                        /*
146                         * Keep track of the quoted-printable characters to prevent them
147                         * from being cut in two at a folding boundary.
148                         */
149                        if (encodedCharPos >= 0) {
150                                encodedCharPos++;
151                                if (encodedCharPos == 3) {
152                                        encodedCharPos = -1;
153                                }
154                        }
155
156                        if (c == '\n') {
157                                writer.write(cbuf, start, i - start + 1);
158                                curLineLength = 0;
159                                start = i + 1;
160                                continue;
161                        }
162
163                        if (c == '\r') {
164                                if (i == end - 1 || cbuf[i + 1] != '\n') {
165                                        writer.write(cbuf, start, i - start + 1);
166                                        curLineLength = 0;
167                                        start = i + 1;
168                                } else {
169                                        curLineLength++;
170                                }
171                                continue;
172                        }
173
174                        if (c == '=' && quotedPrintable) {
175                                encodedCharPos = 0;
176                        }
177
178                        if (curLineLength >= effectiveLineLength) {
179                                /*
180                                 * If the last characters on the line are whitespace, then
181                                 * exceed the max line length in order to include the whitespace
182                                 * on the same line.
183                                 * 
184                                 * This is to prevent the whitespace from merging with the
185                                 * folding whitespace of the following folded line and
186                                 * potentially being lost.
187                                 * 
188                                 * Old syntax style allows multiple whitespace characters to be
189                                 * used for folding, so it could get lost here. New syntax style
190                                 * only allows one character to be used.
191                                 */
192                                if (Character.isWhitespace(c)) {
193                                        while (Character.isWhitespace(c) && i < end - 1) {
194                                                i++;
195                                                c = cbuf[i];
196                                        }
197                                        if (i >= end - 1) {
198                                                /*
199                                                 * The rest of the char array is whitespace, so leave
200                                                 * the loop.
201                                                 */
202                                                break;
203                                        }
204                                }
205
206                                /*
207                                 * If we are in the middle of a quoted-printable encoded
208                                 * character, then exceed the max line length so the sequence
209                                 * doesn't get split up across multiple lines.
210                                 */
211                                if (encodedCharPos > 0) {
212                                        i += 3 - encodedCharPos;
213                                        if (i >= end - 1) {
214                                                /*
215                                                 * The rest of the char array was a quoted-printable
216                                                 * encoded char, so leave the loop.
217                                                 */
218                                                break;
219                                        }
220                                }
221
222                                /*
223                                 * If the last char is the low (second) char in a surrogate
224                                 * pair, don't split the pair across two lines.
225                                 */
226                                if (Character.isLowSurrogate(c)) {
227                                        i++;
228                                        if (i >= end - 1) {
229                                                /*
230                                                 * Surrogate pair finishes the char array, so leave the
231                                                 * loop.
232                                                 */
233                                                break;
234                                        }
235                                }
236
237                                writer.write(cbuf, start, i - start);
238                                if (quotedPrintable) {
239                                        writer.write('=');
240                                }
241                                writer.write(CRLF);
242
243                                /*
244                                 * Do not include indentation whitespace if the value is
245                                 * quoted-printable.
246                                 */
247                                curLineLength = 1;
248                                if (!quotedPrintable) {
249                                        writer.write(indent);
250                                        curLineLength += indent.length();
251                                }
252
253                                start = i;
254
255                                continue;
256                        }
257
258                        curLineLength++;
259                }
260
261                writer.write(cbuf, start, end - start);
262        }
263
264        /**
265         * Gets the maximum length a line can be before it is folded (excluding the
266         * newline, defaults to 75).
267         * @return the line length or null if folding is disabled
268         */
269        public Integer getLineLength() {
270                return lineLength;
271        }
272
273        /**
274         * Sets the maximum length a line can be before it is folded (excluding the
275         * newline, defaults to 75).
276         * @param lineLength the line length or null to disable folding
277         * @throws IllegalArgumentException if the line length is less than or equal
278         * to zero, or the line length is less than the length of the indent string
279         */
280        public void setLineLength(Integer lineLength) {
281                if (lineLength != null) {
282                        if (lineLength <= 0) {
283                                throw new IllegalArgumentException("Line length must be greater than 0.");
284                        }
285                        if (lineLength <= indent.length()) {
286                                throw new IllegalArgumentException("Line length must be greater than indent string length.");
287                        }
288                }
289
290                this.lineLength = lineLength;
291        }
292
293        /**
294         * Gets the string that is prepended to each folded line (defaults to a
295         * single space character).
296         * @return the indent string
297         */
298        public String getIndent() {
299                return indent;
300        }
301
302        /**
303         * Sets the string that is prepended to each folded line (defaults to a
304         * single space character).
305         * @param indent the indent string (cannot be empty, may only contain tabs
306         * and spaces). Note that data streams using {@link SyntaxStyle#NEW} syntax
307         * MUST use an indent string that contains EXACTLY ONE character.
308         * @throws IllegalArgumentException if the indent string is empty, or the
309         * length of the indent string is greater than the max line length, or the
310         * indent string contains illegal characters
311         */
312        public void setIndent(String indent) {
313                if (indent.length() == 0) {
314                        throw new IllegalArgumentException("Indent string cannot be empty.");
315                }
316
317                if (lineLength != null && indent.length() >= lineLength) {
318                        throw new IllegalArgumentException("Indent string length must be less than the line length.");
319                }
320
321                for (int i = 0; i < indent.length(); i++) {
322                        char c = indent.charAt(i);
323                        switch (c) {
324                        case ' ':
325                        case '\t':
326                                break;
327                        default:
328                                throw new IllegalArgumentException("Indent string can only contain tabs and spaces.");
329                        }
330                }
331
332                this.indent = indent;
333        }
334
335        /**
336         * Gets the wrapped {@link Writer} object.
337         * @return the writer object
338         */
339        public Writer getWriter() {
340                return writer;
341        }
342
343        /**
344         * Closes the writer.
345         */
346        @Override
347        public void close() throws IOException {
348                writer.close();
349        }
350
351        /**
352         * Flushes the writer.
353         */
354        @Override
355        public void flush() throws IOException {
356                writer.flush();
357        }
358}