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 }