View Javadoc
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.java 1429868 2013-01-07 16:08:05Z 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 }