View Javadoc

1   /*
2    * $Source: /usr/cvsroot/melati/melati/src/main/java/org/melati/util/AcceptCharset.java,v $
3    * $Revision: 1.14 $
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     /**
265      * {@inheritDoc}
266      * 
267      * @see java.util.Comparator#compare(T, T)
268      */
269     public final int compare(Object one, Object two) {
270       return compareCharsetAndQValue((CharsetAndQValue) one,
271           (CharsetAndQValue) two);
272     }
273 
274     /**
275      * This default compares according to client requirements.
276      */
277     protected int compareCharsetAndQValue(CharsetAndQValue one,
278         CharsetAndQValue two) {
279       if (one.q == two.q) {
280         return two.position - one.position;
281       } else if (one.q > two.q) {
282         return 1;
283       } else {
284         // assert one.q < two.q : "Only this possibility";
285         return -1;
286       }
287     }
288   }
289 
290   /**
291    * A charset and associated qvalue.
292    */
293   public static class CharsetAndQValue extends TokenAndQValue {
294 
295     /**
296      * Java platform charset or null if this is the wildcard.
297      */
298     Charset charset = null;
299 
300     /**
301      * An integer that is less for more preferable instances from server point
302      * of view.
303      * <p>
304      * It might be the index of the array of supported server preferences or
305      * <code>Integer.MAX_VALUE</code>.
306      */
307     public int serverPreferability = Integer.MAX_VALUE;
308 
309     /**
310      * An integer that indicates where this charset was explicitly specified in
311      * Accept-Charset relative to others.
312      * <p>
313      * This increases left to right so it could be the actual position but need
314      * not be.
315      * <p>
316      * It is <code>Integer.MAX_VALUE</code> if the charset was not explicitly
317      * specified, regardless of the position of any wildcard.
318      */
319     public int position = Integer.MAX_VALUE;
320 
321     /**
322      * Create an instance and initialize it by reading a tokenizer.
323      * 
324      * @param t
325      *          tokenizer
326      */
327     public CharsetAndQValue(Tokenizer t) {
328       super(t);
329       if (!isWildcard()) {
330         charset = Charset.forName(token);
331       }
332     }
333 
334     /**
335      * Creates an instance for the given charset and q value.
336      */
337     public CharsetAndQValue(Charset charset, float q) {
338       super();
339       this.token = charset.name();
340       this.charset = charset;
341       this.q = q;
342     }
343 
344     /**
345      * Creates an instance for the given <code>Charset</code> using the q value
346      * from a parsed wildcard Accept-Charset field.
347      */
348     public CharsetAndQValue(Charset charset, CharsetAndQValue wildcard) {
349       this(charset, wildcard.q);
350     }
351 
352     /**
353      * @return whether the given charset token is an asterix
354      */
355     public boolean isWildcard() {
356       return token.equals("*");
357     }
358 
359   }
360 
361 }