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 }