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 }