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 }