1 /*
2 * Licensed to the Apache Software Foundation (ASF) under one or more
3 * contributor license agreements. See the NOTICE file distributed with
4 * this work for additional information regarding copyright ownership.
5 * The ASF licenses this file to You under the Apache License, Version 2.0
6 * (the "License"); you may not use this file except in compliance with
7 * the License. You may obtain a copy of the License at
8 *
9 * http://www.apache.org/licenses/LICENSE-2.0
10 *
11 * Unless required by applicable law or agreed to in writing, software
12 * distributed under the License is distributed on an "AS IS" BASIS,
13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14 * See the License for the specific language governing permissions and
15 * limitations under the License.
16 */
17
18 package org.apache.commons.codec.net;
19
20 import java.io.UnsupportedEncodingException;
21 import java.nio.charset.Charset;
22 import java.util.BitSet;
23
24 import org.apache.commons.codec.Charsets;
25 import org.apache.commons.codec.DecoderException;
26 import org.apache.commons.codec.EncoderException;
27 import org.apache.commons.codec.StringDecoder;
28 import org.apache.commons.codec.StringEncoder;
29
30 /**
31 * Similar to the Quoted-Printable content-transfer-encoding defined in
32 * <a href="http://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a> and designed to allow text containing mostly ASCII
33 * characters to be decipherable on an ASCII terminal without decoding.
34 * <p>
35 * <a href="http://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a> describes techniques to allow the encoding of non-ASCII
36 * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message
37 * handling software.
38 * <p>
39 * This class is conditionally thread-safe.
40 * The instance field {@link #encodeBlanks} is mutable {@link #setEncodeBlanks(boolean)}
41 * but is not volatile, and accesses are not synchronised.
42 * If an instance of the class is shared between threads, the caller needs to ensure that suitable synchronisation
43 * is used to ensure safe publication of the value between threads, and must not invoke
44 * {@link #setEncodeBlanks(boolean)} after initial setup.
45 *
46 * @see <a href="http://www.ietf.org/rfc/rfc1522.txt">MIME (Multipurpose Internet Mail Extensions) Part Two: Message
47 * Header Extensions for Non-ASCII Text</a>
48 *
49 * @since 1.3
50 * @version $Id: QCodec.html 889935 2013-12-11 05:05:13Z ggregory $
51 */
52 public class QCodec extends RFC1522Codec implements StringEncoder, StringDecoder {
53 /**
54 * The default charset used for string decoding and encoding.
55 */
56 private final Charset charset;
57
58 /**
59 * BitSet of printable characters as defined in RFC 1522.
60 */
61 private static final BitSet PRINTABLE_CHARS = new BitSet(256);
62 // Static initializer for printable chars collection
63 static {
64 // alpha characters
65 PRINTABLE_CHARS.set(' ');
66 PRINTABLE_CHARS.set('!');
67 PRINTABLE_CHARS.set('"');
68 PRINTABLE_CHARS.set('#');
69 PRINTABLE_CHARS.set('$');
70 PRINTABLE_CHARS.set('%');
71 PRINTABLE_CHARS.set('&');
72 PRINTABLE_CHARS.set('\'');
73 PRINTABLE_CHARS.set('(');
74 PRINTABLE_CHARS.set(')');
75 PRINTABLE_CHARS.set('*');
76 PRINTABLE_CHARS.set('+');
77 PRINTABLE_CHARS.set(',');
78 PRINTABLE_CHARS.set('-');
79 PRINTABLE_CHARS.set('.');
80 PRINTABLE_CHARS.set('/');
81 for (int i = '0'; i <= '9'; i++) {
82 PRINTABLE_CHARS.set(i);
83 }
84 PRINTABLE_CHARS.set(':');
85 PRINTABLE_CHARS.set(';');
86 PRINTABLE_CHARS.set('<');
87 PRINTABLE_CHARS.set('>');
88 PRINTABLE_CHARS.set('@');
89 for (int i = 'A'; i <= 'Z'; i++) {
90 PRINTABLE_CHARS.set(i);
91 }
92 PRINTABLE_CHARS.set('[');
93 PRINTABLE_CHARS.set('\\');
94 PRINTABLE_CHARS.set(']');
95 PRINTABLE_CHARS.set('^');
96 PRINTABLE_CHARS.set('`');
97 for (int i = 'a'; i <= 'z'; i++) {
98 PRINTABLE_CHARS.set(i);
99 }
100 PRINTABLE_CHARS.set('{');
101 PRINTABLE_CHARS.set('|');
102 PRINTABLE_CHARS.set('}');
103 PRINTABLE_CHARS.set('~');
104 }
105
106 private static final byte BLANK = 32;
107
108 private static final byte UNDERSCORE = 95;
109
110 private boolean encodeBlanks = false;
111
112 /**
113 * Default constructor.
114 */
115 public QCodec() {
116 this(Charsets.UTF_8);
117 }
118
119 /**
120 * Constructor which allows for the selection of a default charset.
121 *
122 * @param charset
123 * the default string charset to use.
124 *
125 * @see <a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
126 * @since 1.7
127 */
128 public QCodec(final Charset charset) {
129 super();
130 this.charset = charset;
131 }
132
133 /**
134 * Constructor which allows for the selection of a default charset.
135 *
136 * @param charsetName
137 * the charset to use.
138 * @throws java.nio.charset.UnsupportedCharsetException
139 * If the named charset is unavailable
140 * @since 1.7 throws UnsupportedCharsetException if the named charset is unavailable
141 * @see <a href="http://download.oracle.com/javase/6/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
142 */
143 public QCodec(final String charsetName) {
144 this(Charset.forName(charsetName));
145 }
146
147 @Override
148 protected String getEncoding() {
149 return "Q";
150 }
151
152 @Override
153 protected byte[] doEncoding(final byte[] bytes) {
154 if (bytes == null) {
155 return null;
156 }
157 final byte[] data = QuotedPrintableCodec.encodeQuotedPrintable(PRINTABLE_CHARS, bytes);
158 if (this.encodeBlanks) {
159 for (int i = 0; i < data.length; i++) {
160 if (data[i] == BLANK) {
161 data[i] = UNDERSCORE;
162 }
163 }
164 }
165 return data;
166 }
167
168 @Override
169 protected byte[] doDecoding(final byte[] bytes) throws DecoderException {
170 if (bytes == null) {
171 return null;
172 }
173 boolean hasUnderscores = false;
174 for (final byte b : bytes) {
175 if (b == UNDERSCORE) {
176 hasUnderscores = true;
177 break;
178 }
179 }
180 if (hasUnderscores) {
181 final byte[] tmp = new byte[bytes.length];
182 for (int i = 0; i < bytes.length; i++) {
183 final byte b = bytes[i];
184 if (b != UNDERSCORE) {
185 tmp[i] = b;
186 } else {
187 tmp[i] = BLANK;
188 }
189 }
190 return QuotedPrintableCodec.decodeQuotedPrintable(tmp);
191 }
192 return QuotedPrintableCodec.decodeQuotedPrintable(bytes);
193 }
194
195 /**
196 * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
197 *
198 * @param str
199 * string to convert to quoted-printable form
200 * @param charset
201 * the charset for str
202 * @return quoted-printable string
203 * @throws EncoderException
204 * thrown if a failure condition is encountered during the encoding process.
205 * @since 1.7
206 */
207 public String encode(final String str, final Charset charset) throws EncoderException {
208 if (str == null) {
209 return null;
210 }
211 return encodeText(str, charset);
212 }
213
214 /**
215 * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
216 *
217 * @param str
218 * string to convert to quoted-printable form
219 * @param charset
220 * the charset for str
221 * @return quoted-printable string
222 * @throws EncoderException
223 * thrown if a failure condition is encountered during the encoding process.
224 */
225 public String encode(final String str, final String charset) throws EncoderException {
226 if (str == null) {
227 return null;
228 }
229 try {
230 return encodeText(str, charset);
231 } catch (final UnsupportedEncodingException e) {
232 throw new EncoderException(e.getMessage(), e);
233 }
234 }
235
236 /**
237 * Encodes a string into its quoted-printable form using the default charset. Unsafe characters are escaped.
238 *
239 * @param str
240 * string to convert to quoted-printable form
241 * @return quoted-printable string
242 * @throws EncoderException
243 * thrown if a failure condition is encountered during the encoding process.
244 */
245 @Override
246 public String encode(final String str) throws EncoderException {
247 if (str == null) {
248 return null;
249 }
250 return encode(str, getCharset());
251 }
252
253 /**
254 * Decodes a quoted-printable string into its original form. Escaped characters are converted back to their original
255 * representation.
256 *
257 * @param str
258 * quoted-printable string to convert into its original form
259 * @return original string
260 * @throws DecoderException
261 * A decoder exception is thrown if a failure condition is encountered during the decode process.
262 */
263 @Override
264 public String decode(final String str) throws DecoderException {
265 if (str == null) {
266 return null;
267 }
268 try {
269 return decodeText(str);
270 } catch (final UnsupportedEncodingException e) {
271 throw new DecoderException(e.getMessage(), e);
272 }
273 }
274
275 /**
276 * Encodes an object into its quoted-printable form using the default charset. Unsafe characters are escaped.
277 *
278 * @param obj
279 * object to convert to quoted-printable form
280 * @return quoted-printable object
281 * @throws EncoderException
282 * thrown if a failure condition is encountered during the encoding process.
283 */
284 @Override
285 public Object encode(final Object obj) throws EncoderException {
286 if (obj == null) {
287 return null;
288 } else if (obj instanceof String) {
289 return encode((String) obj);
290 } else {
291 throw new EncoderException("Objects of type " +
292 obj.getClass().getName() +
293 " cannot be encoded using Q codec");
294 }
295 }
296
297 /**
298 * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original
299 * representation.
300 *
301 * @param obj
302 * quoted-printable object to convert into its original form
303 * @return original object
304 * @throws DecoderException
305 * Thrown if the argument is not a <code>String</code>. Thrown if a failure condition is encountered
306 * during the decode process.
307 */
308 @Override
309 public Object decode(final Object obj) throws DecoderException {
310 if (obj == null) {
311 return null;
312 } else if (obj instanceof String) {
313 return decode((String) obj);
314 } else {
315 throw new DecoderException("Objects of type " +
316 obj.getClass().getName() +
317 " cannot be decoded using Q codec");
318 }
319 }
320
321 /**
322 * Gets the default charset name used for string decoding and encoding.
323 *
324 * @return the default charset name
325 * @since 1.7
326 */
327 public Charset getCharset() {
328 return this.charset;
329 }
330
331 /**
332 * Gets the default charset name used for string decoding and encoding.
333 *
334 * @return the default charset name
335 */
336 public String getDefaultCharset() {
337 return this.charset.name();
338 }
339
340 /**
341 * Tests if optional transformation of SPACE characters is to be used
342 *
343 * @return {@code true} if SPACE characters are to be transformed, {@code false} otherwise
344 */
345 public boolean isEncodeBlanks() {
346 return this.encodeBlanks;
347 }
348
349 /**
350 * Defines whether optional transformation of SPACE characters is to be used
351 *
352 * @param b
353 * {@code true} if SPACE characters are to be transformed, {@code false} otherwise
354 */
355 public void setEncodeBlanks(final boolean b) {
356 this.encodeBlanks = b;
357 }
358 }