View Javadoc

1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *  http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  
20  package org.apache.geronimo.mail.util;
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.IOException;
24  import java.io.OutputStream;
25  import java.io.UnsupportedEncodingException;
26  
27  import javax.mail.internet.MimeUtility;
28  
29  /**
30   * Encoder for RFC2231 encoded parameters
31   *
32   * RFC2231 string are encoded as
33   *
34   *    charset'language'encoded-text
35   *
36   * and
37   *
38   *    encoded-text = *(char / hexchar)
39   *
40   * where
41   *
42   *    char is any ASCII character in the range 33-126, EXCEPT
43   *    the characters "%" and " ".
44   *
45   *    hexchar is an ASCII "%" followed by two upper case
46   *    hexadecimal digits.
47   */
48  
49  public class RFC2231Encoder implements Encoder
50  {
51      protected final byte[] encodingTable =
52          {
53              (byte)'0', (byte)'1', (byte)'2', (byte)'3', (byte)'4', (byte)'5', (byte)'6', (byte)'7',
54              (byte)'8', (byte)'9', (byte)'A', (byte)'B', (byte)'C', (byte)'D', (byte)'E', (byte)'F'
55          };
56  
57      protected String DEFAULT_SPECIALS = " *'%";
58      protected String specials = DEFAULT_SPECIALS;
59  
60      /*
61       * set up the decoding table.
62       */
63      protected final byte[] decodingTable = new byte[128];
64  
65      protected void initialiseDecodingTable()
66      {
67          for (int i = 0; i < encodingTable.length; i++)
68          {
69              decodingTable[encodingTable[i]] = (byte)i;
70          }
71      }
72  
73      public RFC2231Encoder()
74      {
75          this(null);
76      }
77  
78      public RFC2231Encoder(String specials)
79      {
80          if (specials != null) {
81              this.specials = DEFAULT_SPECIALS + specials;
82          }
83          initialiseDecodingTable();
84      }
85  
86  
87      /**
88       * encode the input data producing an RFC2231 output stream.
89       *
90       * @return the number of bytes produced.
91       */
92      public int encode(byte[] data, int off, int length, OutputStream out) throws IOException {
93  
94          int bytesWritten = 0;
95          for (int i = off; i < (off + length); i++)
96          {
97              int ch = data[i] & 0xff;
98              // character tha must be encoded?  Prefix with a '%' and encode in hex.
99              if (ch <= 32 || ch >= 127 || specials.indexOf(ch) != -1) {
100                 out.write((byte)'%');
101                 out.write(encodingTable[ch >> 4]);
102                 out.write(encodingTable[ch & 0xf]);
103                 bytesWritten += 3;
104             }
105             else {
106                 // add unchanged.
107                 out.write((byte)ch);
108                 bytesWritten++;
109             }
110         }
111 
112         return bytesWritten;
113     }
114 
115 
116     /**
117      * decode the RFC2231 encoded byte data writing it to the given output stream
118      *
119      * @return the number of bytes produced.
120      */
121     public int decode(byte[] data, int off, int length, OutputStream out) throws IOException {
122         int        outLen = 0;
123         int        end = off + length;
124 
125         int i = off;
126         while (i < end)
127         {
128             byte v = data[i++];
129             // a percent is a hex character marker, need to decode a hex value.
130             if (v == '%') {
131                 byte b1 = decodingTable[data[i++]];
132                 byte b2 = decodingTable[data[i++]];
133                 out.write((b1 << 4) | b2);
134             }
135             else {
136                 // copied over unchanged.
137                 out.write(v);
138             }
139             // always just one byte added
140             outLen++;
141         }
142 
143         return outLen;
144     }
145 
146     /**
147      * decode the RFC2231 encoded String data writing it to the given output stream.
148      *
149      * @return the number of bytes produced.
150      */
151     public int decode(String data, OutputStream out) throws IOException
152     {
153         int        length = 0;
154         int        end = data.length();
155 
156         int i = 0;
157         while (i < end)
158         {
159             char v = data.charAt(i++);
160             if (v == '%') {
161                 byte b1 = decodingTable[data.charAt(i++)];
162                 byte b2 = decodingTable[data.charAt(i++)];
163 
164                 out.write((b1 << 4) | b2);
165             }
166             else {
167                 out.write((byte)v);
168             }
169             length++;
170         }
171 
172         return length;
173     }
174 
175 
176     /**
177      * Encode a string as an RFC2231 encoded parameter, using the
178      * given character set and language.
179      *
180      * @param charset  The source character set (the MIME version).
181      * @param language The encoding language.
182      * @param data     The data to encode.
183      *
184      * @return The encoded string.
185      */
186     public String encode(String charset, String language, String data) throws IOException {
187 
188         byte[] bytes = null;
189         try {
190             // the charset we're adding is the MIME-defined name.  We need the java version
191             // in order to extract the bytes.
192             bytes = data.getBytes(MimeUtility.javaCharset(charset));
193         } catch (UnsupportedEncodingException e) {
194             // we have a translation problem here.
195             return null;
196         }
197 
198         StringBuffer result = new StringBuffer();
199 
200         // append the character set, if we have it.
201         if (charset != null) {
202             result.append(charset);
203         }
204         // the field marker is required.
205         result.append("'");
206 
207         // and the same for the language.
208         if (language != null) {
209             result.append(language);
210         }
211         // the field marker is required.
212         result.append("'");
213 
214         // wrap an output stream around our buffer for the decoding
215         OutputStream out = new StringBufferOutputStream(result);
216 
217         // encode the data stream
218         encode(bytes, 0, bytes.length, out);
219 
220         // finis!
221         return result.toString();
222     }
223 
224 
225     /**
226      * Decode an RFC2231 encoded string.
227      *
228      * @param data   The data to decode.
229      *
230      * @return The decoded string.
231      * @exception IOException
232      * @exception UnsupportedEncodingException
233      */
234     public String decode(String data) throws IOException, UnsupportedEncodingException {
235         // get the end of the language field
236         int charsetEnd = data.indexOf('\'');
237         // uh oh, might not be there
238         if (charsetEnd == -1) {
239             throw new IOException("Missing charset in RFC2231 encoded value");
240         }
241 
242         String charset = data.substring(0, charsetEnd);
243 
244         // now pull out the language the same way
245         int languageEnd = data.indexOf('\'', charsetEnd + 1);
246         if (languageEnd == -1) {
247             throw new IOException("Missing language in RFC2231 encoded value");
248         }
249 
250         String language = data.substring(charsetEnd + 1, languageEnd);
251 
252         ByteArrayOutputStream out = new ByteArrayOutputStream(data.length());
253 
254         // decode the data
255         decode(data.substring(languageEnd + 1), out);
256 
257         byte[] bytes = out.toByteArray();
258         // build a new string from this using the java version of the encoded charset.
259         return new String(bytes, 0, bytes.length, MimeUtility.javaCharset(charset));
260     }
261 }