001    package ezvcard.io.text;
002    
003    import java.io.IOException;
004    import java.io.OutputStreamWriter;
005    import java.io.Writer;
006    import java.nio.charset.Charset;
007    
008    import ezvcard.util.org.apache.commons.codec.EncoderException;
009    import ezvcard.util.org.apache.commons.codec.net.QuotedPrintableCodec;
010    
011    /*
012     Copyright (c) 2013, 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     The views and conclusions contained in the software and documentation are those
036     of the authors and should not be interpreted as representing official policies, 
037     either expressed or implied, of the FreeBSD Project.
038     */
039    
040    /**
041     * Automatically folds lines as they are written.
042     * @author Michael Angstadt
043     */
044    public class FoldedLineWriter extends Writer {
045            private final Writer writer;
046            private int curLineLength = 0;
047            private Integer lineLength;
048            private String indent;
049            private String newline;
050    
051            /**
052             * Creates a folded line writer.
053             * @param writer the writer object to wrap
054             * @param lineLength the maximum length a line can be before it is folded
055             * (excluding the newline), or null disable folding
056             * @param indent the string to prepend to each folded line (e.g. a single
057             * space character)
058             * @param newline the newline sequence to use (e.g. "\r\n")
059             * @throws IllegalArgumentException if the line length is less than or equal
060             * to zero
061             * @throws IllegalArgumentException if the length of the indent string is
062             * greater than the max line length
063             */
064            public FoldedLineWriter(Writer writer, Integer lineLength, String indent, String newline) {
065                    this.writer = writer;
066                    setLineLength(lineLength);
067                    setIndent(indent);
068                    this.newline = newline;
069            }
070    
071            /**
072             * Writes a string, followed by a newline.
073             * @param str the text to write
074             * @throws IOException if there's a problem writing to the output stream
075             */
076            public void writeln(String str) throws IOException {
077                    write(str);
078                    write(newline);
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             * @return this
090             * @throws IOException if there's a problem writing to the output stream
091             */
092            public FoldedLineWriter append(CharSequence str, boolean quotedPrintable, Charset charset) throws IOException {
093                    write(str, quotedPrintable, charset);
094                    return this;
095            }
096    
097            /**
098             * Writes a string.
099             * @param str the string to write
100             * @param quotedPrintable true to encode the string in quoted-printable
101             * encoding, false not to
102             * @param charset the character set to use when encoding into
103             * quoted-printable, or null to use the writer's character encoding (only
104             * applicable if "quotedPrintable" is set to true)
105             * @throws IOException if there's a problem writing to the output stream
106             */
107            public void write(CharSequence str, boolean quotedPrintable, Charset charset) throws IOException {
108                    write(str.toString().toCharArray(), 0, str.length(), quotedPrintable, charset);
109            }
110    
111            @Override
112            public void write(char[] cbuf, int off, int len) throws IOException {
113                    write(cbuf, off, len, false, null);
114            }
115    
116            /**
117             * Writes a portion of an array of characters.
118             * @param cbuf the array of characters
119             * @param off the offset from which to start writing characters
120             * @param len the number of characters to write
121             * @param quotedPrintable true to encode the string in quoted-printable
122             * encoding, false not to
123             * @param charset the character set to use when encoding into
124             * quoted-printable, or null to use the writer's character encoding (only
125             * applicable if "quotedPrintable" is set to true)
126             * @throws IOException if there's a problem writing to the output stream
127             */
128            public void write(char[] cbuf, int off, int len, boolean quotedPrintable, Charset charset) throws IOException {
129                    //encode to quoted-printable
130                    if (quotedPrintable) {
131                            if (charset == null) {
132                                    charset = getEncoding();
133                                    if (charset == null) {
134                                            charset = Charset.defaultCharset();
135                                    }
136                            }
137    
138                            QuotedPrintableCodec codec = new QuotedPrintableCodec(charset.name());
139                            try {
140                                    String str = new String(cbuf, off, len);
141                                    String encoded = codec.encode(str);
142    
143                                    cbuf = encoded.toCharArray();
144                                    off = 0;
145                                    len = cbuf.length;
146                            } catch (EncoderException e) {
147                                    //thrown if an unsupported charset is passed into the codec
148                                    //this should never be thrown because we already know the charset is valid (Charset object is passed in)
149                                    throw new RuntimeException(e);
150                            }
151                    }
152    
153                    if (lineLength == null) {
154                            //if line folding is disabled, then write directly to the Writer
155                            writer.write(cbuf, off, len);
156                            return;
157                    }
158    
159                    int effectiveLineLength = lineLength;
160                    if (quotedPrintable) {
161                            //"=" must be appended onto each line
162                            effectiveLineLength -= 1;
163                    }
164    
165                    int encodedCharPos = -1;
166                    int start = off;
167                    int end = off + len;
168                    for (int i = start; i < end; i++) {
169                            char c = cbuf[i];
170    
171                            //keep track of the quoted-printable characters to prevent them from being cut in two at a folding boundary
172                            if (encodedCharPos >= 0) {
173                                    encodedCharPos++;
174                                    if (encodedCharPos == 3) {
175                                            encodedCharPos = -1;
176                                    }
177                            }
178    
179                            if (c == '\n') {
180                                    writer.write(cbuf, start, i - start + 1);
181                                    curLineLength = 0;
182                                    start = i + 1;
183                                    continue;
184                            }
185    
186                            if (c == '\r') {
187                                    if (i == end - 1 || cbuf[i + 1] != '\n') {
188                                            writer.write(cbuf, start, i - start + 1);
189                                            curLineLength = 0;
190                                            start = i + 1;
191                                    } else {
192                                            curLineLength++;
193                                    }
194                                    continue;
195                            }
196    
197                            if (c == '=' && quotedPrintable) {
198                                    encodedCharPos = 0;
199                            }
200    
201                            if (curLineLength >= effectiveLineLength) {
202                                    //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
203                                    //otherwise it will be lost because it will merge with the padding on the next line
204                                    if (Character.isWhitespace(c)) {
205                                            while (Character.isWhitespace(c) && i < end - 1) {
206                                                    i++;
207                                                    c = cbuf[i];
208                                            }
209                                            if (i >= end - 1) {
210                                                    //the rest of the char array is whitespace, so leave the loop
211                                                    break;
212                                            }
213                                    }
214    
215                                    //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
216                                    if (encodedCharPos > 0) {
217                                            i += 3 - encodedCharPos;
218                                            if (i >= end - 1) {
219                                                    //the rest of the char array was an encoded char, so leave the loop
220                                                    break;
221                                            }
222                                    }
223    
224                                    writer.write(cbuf, start, i - start);
225                                    if (quotedPrintable) {
226                                            writer.write('=');
227                                    }
228                                    writer.write(newline);
229                                    writer.write(indent);
230                                    curLineLength = indent.length() + 1;
231                                    start = i;
232    
233                                    continue;
234                            }
235    
236                            curLineLength++;
237                    }
238    
239                    writer.write(cbuf, start, end - start);
240            }
241    
242            /**
243             * Closes the writer.
244             */
245            @Override
246            public void close() throws IOException {
247                    writer.close();
248            }
249    
250            /**
251             * Flushes the writer.
252             */
253            @Override
254            public void flush() throws IOException {
255                    writer.flush();
256            }
257    
258            /**
259             * Gets the maximum length a line can be before it is folded (excluding the
260             * newline).
261             * @return the line length or null if folding is disabled
262             */
263            public Integer getLineLength() {
264                    return lineLength;
265            }
266    
267            /**
268             * Sets the maximum length a line can be before it is folded (excluding the
269             * newline).
270             * @param lineLength the line length or null to disable folding
271             * @throws IllegalArgumentException if the line length is less than or equal
272             * to zero
273             */
274            public void setLineLength(Integer lineLength) {
275                    if (lineLength != null && lineLength <= 0) {
276                            throw new IllegalArgumentException("Line length must be greater than 0.");
277                    }
278                    this.lineLength = lineLength;
279            }
280    
281            /**
282             * Gets the string that is prepended to each folded line.
283             * @return the indent string
284             */
285            public String getIndent() {
286                    return indent;
287            }
288    
289            /**
290             * Sets the string that is prepended to each folded line.
291             * @param indent the indent string (e.g. a single space character)
292             * @throws IllegalArgumentException if the length of the indent string is
293             * greater than the max line length
294             */
295            public void setIndent(String indent) {
296                    if (lineLength != null && indent.length() >= lineLength) {
297                            throw new IllegalArgumentException("The length of the indent string must be less than the max line length.");
298                    }
299                    this.indent = indent;
300            }
301    
302            /**
303             * Gets the newline sequence that is used to separate lines.
304             * @return the newline sequence
305             */
306            public String getNewline() {
307                    return newline;
308            }
309    
310            /**
311             * Sets the newline sequence that is used to separate lines
312             * @param newline the newline sequence
313             */
314            public void setNewline(String newline) {
315                    this.newline = newline;
316            }
317    
318            /**
319             * Gets the wrapped {@link Writer} object.
320             * @return the wrapped writer
321             */
322            public Writer getWriter() {
323                    return writer;
324            }
325    
326            /**
327             * Gets the writer's character encoding.
328             * @return the writer's character encoding or null if undefined
329             */
330            public Charset getEncoding() {
331                    if (!(writer instanceof OutputStreamWriter)) {
332                            return null;
333                    }
334    
335                    OutputStreamWriter osw = (OutputStreamWriter) writer;
336                    String charsetStr = osw.getEncoding();
337                    return (charsetStr == null) ? null : Charset.forName(charsetStr);
338            }
339    }