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