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