View Javadoc

1   /*
2    * $Source: /usr/cvsroot/melati/melati/src/main/java/org/melati/util/AcceptCharset.java,v $
3    * $Revision: 1.11 $
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    private boolean debug = true;
68    
69    /**
70     * Charsets supported by the jvm and accepted by the client 
71     * or preferred by the server.
72     */
73    protected HashMap supportedAcceptedOrPreferred = new HashMap();
74  
75    /**
76     * Client wildcard * specification if any.
77     */
78    CharsetAndQValue wildcard = null;
79  
80    /**
81     * The name of the first server preferred charset that is not acceptable
82     * to the client but is supported by the jvm.
83     * <p>
84     * This may be worth checking by the caller if there are no acceptable
85     * charsets, or the caller can respond with a 406 error code.
86     * <p>
87     * Note that if there is a wildcard then this will be null.
88     */
89    String firstOther = null;
90  
91    /**
92     * Create an instance from the Accept-Charset header field values and
93     * a set of server preferred charset names.
94     * <p>
95     * The field values might have appeared in a single Accept-Charset header
96     * or in several that were concatenated with comma separator in order.
97     * This concatenation is often done for the caller, by a servlet
98     * container or something, but it must be done.
99     * <p>
100    * <code>null</code> is taken to mean there were no Accept-Charset header
101    * fields.
102    * <p>
103    * If a client supported charset is unsupported by the JVM it is ignored.
104    * If the caller wants to ensure that there are none then it must check 
105    * for itself.
106    * <p>
107    * If the same charset is specified more than once (perhaps under
108    * different names or aliases) then the first occurrence is significant.
109    * <p>
110    * The server preferences provides a list of charsets used if there is 
111    * a wildcard specification.
112    * 
113    * This class does not currently try other available charsets so
114    * to avoid 406 errors to reasonable clients, enough reasonable charsets
115    * must be listed in serverPreferences.
116    */
117   public AcceptCharset(String values, List serverPreference) throws HttpHeaderException {
118     super(values);
119     if (debug) System.err.println("values:" + values);
120     if (debug) System.err.println("serverPreference:" + serverPreference);
121     int position = 0;
122     for (CharsetAndQValueIterator i = charsetAndQValueIterator(); 
123          i.hasNext();) {
124       CharsetAndQValue c = i.nextCharsetAndQValue();
125       if (c.isWildcard()) {
126         wildcard = c;
127         if (debug) System.err.println("Tested 1");
128       } else {
129         try {
130           String n = c.charset.name();
131           if (supportedAcceptedOrPreferred.get(c) == null) {
132             supportedAcceptedOrPreferred.put(n, c);
133             c.position = position++;
134              if (debug) System.err.println("Tested 2:" + n);
135           }
136         }
137         catch (UnsupportedCharsetException uce) {
138           // Continue with next one
139           uce = null; // shut PMD up          
140         }
141       }
142     }
143     if (wildcard == null) {
144       Charset latin1 = Charset.forName("ISO-8859-1");
145       if (supportedAcceptedOrPreferred.get(latin1.name()) == null) {
146         CharsetAndQValue c = new CharsetAndQValue(latin1, 1.0f);
147         supportedAcceptedOrPreferred.put(latin1.name(), c);
148         if (debug) System.err.println("Tested 3 + " + latin1.name());
149       }
150     }
151     for (int i = 0; i < serverPreference.size(); i++) {
152       try {
153         Charset charset = Charset.forName((String)serverPreference.get(i));
154         CharsetAndQValue acceptable =
155             (CharsetAndQValue)supportedAcceptedOrPreferred.get(charset.name());
156         if (acceptable == null) {
157           if (wildcard == null) {
158             if (firstOther == null) {
159               firstOther = charset.name();
160               if (debug) System.err.println("Tested 4" + charset.name());
161             }
162           } else {
163             CharsetAndQValue c = new CharsetAndQValue(charset, wildcard);
164             supportedAcceptedOrPreferred.put(charset.name(), c);
165             c.serverPreferability = i;
166             if (debug) System.err.println("Tested 5:" + charset.name());
167           }
168         } else {
169           supportedAcceptedOrPreferred.put(charset.name(), acceptable);
170           if (i < acceptable.serverPreferability) {
171             acceptable.serverPreferability = i;
172             if (debug) System.err.println("Tested 6");
173           }
174         }
175       }
176       catch (UnsupportedCharsetException uce) {
177         // Ignore this charset
178         // if (debug) System.err.println("Tested 7");
179         uce = null; // shut PMD up          
180       }
181     }
182   }
183 
184   /**
185    * Enumeration of {@link AcceptCharset.CharsetAndQValue}.
186    */
187   public class CharsetAndQValueIterator extends TokenAndQValueIterator {
188 
189     /**
190      * @return the next one
191      */
192     public CharsetAndQValue nextCharsetAndQValue() throws HttpHeaderException {
193       // if (debug) System.err.println("Tested 7a");
194       return (CharsetAndQValue)AcceptCharset.this.nextTokenAndQValue();
195     }
196   }
197 
198   /**
199    * {@inheritDoc}
200    * @see org.melati.util.HttpHeader#nextTokenAndQValue()
201    */
202   public TokenAndQValue nextTokenAndQValue() throws HttpHeaderException {
203     // if (debug) System.err.println("Tested 7b");
204     return new CharsetAndQValue(tokenizer);
205   }
206 
207   /**
208    * Factory method to create and return the next
209    * {@link HttpHeader.TokenAndQValue}.
210    * @return a new Iterator
211    */
212   public CharsetAndQValueIterator charsetAndQValueIterator() {
213     // if (debug) System.err.println("Tested 7c");
214     return new CharsetAndQValueIterator();
215   }
216 
217   private final Comparator clientComparator = new Comparator();
218 
219   /**
220    * @return the first supported charset that is also acceptable to the
221    * client in order of client preference.
222    *  
223    */
224   public String clientChoice() {
225     // if (debug) System.err.println("Tested 8");
226     return choice(clientComparator);
227   }
228 
229   private final Comparator serverComparator = new Comparator() {
230       protected int compareCharsetAndQValue(CharsetAndQValue one,
231                                    CharsetAndQValue two) {
232         int result;
233         result = two.serverPreferability - one.serverPreferability;
234         if (result == 0) {
235           result = super.compareCharsetAndQValue(one, two);
236           // if (debug) System.err.println("Tested 9");
237         }
238         return result;   
239       }
240     };
241 
242   /**
243    * @return the first supported charset also acceptable to the client
244    * in order of server preference.
245    */
246   public String serverChoice() {
247     // if (debug) System.err.println("Tested 10");
248     return choice(serverComparator);
249   }
250 
251   /**
252    * If there is none, return null, and the caller can either use an
253    * unacceptable character set or generate a 406 error.
254    *
255    * see #firstOther
256    * @return the first supported charset also acceptable to the client
257    * in order defined by the given {@link Comparator}
258    */
259   public String choice(Comparator comparator) {
260     CharsetAndQValue best = null;
261     for (Iterator i = supportedAcceptedOrPreferred.values().iterator(); i.hasNext();) {
262       CharsetAndQValue c = (CharsetAndQValue)i.next();
263       if (best == null || comparator.compare(c, best) > 0) {
264         best = c;
265         // if (debug) System.err.println("Tested 11");
266       }
267     }
268     if (best == null || best.q == 0.0) {
269       // if (debug) System.err.println("Tested 12");
270       return null;
271     } else {
272       // if (debug) System.err.println("Tested 13");
273       return best.charset.name();
274     }
275   }
276 
277   /**
278    * Comparator for comparing {@link AcceptCharset.CharsetAndQValue} objects.
279    */
280   protected static class Comparator implements java.util.Comparator {
281     
282     /**
283      * {@inheritDoc}
284      * @see java.util.Comparator#compare(T, T)
285      */
286     public final int compare(Object one, Object two) {
287       // if (debug) System.err.println("Tested 14");
288       return compareCharsetAndQValue((CharsetAndQValue)one, (CharsetAndQValue)two);
289     }
290     
291     /**
292      * This default compares according to client requirements.
293      */
294     protected int compareCharsetAndQValue(CharsetAndQValue one, CharsetAndQValue two) {
295       if (one.q == two.q) {
296         // if (debug) System.err.println("Tested 15");
297         return two.position - one.position;
298       } else if (one.q > two.q) {
299         // if (debug) System.err.println("Tested 16");
300         return 1;
301       } else {
302         // if (debug) System.err.println("Tested 17");
303         //assert one.q < two.q : "Only this possibility";
304         return -1;
305       }
306     }
307   }
308   
309   /**
310    * A charset and associated qvalue.
311    */
312   public static class CharsetAndQValue extends TokenAndQValue {
313 
314     /**
315      * Java platform charset or null if this is the wildcard.
316      */
317     Charset charset = null;
318 
319     /**
320      * An integer that is less for more preferable instances from
321      * server point of view.
322      * <p>
323      * It might be the index of the array of supported server
324      * preferences or <code>Integer.MAX_VALUE</code>.
325      */
326     public int serverPreferability = Integer.MAX_VALUE;
327     
328     /**
329      * An integer that indicates where this charset was explicitly
330      * specified in Accept-Charset relative to others.
331      * <p>
332      * This increases left to right so it could be the actual position
333      * but need not be.
334      * <p>
335      * It is <code>Integer.MAX_VALUE</code> if the charset was not
336      * explicitly specified, regardless of the position of any wildcard.
337      */
338     public int position = Integer.MAX_VALUE;
339     
340     /**
341      * Create an instance and initialize it by reading a tokenizer.
342      * @param t tokenizer
343      */
344     public CharsetAndQValue(Tokenizer t) throws HttpHeaderException {
345       super(t);
346       if (! isWildcard()) {
347         try {
348           charset = Charset.forName(token);
349         } catch (UnsupportedCharsetException e) {
350           throw new HttpHeaderException("Unsupported Character set:", e);
351         }
352       }
353     }
354 
355     /**
356      * Creates an instance for the given charset and q value.
357      */
358     public CharsetAndQValue(Charset charset, float q) {
359       super();
360       this.token = charset.name();
361       this.charset = charset;        
362       this.q = q;
363       // if (debug) System.err.println("Tested 19");
364     }
365 
366     /**
367      * Creates an instance for the given <code>Charset</code>
368      * using the q value from a parsed wildcard Accept-Charset field.
369      */
370     public CharsetAndQValue(Charset charset, CharsetAndQValue wildcard) {
371       this(charset, wildcard.q);
372       // if (debug) System.err.println("Tested 20");
373     }
374 
375     /**
376      * @return whether the given charset token is an asterix
377      */
378     public boolean isWildcard() {
379       // if (debug) System.err.println("Tested 20a");
380       return token.equals("*");
381     }
382 
383   }
384 
385 } 
386 
387