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 }