001/*
002 * Licensed to the Apache Software Foundation (ASF) under one or more
003 * contributor license agreements.  See the NOTICE file distributed with
004 * this work for additional information regarding copyright ownership.
005 * The ASF licenses this file to You under the Apache License, Version 2.0
006 * (the "License"); you may not use this file except in compliance with
007 * the License.  You may obtain a copy of the License at
008 * 
009 *      http://www.apache.org/licenses/LICENSE-2.0
010 * 
011 * Unless required by applicable law or agreed to in writing, software
012 * distributed under the License is distributed on an "AS IS" BASIS,
013 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
014 * See the License for the specific language governing permissions and
015 * limitations under the License.
016 */
017
018package org.apache.commons.codec.net;
019
020import java.io.UnsupportedEncodingException;
021import java.util.BitSet;
022
023import org.apache.commons.codec.DecoderException;
024import org.apache.commons.codec.EncoderException;
025import org.apache.commons.codec.CharEncoding;
026import org.apache.commons.codec.StringDecoder;
027import org.apache.commons.codec.StringEncoder;
028
029/**
030 * <p>
031 * Similar to the Quoted-Printable content-transfer-encoding defined in <a
032 * href="http://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a> and designed to allow text containing mostly ASCII
033 * characters to be decipherable on an ASCII terminal without decoding.
034 * </p>
035 * 
036 * <p>
037 * <a href="http://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a> describes techniques to allow the encoding of non-ASCII
038 * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message
039 * handling software.
040 * </p>
041 * 
042 * @see <a href="http://www.ietf.org/rfc/rfc1522.txt">MIME (Multipurpose Internet Mail Extensions) Part Two: Message
043 *          Header Extensions for Non-ASCII Text</a>
044 * 
045 * @author Apache Software Foundation
046 * @since 1.3
047 * @version $Id: QCodec.java 797857 2009-07-25 23:43:33Z ggregory $
048 */
049public class QCodec extends RFC1522Codec implements StringEncoder, StringDecoder {
050    /**
051     * The default charset used for string decoding and encoding.
052     */
053    private final String charset;
054
055    /**
056     * BitSet of printable characters as defined in RFC 1522.
057     */
058    private static final BitSet PRINTABLE_CHARS = new BitSet(256);
059    // Static initializer for printable chars collection
060    static {
061        // alpha characters
062        PRINTABLE_CHARS.set(' ');
063        PRINTABLE_CHARS.set('!');
064        PRINTABLE_CHARS.set('"');
065        PRINTABLE_CHARS.set('#');
066        PRINTABLE_CHARS.set('$');
067        PRINTABLE_CHARS.set('%');
068        PRINTABLE_CHARS.set('&');
069        PRINTABLE_CHARS.set('\'');
070        PRINTABLE_CHARS.set('(');
071        PRINTABLE_CHARS.set(')');
072        PRINTABLE_CHARS.set('*');
073        PRINTABLE_CHARS.set('+');
074        PRINTABLE_CHARS.set(',');
075        PRINTABLE_CHARS.set('-');
076        PRINTABLE_CHARS.set('.');
077        PRINTABLE_CHARS.set('/');
078        for (int i = '0'; i <= '9'; i++) {
079            PRINTABLE_CHARS.set(i);
080        }
081        PRINTABLE_CHARS.set(':');
082        PRINTABLE_CHARS.set(';');
083        PRINTABLE_CHARS.set('<');
084        PRINTABLE_CHARS.set('>');
085        PRINTABLE_CHARS.set('@');
086        for (int i = 'A'; i <= 'Z'; i++) {
087            PRINTABLE_CHARS.set(i);
088        }
089        PRINTABLE_CHARS.set('[');
090        PRINTABLE_CHARS.set('\\');
091        PRINTABLE_CHARS.set(']');
092        PRINTABLE_CHARS.set('^');
093        PRINTABLE_CHARS.set('`');
094        for (int i = 'a'; i <= 'z'; i++) {
095            PRINTABLE_CHARS.set(i);
096        }
097        PRINTABLE_CHARS.set('{');
098        PRINTABLE_CHARS.set('|');
099        PRINTABLE_CHARS.set('}');
100        PRINTABLE_CHARS.set('~');
101    }
102
103    private static final byte BLANK = 32;
104
105    private static final byte UNDERSCORE = 95;
106
107    private boolean encodeBlanks = false;
108
109    /**
110     * Default constructor.
111     */
112    public QCodec() {
113        this(CharEncoding.UTF_8);
114    }
115
116    /**
117     * Constructor which allows for the selection of a default charset
118     * 
119     * @param charset
120     *                  the default string charset to use.
121     * 
122     * @see <a href="http://java.sun.com/j2se/1.4.2/docs/api/java/nio/charset/Charset.html">Standard charsets</a>
123     */
124    public QCodec(final String charset) {
125        super();
126        this.charset = charset;
127    }
128
129    protected String getEncoding() {
130        return "Q";
131    }
132
133    protected byte[] doEncoding(byte[] bytes) {
134        if (bytes == null) {
135            return null;
136        }
137        byte[] data = QuotedPrintableCodec.encodeQuotedPrintable(PRINTABLE_CHARS, bytes);
138        if (this.encodeBlanks) {
139            for (int i = 0; i < data.length; i++) {
140                if (data[i] == BLANK) {
141                    data[i] = UNDERSCORE;
142                }
143            }
144        }
145        return data;
146    }
147
148    protected byte[] doDecoding(byte[] bytes) throws DecoderException {
149        if (bytes == null) {
150            return null;
151        }
152        boolean hasUnderscores = false;
153        for (int i = 0; i < bytes.length; i++) {
154            if (bytes[i] == UNDERSCORE) {
155                hasUnderscores = true;
156                break;
157            }
158        }
159        if (hasUnderscores) {
160            byte[] tmp = new byte[bytes.length];
161            for (int i = 0; i < bytes.length; i++) {
162                byte b = bytes[i];
163                if (b != UNDERSCORE) {
164                    tmp[i] = b;
165                } else {
166                    tmp[i] = BLANK;
167                }
168            }
169            return QuotedPrintableCodec.decodeQuotedPrintable(tmp);
170        } 
171        return QuotedPrintableCodec.decodeQuotedPrintable(bytes);       
172    }
173
174    /**
175     * Encodes a string into its quoted-printable form using the specified charset. Unsafe characters are escaped.
176     * 
177     * @param pString
178     *                  string to convert to quoted-printable form
179     * @param charset
180     *                  the charset for pString
181     * @return quoted-printable string
182     * 
183     * @throws EncoderException
184     *                  thrown if a failure condition is encountered during the encoding process.
185     */
186    public String encode(final String pString, final String charset) throws EncoderException {
187        if (pString == null) {
188            return null;
189        }
190        try {
191            return encodeText(pString, charset);
192        } catch (UnsupportedEncodingException e) {
193            throw new EncoderException(e.getMessage(), e);
194        }
195    }
196
197    /**
198     * Encodes a string into its quoted-printable form using the default charset. Unsafe characters are escaped.
199     * 
200     * @param pString
201     *                  string to convert to quoted-printable form
202     * @return quoted-printable string
203     * 
204     * @throws EncoderException
205     *                  thrown if a failure condition is encountered during the encoding process.
206     */
207    public String encode(String pString) throws EncoderException {
208        if (pString == null) {
209            return null;
210        }
211        return encode(pString, getDefaultCharset());
212    }
213
214    /**
215     * Decodes a quoted-printable string into its original form. Escaped characters are converted back to their original
216     * representation.
217     * 
218     * @param pString
219     *                  quoted-printable string to convert into its original form
220     * 
221     * @return original string
222     * 
223     * @throws DecoderException
224     *                  A decoder exception is thrown if a failure condition is encountered during the decode process.
225     */
226    public String decode(String pString) throws DecoderException {
227        if (pString == null) {
228            return null;
229        }
230        try {
231            return decodeText(pString);
232        } catch (UnsupportedEncodingException e) {
233            throw new DecoderException(e.getMessage(), e);
234        }
235    }
236
237    /**
238     * Encodes an object into its quoted-printable form using the default charset. Unsafe characters are escaped.
239     * 
240     * @param pObject
241     *                  object to convert to quoted-printable form
242     * @return quoted-printable object
243     * 
244     * @throws EncoderException
245     *                  thrown if a failure condition is encountered during the encoding process.
246     */
247    public Object encode(Object pObject) throws EncoderException {
248        if (pObject == null) {
249            return null;
250        } else if (pObject instanceof String) {
251            return encode((String) pObject);
252        } else {
253            throw new EncoderException("Objects of type " + 
254                  pObject.getClass().getName() + 
255                  " cannot be encoded using Q codec");
256        }
257    }
258
259    /**
260     * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original
261     * representation.
262     * 
263     * @param pObject
264     *                  quoted-printable object to convert into its original form
265     * 
266     * @return original object
267     * 
268     * @throws DecoderException
269     *                  Thrown if the argument is not a <code>String</code>. Thrown if a failure condition is
270     *                  encountered during the decode process.
271     */
272    public Object decode(Object pObject) throws DecoderException {
273        if (pObject == null) {
274            return null;
275        } else if (pObject instanceof String) {
276            return decode((String) pObject);
277        } else {
278            throw new DecoderException("Objects of type " + 
279                  pObject.getClass().getName() + 
280                  " cannot be decoded using Q codec");
281        }
282    }
283
284    /**
285     * The default charset used for string decoding and encoding.
286     * 
287     * @return the default string charset.
288     */
289    public String getDefaultCharset() {
290        return this.charset;
291    }
292
293    /**
294     * Tests if optional tranformation of SPACE characters is to be used
295     * 
296     * @return <code>true</code> if SPACE characters are to be transformed, <code>false</code> otherwise
297     */
298    public boolean isEncodeBlanks() {
299        return this.encodeBlanks;
300    }
301
302    /**
303     * Defines whether optional tranformation of SPACE characters is to be used
304     * 
305     * @param b
306     *                  <code>true</code> if SPACE characters are to be transformed, <code>false</code> otherwise
307     */
308    public void setEncodeBlanks(boolean b) {
309        this.encodeBlanks = b;
310    }
311}