001/* 002 * MIT License 003 * 004 * Copyright (c) 2016 Michael Angstadt 005 * 006 * Permission is hereby granted, free of charge, to any person obtaining a copy 007 * of this software and associated documentation files (the "Software"), to deal 008 * in the Software without restriction, including without limitation the rights 009 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 010 * copies of the Software, and to permit persons to whom the Software is 011 * furnished to do so, subject to the following conditions: 012 * 013 * The above copyright notice and this permission notice shall be included in 014 * all copies or substantial portions of the Software. 015 * 016 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 017 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 018 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 019 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 020 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 021 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 022 * SOFTWARE. 023 */ 024 025package com.github.mangstadt.vinnie.io; 026 027import java.io.IOException; 028import java.io.Writer; 029import java.nio.charset.Charset; 030 031import com.github.mangstadt.vinnie.SyntaxStyle; 032import com.github.mangstadt.vinnie.codec.EncoderException; 033import com.github.mangstadt.vinnie.codec.QuotedPrintableCodec; 034 035/** 036 * Automatically folds lines as they are written. 037 * @author Michael Angstadt 038 */ 039public class FoldedLineWriter extends Writer { 040 private static final String CRLF = "\r\n"; 041 private final Writer writer; 042 043 private Integer lineLength = 75; 044 private String indent = " "; 045 046 private int curLineLength = 0; 047 048 /** 049 * Creates a folded line writer. 050 * @param writer the writer object to wrap 051 */ 052 public FoldedLineWriter(Writer writer) { 053 this.writer = writer; 054 } 055 056 /** 057 * Writes a newline. 058 * @throws IOException if there's a problem writing to the output stream 059 */ 060 public void writeln() throws IOException { 061 write(CRLF); 062 } 063 064 /** 065 * Writes a string. 066 * @param str the string to write 067 * @param quotedPrintable true to encode the string in quoted-printable 068 * encoding, false not to 069 * @param charset the character set to use when encoding the string into 070 * quoted-printable 071 * @throws IOException if there's a problem writing to the output stream 072 */ 073 public void write(CharSequence str, boolean quotedPrintable, Charset charset) throws IOException { 074 write(str.toString().toCharArray(), 0, str.length(), quotedPrintable, charset); 075 } 076 077 /** 078 * Writes a portion of an array of characters. 079 * @param cbuf the array of characters 080 * @param off the offset from which to start writing characters 081 * @param len the number of characters to write 082 * @throws IOException if there's a problem writing to the output stream 083 */ 084 @Override 085 public void write(char[] cbuf, int off, int len) throws IOException { 086 write(cbuf, off, len, false, null); 087 } 088 089 /** 090 * Writes a portion of an array of characters. 091 * @param cbuf the array of characters 092 * @param off the offset from which to start writing characters 093 * @param len the number of characters to write 094 * @param quotedPrintable true to encode the string in quoted-printable 095 * encoding, false not to 096 * @param charset the character set to use when encoding the string into 097 * quoted-printable 098 * @throws IOException if there's a problem writing to the output stream 099 */ 100 public void write(char[] cbuf, int off, int len, boolean quotedPrintable, Charset charset) throws IOException { 101 if (quotedPrintable) { 102 String str = new String(cbuf, off, len); 103 QuotedPrintableCodec codec = new QuotedPrintableCodec(charset.name()); 104 105 String encoded; 106 try { 107 encoded = codec.encode(str); 108 } catch (EncoderException e) { 109 /* 110 * Thrown if an unsupported charset is passed into the codec. 111 * This should never happen because we already know the charset 112 * is valid (a Charset object is passed into the method). 113 */ 114 throw new IOException(e); 115 } 116 117 cbuf = encoded.toCharArray(); 118 off = 0; 119 len = cbuf.length; 120 } 121 122 if (lineLength == null) { 123 /* 124 * If line folding is disabled, then write directly to the Writer. 125 */ 126 writer.write(cbuf, off, len); 127 return; 128 } 129 130 int effectiveLineLength = lineLength; 131 if (quotedPrintable) { 132 /* 133 * Account for the "=" character that must be appended onto each 134 * line. 135 */ 136 effectiveLineLength -= 1; 137 } 138 139 int encodedCharPos = -1; 140 int start = off; 141 int end = off + len; 142 for (int i = start; i < end; i++) { 143 char c = cbuf[i]; 144 145 /* 146 * Keep track of the quoted-printable characters to prevent them 147 * from being cut in two at a folding boundary. 148 */ 149 if (encodedCharPos >= 0) { 150 encodedCharPos++; 151 if (encodedCharPos == 3) { 152 encodedCharPos = -1; 153 } 154 } 155 156 if (c == '\n') { 157 writer.write(cbuf, start, i - start + 1); 158 curLineLength = 0; 159 start = i + 1; 160 continue; 161 } 162 163 if (c == '\r') { 164 if (i == end - 1 || cbuf[i + 1] != '\n') { 165 writer.write(cbuf, start, i - start + 1); 166 curLineLength = 0; 167 start = i + 1; 168 } else { 169 curLineLength++; 170 } 171 continue; 172 } 173 174 if (c == '=' && quotedPrintable) { 175 encodedCharPos = 0; 176 } 177 178 if (curLineLength >= effectiveLineLength) { 179 /* 180 * If the last characters on the line are whitespace, then 181 * exceed the max line length in order to include the whitespace 182 * on the same line. 183 * 184 * This is to prevent the whitespace from merging with the 185 * folding whitespace of the following folded line and 186 * potentially being lost. 187 * 188 * Old syntax style allows multiple whitespace characters to be 189 * used for folding, so it could get lost here. New syntax style 190 * only allows one character to be used. 191 */ 192 if (Character.isWhitespace(c)) { 193 while (Character.isWhitespace(c) && i < end - 1) { 194 i++; 195 c = cbuf[i]; 196 } 197 if (i >= end - 1) { 198 /* 199 * The rest of the char array is whitespace, so leave 200 * the loop. 201 */ 202 break; 203 } 204 } 205 206 /* 207 * If we are in the middle of a quoted-printable encoded 208 * character, then exceed the max line length so the sequence 209 * doesn't get split up across multiple lines. 210 */ 211 if (encodedCharPos > 0) { 212 i += 3 - encodedCharPos; 213 if (i >= end - 1) { 214 /* 215 * The rest of the char array was a quoted-printable 216 * encoded char, so leave the loop. 217 */ 218 break; 219 } 220 } 221 222 /* 223 * If the last char is the low (second) char in a surrogate 224 * pair, don't split the pair across two lines. 225 */ 226 if (Character.isLowSurrogate(c)) { 227 i++; 228 if (i >= end - 1) { 229 /* 230 * Surrogate pair finishes the char array, so leave the 231 * loop. 232 */ 233 break; 234 } 235 } 236 237 writer.write(cbuf, start, i - start); 238 if (quotedPrintable) { 239 writer.write('='); 240 } 241 writer.write(CRLF); 242 243 /* 244 * Do not include indentation whitespace if the value is 245 * quoted-printable. 246 */ 247 curLineLength = 1; 248 if (!quotedPrintable) { 249 writer.write(indent); 250 curLineLength += indent.length(); 251 } 252 253 start = i; 254 255 continue; 256 } 257 258 curLineLength++; 259 } 260 261 writer.write(cbuf, start, end - start); 262 } 263 264 /** 265 * Gets the maximum length a line can be before it is folded (excluding the 266 * newline, defaults to 75). 267 * @return the line length or null if folding is disabled 268 */ 269 public Integer getLineLength() { 270 return lineLength; 271 } 272 273 /** 274 * Sets the maximum length a line can be before it is folded (excluding the 275 * newline, defaults to 75). 276 * @param lineLength the line length or null to disable folding 277 * @throws IllegalArgumentException if the line length is less than or equal 278 * to zero, or the line length is less than the length of the indent string 279 */ 280 public void setLineLength(Integer lineLength) { 281 if (lineLength != null) { 282 if (lineLength <= 0) { 283 throw new IllegalArgumentException("Line length must be greater than 0."); 284 } 285 if (lineLength <= indent.length()) { 286 throw new IllegalArgumentException("Line length must be greater than indent string length."); 287 } 288 } 289 290 this.lineLength = lineLength; 291 } 292 293 /** 294 * Gets the string that is prepended to each folded line (defaults to a 295 * single space character). 296 * @return the indent string 297 */ 298 public String getIndent() { 299 return indent; 300 } 301 302 /** 303 * Sets the string that is prepended to each folded line (defaults to a 304 * single space character). 305 * @param indent the indent string (cannot be empty, may only contain tabs 306 * and spaces). Note that data streams using {@link SyntaxStyle#NEW} syntax 307 * MUST use an indent string that contains EXACTLY ONE character. 308 * @throws IllegalArgumentException if the indent string is empty, or the 309 * length of the indent string is greater than the max line length, or the 310 * indent string contains illegal characters 311 */ 312 public void setIndent(String indent) { 313 if (indent.length() == 0) { 314 throw new IllegalArgumentException("Indent string cannot be empty."); 315 } 316 317 if (lineLength != null && indent.length() >= lineLength) { 318 throw new IllegalArgumentException("Indent string length must be less than the line length."); 319 } 320 321 for (int i = 0; i < indent.length(); i++) { 322 char c = indent.charAt(i); 323 switch (c) { 324 case ' ': 325 case '\t': 326 break; 327 default: 328 throw new IllegalArgumentException("Indent string can only contain tabs and spaces."); 329 } 330 } 331 332 this.indent = indent; 333 } 334 335 /** 336 * Gets the wrapped {@link Writer} object. 337 * @return the writer object 338 */ 339 public Writer getWriter() { 340 return writer; 341 } 342 343 /** 344 * Closes the writer. 345 */ 346 @Override 347 public void close() throws IOException { 348 writer.close(); 349 } 350 351 /** 352 * Flushes the writer. 353 */ 354 @Override 355 public void flush() throws IOException { 356 writer.flush(); 357 } 358}