View Javadoc
1   /*
2    * $Source$
3    * $Revision$
4    *
5    * Copyright (C) 2003 Jim Wright
6    *
7    * Part of Melati (http://melati.org), a framework for the rapid
8    * development of clean, maintainable web applications.
9    *
10   * Melati is free software; Permission is granted to copy, distribute
11   * and/or modify this software under the terms either:
12   *
13   * a) the GNU General Public License as published by the Free Software
14   *    Foundation; either version 2 of the License, or (at your option)
15   *    any later version,
16   *
17   *    or
18   *
19   * b) any version of the Melati Software License, as published
20   *    at http://melati.org
21   *
22   * You should have received a copy of the GNU General Public License and
23   * the Melati Software License along with this program;
24   * if not, write to the Free Software Foundation, Inc.,
25   * 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA to obtain the
26   * GNU General Public License and visit http://melati.org to obtain the
27   * Melati Software License.
28   *
29   * Feel free to contact the Developers of Melati (http://melati.org),
30   * if you would like to work out a different arrangement than the options
31   * outlined here.  It is our intention to allow Melati to be used by as
32   * wide an audience as possible.
33   *
34   * This program is distributed in the hope that it will be useful,
35   * but WITHOUT ANY WARRANTY; without even the implied warranty of
36   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
37   * GNU General Public License for more details.
38   *
39   * Contact details for copyright holder:
40   *
41   *     Jim Wright <jimw At paneris.org>
42   *     Bohemian Enterprise
43   *     Predmerice nad Jizerou 77
44   *     294 74
45   *     Mlada Boleslav
46   *     Czech Republic
47   */
48  
49  package org.melati.util;
50  
51  import java.nio.charset.Charset;
52  import java.nio.charset.UnsupportedCharsetException;
53  import java.util.HashMap;
54  import java.util.Iterator;
55  import java.util.List;
56  
57  /**
58   * Representation of the Accept-Charset header fields.
59   * <p>
60   * Provides features for choosing a charset according to client or server
61   * preferences.
62   * 
63   * @author jimw At paneris.org
64   */
65  public class AcceptCharset extends HttpHeader {
66  
67  
68    /**
69     * Charsets supported by the jvm and accepted by the client or preferred by
70     * the server.
71     */
72    protected HashMap<String, CharsetAndQValue> supportedAcceptedOrPreferred = new HashMap<String, CharsetAndQValue>();
73  
74    /**
75     * Client wildcard * specification if any.
76     */
77    CharsetAndQValue wildcard = null;
78  
79    /**
80     * The name of the first server preferred charset that is not acceptable to
81     * the client but is supported by the jvm.
82     * <p>
83     * This may be worth checking by the caller if there are no acceptable
84     * charsets, or the caller can respond with a 406 error code.
85     * <p>
86     * Note that if there is a wildcard then this will be null.
87     */
88    String firstOther = null;
89  
90    /**
91     * Create an instance from the Accept-Charset header field values and a set of
92     * server preferred charset names.
93     * <p>
94     * The field values might have appeared in a single Accept-Charset header or
95     * in several that were concatenated with comma separator in order. This
96     * concatenation is often done for the caller, by a servlet container or
97     * something, but it must be done.
98     * <p>
99     * <code>null</code> is taken to mean there were no Accept-Charset header
100    * fields.
101    * <p>
102    * If a client supported charset is unsupported by the JVM it is ignored. If
103    * the caller wants to ensure that there are none then it must check for
104    * itself.
105    * <p>
106    * If the same charset is specified more than once (perhaps under different
107    * names or aliases) then the first occurrence is significant.
108    * <p>
109    * The server preferences provides a list of charsets used if there is a
110    * wildcard specification.
111    * 
112    * This class does not currently try other available charsets so to avoid 406
113    * errors to reasonable clients, enough reasonable charsets must be listed in
114    * serverPreferences.
115    */
116   public AcceptCharset(String values, List<String> serverPreference) {
117     super(values);
118     int position = 0;
119     for (CharsetAndQValueIterator i = charsetAndQValueIterator(); i.hasNext();) {
120       try {
121         CharsetAndQValue c = i.nextCharsetAndQValue();
122         if (c.isWildcard()) {
123           wildcard = c;
124         } else {
125           String n = c.charset.name();
126           if (supportedAcceptedOrPreferred.get(c) == null) {
127             supportedAcceptedOrPreferred.put(n, c);
128             c.position = position++;
129           }
130         }
131       } catch (UnsupportedCharsetException uce) {
132         // Continue with next one
133         uce = null; // shut PMD up
134       }
135     }
136     if (wildcard == null) {
137       Charset latin1 = Charset.forName("ISO-8859-1");
138       if (supportedAcceptedOrPreferred.get(latin1.name()) == null) {
139         CharsetAndQValue c = new CharsetAndQValue(latin1, 1.0f);
140         supportedAcceptedOrPreferred.put(latin1.name(), c);
141       }
142     }
143     for (int i = 0; i < serverPreference.size(); i++) {
144       try {
145         Charset charset = Charset.forName(serverPreference.get(i));
146         CharsetAndQValue acceptable = (CharsetAndQValue) supportedAcceptedOrPreferred
147             .get(charset.name());
148         if (acceptable == null) {
149           if (wildcard == null) {
150             if (firstOther == null) {
151               firstOther = charset.name();
152             }
153           } else {
154             CharsetAndQValue c = new CharsetAndQValue(charset, wildcard);
155             supportedAcceptedOrPreferred.put(charset.name(), c);
156             c.serverPreferability = i;
157           }
158         } else {
159           supportedAcceptedOrPreferred.put(charset.name(), acceptable);
160           if (i < acceptable.serverPreferability) {
161             acceptable.serverPreferability = i;
162           }
163         }
164       } catch (UnsupportedCharsetException uce) {
165         // Ignore this charset, go on to next
166         uce = null; // shut PMD up
167       }
168     }
169   }
170 
171   /**
172    * Enumeration of {@link AcceptCharset.CharsetAndQValue}.
173    */
174   public class CharsetAndQValueIterator extends TokenAndQValueIterator {
175 
176     /**
177      * @return the next one
178      */
179     public CharsetAndQValue nextCharsetAndQValue() throws HttpHeaderException {
180       return (CharsetAndQValue) AcceptCharset.this.nextTokenAndQValue();
181     }
182   }
183 
184   /**
185    * {@inheritDoc}
186    * 
187    * @see org.melati.util.HttpHeader#nextTokenAndQValue()
188    */
189   public TokenAndQValue nextTokenAndQValue() {
190     return new CharsetAndQValue(tokenizer);
191   }
192 
193   /**
194    * Factory method to create and return the next
195    * {@link HttpHeader.TokenAndQValue}.
196    * 
197    * @return a new Iterator
198    */
199   public CharsetAndQValueIterator charsetAndQValueIterator() {
200     return new CharsetAndQValueIterator();
201   }
202 
203   private final Comparator<CharsetAndQValue> clientComparator = new Comparator<CharsetAndQValue>();
204 
205   /**
206    * @return the first supported charset that is also acceptable to the client
207    *         in order of client preference.
208    * 
209    */
210   public String clientChoice() {
211     return choice(clientComparator);
212   }
213 
214   private final Comparator<CharsetAndQValue> serverComparator = new Comparator<CharsetAndQValue>() {
215     protected int compareCharsetAndQValue(CharsetAndQValue one,
216         CharsetAndQValue two) {
217       int result;
218       result = two.serverPreferability - one.serverPreferability;
219       if (result == 0) {
220         result = super.compareCharsetAndQValue(one, two);
221       }
222       return result;
223     }
224   };
225 
226   /**
227    * @return the first supported charset also acceptable to the client in order
228    *         of server preference.
229    */
230   public String serverChoice() {
231     return choice(serverComparator);
232   }
233 
234   /**
235    * If there is none, return null, and the caller can either use an
236    * unacceptable character set or generate a 406 error.
237    * 
238    * see #firstOther
239    * 
240    * @return the first supported charset also acceptable to the client in order
241    *         defined by the given {@link Comparator}
242    */
243   public String choice(Comparator<CharsetAndQValue> comparator) {
244     CharsetAndQValue best = null;
245     for (Iterator<CharsetAndQValue> i = supportedAcceptedOrPreferred.values()
246         .iterator(); i.hasNext();) {
247       CharsetAndQValue c = (CharsetAndQValue) i.next();
248       if (best == null || comparator.compare(c, best) > 0) {
249         best = c;
250       }
251     }
252     if (best == null || best.q == 0.0) {
253       return null;
254     } else {
255       return best.charset.name();
256     }
257   }
258 
259   /**
260    * Comparator for comparing {@link AcceptCharset.CharsetAndQValue} objects.
261    */
262   protected static class Comparator<T> implements java.util.Comparator<T> {
263 
264     @Override
265     public final int compare(Object one, Object two) {
266       return compareCharsetAndQValue((CharsetAndQValue) one,
267           (CharsetAndQValue) two);
268     }
269 
270     /**
271      * This default compares according to client requirements.
272      */
273     protected int compareCharsetAndQValue(CharsetAndQValue one,
274         CharsetAndQValue two) {
275       if (one.q == two.q) {
276         return two.position - one.position;
277       } else if (one.q > two.q) {
278         return 1;
279       } else {
280         // assert one.q < two.q : "Only this possibility";
281         return -1;
282       }
283     }
284   }
285 
286   /**
287    * A charset and associated qvalue.
288    */
289   public static class CharsetAndQValue extends TokenAndQValue {
290 
291     /**
292      * Java platform charset or null if this is the wildcard.
293      */
294     Charset charset = null;
295 
296     /**
297      * An integer that is less for more preferable instances from server point
298      * of view.
299      * <p>
300      * It might be the index of the array of supported server preferences or
301      * <code>Integer.MAX_VALUE</code>.
302      */
303     public int serverPreferability = Integer.MAX_VALUE;
304 
305     /**
306      * An integer that indicates where this charset was explicitly specified in
307      * Accept-Charset relative to others.
308      * <p>
309      * This increases left to right so it could be the actual position but need
310      * not be.
311      * <p>
312      * It is <code>Integer.MAX_VALUE</code> if the charset was not explicitly
313      * specified, regardless of the position of any wildcard.
314      */
315     public int position = Integer.MAX_VALUE;
316 
317     /**
318      * Create an instance and initialize it by reading a tokenizer.
319      * 
320      * @param t
321      *          tokenizer
322      */
323     public CharsetAndQValue(Tokenizer t) {
324       super(t);
325       if (!isWildcard()) {
326         charset = Charset.forName(token);
327       }
328     }
329 
330     /**
331      * Creates an instance for the given charset and q value.
332      */
333     public CharsetAndQValue(Charset charset, float q) {
334       super();
335       this.token = charset.name();
336       this.charset = charset;
337       this.q = q;
338     }
339 
340     /**
341      * Creates an instance for the given <code>Charset</code> using the q value
342      * from a parsed wildcard Accept-Charset field.
343      */
344     public CharsetAndQValue(Charset charset, CharsetAndQValue wildcard) {
345       this(charset, wildcard.q);
346     }
347 
348     /**
349      * @return whether the given charset token is an asterix
350      */
351     public boolean isWildcard() {
352       return token.equals("*");
353     }
354 
355   }
356 
357 }