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 javax.mail.internet;
21  
22  import java.io.ByteArrayOutputStream;
23  import java.io.UnsupportedEncodingException;
24  import java.util.ArrayList;// Represents lists in things like
25  import java.util.Collections;
26  import java.util.Enumeration;
27  import java.util.HashMap;
28  import java.util.Iterator;
29  import java.util.List;
30  import java.util.Map;
31  import java.util.StringTokenizer;
32  
33  import org.apache.geronimo.mail.util.ASCIIUtil;
34  import org.apache.geronimo.mail.util.RFC2231Encoder;
35  import org.apache.geronimo.mail.util.SessionUtil;
36  
37  // Content-Type: text/plain;charset=klingon
38  //
39  // The ;charset=klingon is the parameter list, may have more of them with ';'
40  
41  /**
42   * @version $Rev: 669445 $ $Date: 2008-06-19 06:48:18 -0400 (Thu, 19 Jun 2008) $
43   */
44  public class ParameterList {
45      private static final String MIME_ENCODEPARAMETERS = "mail.mime.encodeparameters";
46      private static final String MIME_DECODEPARAMETERS = "mail.mime.decodeparameters";
47      private static final String MIME_DECODEPARAMETERS_STRICT = "mail.mime.decodeparameters.strict";
48  
49      private static final int HEADER_SIZE_LIMIT = 76;
50  
51      private Map _parameters = new HashMap();
52  
53      private boolean encodeParameters = false;
54      private boolean decodeParameters = false;
55      private boolean decodeParametersStrict = false;
56  
57      public ParameterList() {
58          // figure out how parameter handling is to be performed.
59          getInitialProperties();
60      }
61  
62      public ParameterList(String list) throws ParseException {
63          // figure out how parameter handling is to be performed.
64          getInitialProperties();
65          // get a token parser for the type information
66          HeaderTokenizer tokenizer = new HeaderTokenizer(list, HeaderTokenizer.MIME);
67          while (true) {
68              HeaderTokenizer.Token token = tokenizer.next();
69  
70              switch (token.getType()) {
71                  // the EOF token terminates parsing.
72                  case HeaderTokenizer.Token.EOF:
73                      return;
74  
75                  // each new parameter is separated by a semicolon, including the first, which separates
76                  // the parameters from the main part of the header.
77                  case ';':
78                      // the next token needs to be a parameter name
79                      token = tokenizer.next();
80                      // allow a trailing semicolon on the parameters.
81                      if (token.getType() == HeaderTokenizer.Token.EOF) {
82                          return;
83                      }
84  
85                      if (token.getType() != HeaderTokenizer.Token.ATOM) {
86                          throw new ParseException("Invalid parameter name: " + token.getValue());
87                      }
88  
89                      // get the parameter name as a lower case version for better mapping.
90                      String name = token.getValue().toLowerCase();
91  
92                      token = tokenizer.next();
93  
94                      // parameters are name=value, so we must have the "=" here.
95                      if (token.getType() != '=') {
96                          throw new ParseException("Missing '='");
97                      }
98  
99                      // now the value, which may be an atom or a literal
100                     token = tokenizer.next();
101 
102                     if (token.getType() != HeaderTokenizer.Token.ATOM && token.getType() != HeaderTokenizer.Token.QUOTEDSTRING) {
103                         throw new ParseException("Invalid parameter value: " + token.getValue());
104                     }
105 
106                     String value = token.getValue();
107                     String decodedValue = null;
108 
109                     // we might have to do some additional decoding.  A name that ends with "*"
110                     // is marked as being encoded, so if requested, we decode the value.
111                     if (decodeParameters && name.endsWith("*")) {
112                         // the name needs to be pruned of the marker, and we need to decode the value.
113                         name = name.substring(0, name.length() - 1);
114                         // get a new decoder
115                         RFC2231Encoder decoder = new RFC2231Encoder(HeaderTokenizer.MIME);
116 
117                         try {
118                             // decode the value
119                             decodedValue = decoder.decode(value);
120                         } catch (Exception e) {
121                             // if we're doing things strictly, then raise a parsing exception for errors.
122                             // otherwise, leave the value in its current state.
123                             if (decodeParametersStrict) {
124                                 throw new ParseException("Invalid RFC2231 encoded parameter");
125                             }
126                         }
127                         _parameters.put(name, new ParameterValue(name, decodedValue, value));
128                     }
129                     else {
130                         _parameters.put(name, new ParameterValue(name, value));
131                     }
132 
133                     break;
134 
135                 default:
136                     throw new ParseException("Missing ';'");
137 
138             }
139         }
140     }
141 
142     /**
143      * Get the initial parameters that control parsing and values.
144      * These parameters are controlled by System properties.
145      */
146     private void getInitialProperties() {
147         decodeParameters = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS, false);
148         decodeParametersStrict = SessionUtil.getBooleanProperty(MIME_DECODEPARAMETERS_STRICT, false);
149         encodeParameters = SessionUtil.getBooleanProperty(MIME_ENCODEPARAMETERS, true);
150     }
151 
152     public int size() {
153         return _parameters.size();
154     }
155 
156     public String get(String name) {
157         ParameterValue value = (ParameterValue)_parameters.get(name.toLowerCase());
158         if (value != null) {
159             return value.value;
160         }
161         return null;
162     }
163 
164     public void set(String name, String value) {
165         name = name.toLowerCase();
166         _parameters.put(name, new ParameterValue(name, value));
167     }
168 
169     public void set(String name, String value, String charset) {
170         name = name.toLowerCase();
171         // only encode if told to and this contains non-ASCII charactes.
172         if (encodeParameters && !ASCIIUtil.isAscii(value)) {
173             ByteArrayOutputStream out = new ByteArrayOutputStream();
174 
175             try {
176                 RFC2231Encoder encoder = new RFC2231Encoder(HeaderTokenizer.MIME);
177 
178                 // extract the bytes using the given character set and encode
179                 byte[] valueBytes = value.getBytes(MimeUtility.javaCharset(charset));
180 
181                 // the string format is charset''data
182                 out.write(charset.getBytes());
183                 out.write('\'');
184                 out.write('\'');
185                 encoder.encode(valueBytes, 0, valueBytes.length, out);
186 
187                 // default in case there is an exception
188                 _parameters.put(name, new ParameterValue(name, value, new String(out.toByteArray())));
189                 return;
190 
191             } catch (Exception e) {
192                 // just fall through and set the value directly if there is an error
193             }
194         }
195         // default in case there is an exception
196         _parameters.put(name, new ParameterValue(name, value));
197     }
198 
199     public void remove(String name) {
200         _parameters.remove(name);
201     }
202 
203     public Enumeration getNames() {
204         return Collections.enumeration(_parameters.keySet());
205     }
206 
207     public String toString() {
208         // we need to perform folding, but out starting point is 0.
209         return toString(0);
210     }
211 
212     public String toString(int used) {
213         StringBuffer stringValue = new StringBuffer();
214 
215         Iterator values = _parameters.values().iterator();
216 
217         while (values.hasNext()) {
218             ParameterValue parm = (ParameterValue)values.next();
219             // get the values we're going to encode in here.
220             String name = parm.getEncodedName();
221             String value = parm.toString();
222 
223             // add the semicolon separator.  We also add a blank so that folding/unfolding rules can be used.
224             stringValue.append("; ");
225             used += 2;
226 
227             // too big for the current header line?
228             if ((used + name.length() + value.length() + 1) > HEADER_SIZE_LIMIT) {
229                 // and a CRLF-combo combo.
230                 stringValue.append("\r\n\t");
231                 // reset the counter for a fresh line
232                 // note we use use 8 because we're using a rather than a blank 
233                 used = 8;
234             }
235             // now add the keyword/value pair.
236             stringValue.append(name);
237             stringValue.append("=");
238 
239             used += name.length() + 1;
240 
241             // we're not out of the woods yet.  It is possible that the keyword/value pair by itself might
242             // be too long for a single line.  If that's the case, the we need to fold the value, if possible
243             if (used + value.length() > HEADER_SIZE_LIMIT) {
244                 String foldedValue = MimeUtility.fold(used, value);
245 
246                 stringValue.append(foldedValue);
247 
248                 // now we need to sort out how much of the current line is in use.
249                 int lastLineBreak = foldedValue.lastIndexOf('\n');
250 
251                 if (lastLineBreak != -1) {
252                     used = foldedValue.length() - lastLineBreak + 1;
253                 }
254                 else {
255                     used += foldedValue.length();
256                 }
257             }
258             else {
259                 // no folding required, just append.
260                 stringValue.append(value);
261                 used += value.length();
262             }
263         }
264 
265         return stringValue.toString();
266     }
267 
268 
269     /**
270      * Utility class for representing parameter values in the list.
271      */
272     class ParameterValue {
273         public String name;              // the name of the parameter
274         public String value;             // the original set value
275         public String encodedValue;      // an encoded value, if encoding is requested.
276 
277         public ParameterValue(String name, String value) {
278             this.name = name;
279             this.value = value;
280             this.encodedValue = null;
281         }
282 
283         public ParameterValue(String name, String value, String encodedValue) {
284             this.name = name;
285             this.value = value;
286             this.encodedValue = encodedValue;
287         }
288 
289         public String toString() {
290             if (encodedValue != null) {
291                 return MimeUtility.quote(encodedValue, HeaderTokenizer.MIME);
292             }
293             return MimeUtility.quote(value, HeaderTokenizer.MIME);
294         }
295 
296         public String getEncodedName() {
297             if (encodedValue != null) {
298                 return name + "*";
299             }
300             return name;
301         }
302     }
303 }