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