QCodec.java

  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.  *      https://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. package org.apache.commons.codec.net;

  18. import java.io.UnsupportedEncodingException;
  19. import java.nio.charset.Charset;
  20. import java.nio.charset.StandardCharsets;
  21. import java.nio.charset.UnsupportedCharsetException;
  22. import java.util.BitSet;

  23. import org.apache.commons.codec.DecoderException;
  24. import org.apache.commons.codec.EncoderException;
  25. import org.apache.commons.codec.StringDecoder;
  26. import org.apache.commons.codec.StringEncoder;

  27. /**
  28.  * Similar to the Quoted-Printable content-transfer-encoding defined in
  29.  * <a href="http://www.ietf.org/rfc/rfc1521.txt">RFC 1521</a> and designed to allow text containing mostly ASCII
  30.  * characters to be decipherable on an ASCII terminal without decoding.
  31.  * <p>
  32.  * <a href="http://www.ietf.org/rfc/rfc1522.txt">RFC 1522</a> describes techniques to allow the encoding of non-ASCII
  33.  * text in various portions of a RFC 822 [2] message header, in a manner which is unlikely to confuse existing message
  34.  * handling software.
  35.  * </p>
  36.  * <p>
  37.  * This class is conditionally thread-safe.
  38.  * The instance field for encoding blanks is mutable {@link #setEncodeBlanks(boolean)}
  39.  * but is not volatile, and accesses are not synchronized.
  40.  * If an instance of the class is shared between threads, the caller needs to ensure that suitable synchronization
  41.  * is used to ensure safe publication of the value between threads, and must not invoke
  42.  * {@link #setEncodeBlanks(boolean)} after initial setup.
  43.  * </p>
  44.  *
  45.  * @see <a href="http://www.ietf.org/rfc/rfc1522.txt">MIME (Multipurpose Internet Mail Extensions) Part Two: Message
  46.  *          Header Extensions for Non-ASCII Text</a>
  47.  *
  48.  * @since 1.3
  49.  */
  50. public class QCodec extends RFC1522Codec implements StringEncoder, StringDecoder {
  51.     /**
  52.      * BitSet of printable characters as defined in RFC 1522.
  53.      */
  54.     private static final BitSet PRINTABLE_CHARS = new BitSet(256);

  55.     // Static initializer for printable chars collection
  56.     static {
  57.         // alpha characters
  58.         PRINTABLE_CHARS.set(' ');
  59.         PRINTABLE_CHARS.set('!');
  60.         PRINTABLE_CHARS.set('"');
  61.         PRINTABLE_CHARS.set('#');
  62.         PRINTABLE_CHARS.set('$');
  63.         PRINTABLE_CHARS.set('%');
  64.         PRINTABLE_CHARS.set('&');
  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.         for (int i = '0'; i <= '9'; i++) {
  75.             PRINTABLE_CHARS.set(i);
  76.         }
  77.         PRINTABLE_CHARS.set(':');
  78.         PRINTABLE_CHARS.set(';');
  79.         PRINTABLE_CHARS.set('<');
  80.         PRINTABLE_CHARS.set('>');
  81.         PRINTABLE_CHARS.set('@');
  82.         for (int i = 'A'; i <= 'Z'; i++) {
  83.             PRINTABLE_CHARS.set(i);
  84.         }
  85.         PRINTABLE_CHARS.set('[');
  86.         PRINTABLE_CHARS.set('\\');
  87.         PRINTABLE_CHARS.set(']');
  88.         PRINTABLE_CHARS.set('^');
  89.         PRINTABLE_CHARS.set('`');
  90.         for (int i = 'a'; i <= 'z'; i++) {
  91.             PRINTABLE_CHARS.set(i);
  92.         }
  93.         PRINTABLE_CHARS.set('{');
  94.         PRINTABLE_CHARS.set('|');
  95.         PRINTABLE_CHARS.set('}');
  96.         PRINTABLE_CHARS.set('~');
  97.     }
  98.     private static final byte SPACE = 32;

  99.     private static final byte UNDERSCORE = 95;

  100.     private boolean encodeBlanks;

  101.     /**
  102.      * Default constructor.
  103.      */
  104.     public QCodec() {
  105.         this(StandardCharsets.UTF_8);
  106.     }

  107.     /**
  108.      * Constructor which allows for the selection of a default Charset.
  109.      *
  110.      * @param charset
  111.      *            the default string Charset to use.
  112.      *
  113.      * @see Charset
  114.      * @since 1.7
  115.      */
  116.     public QCodec(final Charset charset) {
  117.         super(charset);
  118.     }

  119.     /**
  120.      * Constructor which allows for the selection of a default Charset.
  121.      *
  122.      * @param charsetName
  123.      *            the Charset to use.
  124.      * @throws java.nio.charset.UnsupportedCharsetException
  125.      *             If the named Charset is unavailable
  126.      * @since 1.7 throws UnsupportedCharsetException if the named Charset is unavailable
  127.      * @see Charset
  128.      */
  129.     public QCodec(final String charsetName) {
  130.         this(Charset.forName(charsetName));
  131.     }

  132.     /**
  133.      * Decodes a quoted-printable object into its original form. Escaped characters are converted back to their original
  134.      * representation.
  135.      *
  136.      * @param obj
  137.      *            quoted-printable object to convert into its original form
  138.      * @return original object
  139.      * @throws DecoderException
  140.      *             Thrown if the argument is not a {@code String}. Thrown if a failure condition is encountered
  141.      *             during the decode process.
  142.      */
  143.     @Override
  144.     public Object decode(final Object obj) throws DecoderException {
  145.         if (obj == null) {
  146.             return null;
  147.         }
  148.         if (obj instanceof String) {
  149.             return decode((String) obj);
  150.         }
  151.         throw new DecoderException("Objects of type " + obj.getClass().getName() + " cannot be decoded using Q codec");
  152.     }

  153.     /**
  154.      * Decodes a quoted-printable string into its original form. Escaped characters are converted back to their original
  155.      * representation.
  156.      *
  157.      * @param str
  158.      *            quoted-printable string to convert into its original form
  159.      * @return original string
  160.      * @throws DecoderException
  161.      *             A decoder exception is thrown if a failure condition is encountered during the decode process.
  162.      */
  163.     @Override
  164.     public String decode(final String str) throws DecoderException {
  165.         try {
  166.             return decodeText(str);
  167.         } catch (final UnsupportedEncodingException e) {
  168.             throw new DecoderException(e.getMessage(), e);
  169.         }
  170.     }

  171.     @Override
  172.     protected byte[] doDecoding(final byte[] bytes) throws DecoderException {
  173.         if (bytes == null) {
  174.             return null;
  175.         }
  176.         boolean hasUnderscores = false;
  177.         for (final byte b : bytes) {
  178.             if (b == UNDERSCORE) {
  179.                 hasUnderscores = true;
  180.                 break;
  181.             }
  182.         }
  183.         if (hasUnderscores) {
  184.             final byte[] tmp = new byte[bytes.length];
  185.             for (int i = 0; i < bytes.length; i++) {
  186.                 final byte b = bytes[i];
  187.                 if (b != UNDERSCORE) {
  188.                     tmp[i] = b;
  189.                 } else {
  190.                     tmp[i] = SPACE;
  191.                 }
  192.             }
  193.             return QuotedPrintableCodec.decodeQuotedPrintable(tmp);
  194.         }
  195.         return QuotedPrintableCodec.decodeQuotedPrintable(bytes);
  196.     }

  197.     @Override
  198.     protected byte[] doEncoding(final byte[] bytes) {
  199.         if (bytes == null) {
  200.             return null;
  201.         }
  202.         final byte[] data = QuotedPrintableCodec.encodeQuotedPrintable(PRINTABLE_CHARS, bytes);
  203.         if (this.encodeBlanks) {
  204.             for (int i = 0; i < data.length; i++) {
  205.                 if (data[i] == SPACE) {
  206.                     data[i] = UNDERSCORE;
  207.                 }
  208.             }
  209.         }
  210.         return data;
  211.     }

  212.     /**
  213.      * Encodes an object into its quoted-printable form using the default Charset. Unsafe characters are escaped.
  214.      *
  215.      * @param obj
  216.      *            object to convert to quoted-printable form
  217.      * @return quoted-printable object
  218.      * @throws EncoderException
  219.      *             thrown if a failure condition is encountered during the encoding process.
  220.      */
  221.     @Override
  222.     public Object encode(final Object obj) throws EncoderException {
  223.         if (obj == null) {
  224.             return null;
  225.         }
  226.         if (obj instanceof String) {
  227.             return encode((String) obj);
  228.         }
  229.         throw new EncoderException("Objects of type " + obj.getClass().getName() + " cannot be encoded using Q codec");
  230.     }

  231.     /**
  232.      * Encodes a string into its quoted-printable form using the default Charset. Unsafe characters are escaped.
  233.      *
  234.      * @param sourceStr
  235.      *            string to convert to quoted-printable form
  236.      * @return quoted-printable string
  237.      * @throws EncoderException
  238.      *             thrown if a failure condition is encountered during the encoding process.
  239.      */
  240.     @Override
  241.     public String encode(final String sourceStr) throws EncoderException {
  242.         return encode(sourceStr, getCharset());
  243.     }

  244.     /**
  245.      * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped.
  246.      *
  247.      * @param sourceStr
  248.      *            string to convert to quoted-printable form
  249.      * @param sourceCharset
  250.      *            the Charset for sourceStr
  251.      * @return quoted-printable string
  252.      * @throws EncoderException
  253.      *             thrown if a failure condition is encountered during the encoding process.
  254.      * @since 1.7
  255.      */
  256.     public String encode(final String sourceStr, final Charset sourceCharset) throws EncoderException {
  257.         return encodeText(sourceStr, sourceCharset);
  258.     }

  259.     /**
  260.      * Encodes a string into its quoted-printable form using the specified Charset. Unsafe characters are escaped.
  261.      *
  262.      * @param sourceStr
  263.      *            string to convert to quoted-printable form
  264.      * @param sourceCharset
  265.      *            the Charset for sourceStr
  266.      * @return quoted-printable string
  267.      * @throws EncoderException
  268.      *             thrown if a failure condition is encountered during the encoding process.
  269.      */
  270.     public String encode(final String sourceStr, final String sourceCharset) throws EncoderException {
  271.         try {
  272.             return encodeText(sourceStr, sourceCharset);
  273.         } catch (final UnsupportedCharsetException e) {
  274.             throw new EncoderException(e.getMessage(), e);
  275.         }
  276.     }

  277.     @Override
  278.     protected String getEncoding() {
  279.         return "Q";
  280.     }

  281.     /**
  282.      * Tests if optional transformation of SPACE characters is to be used
  283.      *
  284.      * @return {@code true} if SPACE characters are to be transformed, {@code false} otherwise
  285.      */
  286.     public boolean isEncodeBlanks() {
  287.         return this.encodeBlanks;
  288.     }

  289.     /**
  290.      * Defines whether optional transformation of SPACE characters is to be used
  291.      *
  292.      * @param b
  293.      *            {@code true} if SPACE characters are to be transformed, {@code false} otherwise
  294.      */
  295.     public void setEncodeBlanks(final boolean b) {
  296.         this.encodeBlanks = b;
  297.     }
  298. }