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 }