Classes in this File | Line Coverage | Branch Coverage | Complexity | ||||
HttpHeader |
|
| 2.066666666666667;2.067 | ||||
HttpHeader$FieldIterator |
|
| 2.066666666666667;2.067 | ||||
HttpHeader$TokenAndQValue |
|
| 2.066666666666667;2.067 | ||||
HttpHeader$TokenAndQValueIterator |
|
| 2.066666666666667;2.067 | ||||
HttpHeader$Tokenizer |
|
| 2.066666666666667;2.067 | ||||
HttpHeader$WordIterator |
|
| 2.066666666666667;2.067 |
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.io.StreamTokenizer; | |
52 | import java.io.StringReader; | |
53 | import java.io.IOException; | |
54 | import java.util.Iterator; | |
55 | import java.util.Enumeration; | |
56 | ||
57 | /** | |
58 | * Representation of occurrences of an HTTP header field. | |
59 | * <p> | |
60 | * These are defined in RFC 2616 and have the same general form as in | |
61 | * RFC 822 section 3.1. | |
62 | * <P> | |
63 | * We generally assume that all continuation lines and occurrences in | |
64 | * a message are concatenated with comma separators. | |
65 | * | |
66 | * @author Jim Wright | |
67 | */ | |
68 | public class HttpHeader { | |
69 | ||
70 | /** | |
71 | * Instance of inner {@link Tokenizer}. | |
72 | */ | |
73 | protected Tokenizer tokenizer; | |
74 | ||
75 | /** | |
76 | * Create an instance representing the given comma separated fields. | |
77 | */ | |
78 | 912 | public HttpHeader(String values) { |
79 | 912 | if(values == null) |
80 | 566 | values= ""; |
81 | 912 | tokenizer = new Tokenizer(values); |
82 | 911 | } |
83 | ||
84 | /** | |
85 | * Abstract enumeration of fields. | |
86 | * <p> | |
87 | * Subtypes decide what type of token to return and how | |
88 | * to represent it. | |
89 | * <p> | |
90 | * This class serves to remove doubts about whether we should and can | |
91 | * implement <code>Iterator</code> or <code>Enumeration</code> and | |
92 | * proves itself unnecessary ;-). But we can factor stuff out and | |
93 | * re-use it later. | |
94 | * <p> | |
95 | * Actually, it also removes the need to think about exceptions in | |
96 | * subtypes. | |
97 | */ | |
98 | @SuppressWarnings("rawtypes") | |
99 | 911 | public abstract class FieldIterator implements Iterator<Object>, Enumeration{ |
100 | ||
101 | /** | |
102 | * {@inheritDoc} | |
103 | * @see java.util.Enumeration#hasMoreElements() | |
104 | */ | |
105 | public final boolean hasMoreElements() { | |
106 | 1314 | return hasNext(); |
107 | } | |
108 | ||
109 | /** | |
110 | * {@inheritDoc} | |
111 | * @see java.util.Enumeration#nextElement() | |
112 | */ | |
113 | public final Object nextElement() { | |
114 | 985 | return next(); |
115 | } | |
116 | ||
117 | /** | |
118 | * {@inheritDoc} | |
119 | * @see java.util.Iterator#hasNext() | |
120 | * @see #next() | |
121 | */ | |
122 | public final boolean hasNext() { | |
123 | 1907 | return tokenizer.ttype != StreamTokenizer.TT_EOF; |
124 | } | |
125 | ||
126 | /** | |
127 | * {@inheritDoc} | |
128 | * @see java.util.Iterator#remove() | |
129 | */ | |
130 | public void remove() throws UnsupportedOperationException { | |
131 | 1 | throw new UnsupportedOperationException("Cannot remove tokens from the HTTP header"); |
132 | } | |
133 | ||
134 | /** | |
135 | * Return the next element or an exception. | |
136 | * | |
137 | * @return An exception if an object of the anticipated type cannot be returned | |
138 | */ | |
139 | public Object next() { | |
140 | try { | |
141 | 986 | return nextToken(); |
142 | } | |
143 | 1 | catch (HttpHeaderException e) { |
144 | 1 | return e; |
145 | } | |
146 | } | |
147 | ||
148 | /** | |
149 | * @return the next token or throws an exception | |
150 | */ | |
151 | public abstract Object nextToken() throws HttpHeaderException; | |
152 | ||
153 | } | |
154 | ||
155 | /** | |
156 | * Iteration over {@link HttpHeader.TokenAndQValue}s. | |
157 | */ | |
158 | 328 | public class WordIterator extends FieldIterator { |
159 | ||
160 | /** | |
161 | * @return the next word | |
162 | */ | |
163 | public String nextWord() throws HttpHeaderException { | |
164 | 984 | String result = tokenizer.readWord(); |
165 | 984 | tokenizer.skipAnyCommaSeparator(); |
166 | 984 | return result; |
167 | } | |
168 | ||
169 | /** | |
170 | * {@inheritDoc} | |
171 | * @see org.melati.util.HttpHeader.FieldIterator#nextToken() | |
172 | */ | |
173 | public Object nextToken() throws HttpHeaderException { | |
174 | 984 | return nextWord(); |
175 | } | |
176 | ||
177 | } | |
178 | ||
179 | /** | |
180 | * Factory method to create and return an iterator of words. | |
181 | * | |
182 | * @return a new WordIterator | |
183 | */ | |
184 | public final WordIterator wordIterator() { | |
185 | 328 | return new WordIterator(); |
186 | } | |
187 | ||
188 | /** | |
189 | * Iteration over {@link HttpHeader.TokenAndQValue}s. | |
190 | */ | |
191 | 583 | public class TokenAndQValueIterator extends FieldIterator { |
192 | ||
193 | /** | |
194 | * @return the next TokenAndQValue | |
195 | * @throws HttpHeaderException | |
196 | */ | |
197 | public TokenAndQValue nextTokenAndQValue() throws HttpHeaderException { | |
198 | 12 | return HttpHeader.this.nextTokenAndQValue(); |
199 | } | |
200 | ||
201 | /** | |
202 | * {@inheritDoc} | |
203 | * @see org.melati.util.HttpHeader.FieldIterator#nextToken() | |
204 | */ | |
205 | public Object nextToken() throws HttpHeaderException { | |
206 | 2 | return nextTokenAndQValue(); |
207 | } | |
208 | ||
209 | } | |
210 | ||
211 | /** | |
212 | * Factory method to create and return the next | |
213 | * {@link HttpHeader.TokenAndQValue}. | |
214 | * @return a new TokenAndQValue | |
215 | */ | |
216 | public TokenAndQValue nextTokenAndQValue() throws HttpHeaderException { | |
217 | 12 | return new TokenAndQValue(tokenizer); |
218 | } | |
219 | ||
220 | /** | |
221 | * Factory method to create and return an iterator of {@link TokenAndQValue}'s. | |
222 | * @return a new TokenAndQValueIterator | |
223 | */ | |
224 | public TokenAndQValueIterator tokenAndQValueIterator() { | |
225 | 8 | return new TokenAndQValueIterator(); |
226 | } | |
227 | ||
228 | /** | |
229 | * A token and associated qvalue. | |
230 | */ | |
231 | public static class TokenAndQValue { | |
232 | ||
233 | /** | |
234 | * Token followed by a semicolon separator. | |
235 | */ | |
236 | public String token; | |
237 | ||
238 | /** | |
239 | * Value between zero and one with at most 3 decimal places. | |
240 | * <p> | |
241 | * q stands for "quality" but the RFC 2616 says this is not | |
242 | * completely accurate. | |
243 | * Values closer to 1.0 are better. | |
244 | * Zero means completely unfit. | |
245 | * <p> | |
246 | * The default is 1.0 if not explicitly initialised and this | |
247 | * appears to be correct for most possible uses if not all. | |
248 | */ | |
249 | 603 | public float q = 1.0f; |
250 | ||
251 | /** | |
252 | * Create an uninitialised instance. | |
253 | */ | |
254 | 603 | public TokenAndQValue() { |
255 | 603 | } |
256 | ||
257 | /** | |
258 | * Create an instance and initialise it by reading the given | |
259 | * tokenizer. | |
260 | */ | |
261 | public TokenAndQValue(Tokenizer t) throws HttpHeaderException { | |
262 | 26 | this(); |
263 | 26 | t.readTokenAndQValue(this); |
264 | 18 | t.skipAnyCommaSeparator(); |
265 | 18 | } |
266 | ||
267 | } | |
268 | ||
269 | /** | |
270 | * Tokenizer for parsing occurences of a field. | |
271 | * <p> | |
272 | * Header fields have format defined in RFC 2616 and have the same | |
273 | * general form as in RFC 822 section 3.1. | |
274 | * <p> | |
275 | * This is for fields consisting of tokens, quoted strings and | |
276 | * separators and not those consisting of an arbitrary sequence of | |
277 | * octets. | |
278 | * Tokens are US ASCII characters other than: | |
279 | * <ul> | |
280 | * <li> control characters 0000 to 001F and 007E; | |
281 | * <li> separators defined in RFC 2616; | |
282 | * </ul> | |
283 | * <p> | |
284 | * The convenience methods defined here provide some guidance on how | |
285 | * to interact with the super-type but you can also use inherited | |
286 | * methods. | |
287 | * <p> | |
288 | * We assume that the next token is always already read when a method | |
289 | * starts to interpret a sequence of tokens. | |
290 | * In other words the first token is read by the constructor(s) and then | |
291 | * each such | |
292 | * method returns as a result of reading a token or EOF that it cannot | |
293 | * process but without pushing it back. | |
294 | * The next token to be interpreted is hence the current token | |
295 | * described by the inherited instance variables. | |
296 | * <p> | |
297 | * Note that whitespace is automatically skipped by the supertype. | |
298 | * | |
299 | * @author Jim Wright | |
300 | */ | |
301 | public static class Tokenizer extends StreamTokenizer { | |
302 | ||
303 | /** | |
304 | * Create an instance from a string formed by concatenation of | |
305 | * continuation lines and all occurences of a field, with comma | |
306 | * separators. | |
307 | * <p> | |
308 | * In theory a separator can consist of one or more commas and | |
309 | * spaces and tab. | |
310 | * Fields are never empty. | |
311 | * We cope with this but I doubt typical callers ever encounter | |
312 | * such strings. | |
313 | * <p> | |
314 | * The field list should not be empty but null is | |
315 | * allowed to explicitly indicate that there are no such fields, | |
316 | * if an instance if required nevertheless to provide other | |
317 | * functionality. | |
318 | * | |
319 | * @param fields A non-null, non-empty String | |
320 | * @throws HttpHeaderException Error detected in the argument. | |
321 | */ | |
322 | Tokenizer(String fields) { | |
323 | 912 | super(new StringReader(fields)); |
324 | ||
325 | ||
326 | 912 | resetSyntax(); |
327 | // Initially make all non-control characters token | |
328 | // characters | |
329 | 912 | wordChars('\u0020', '\u007E'); |
330 | // Now change separators back. Tab is not | |
331 | // necessary and there are some ranges but let's | |
332 | // not try and be clever. | |
333 | 912 | String separator = "()<>@,;:\\\"/[]?={} \t"; |
334 | 18240 | for (int i = 0; i < separator.length(); i++) { |
335 | 17328 | ordinaryChar(separator.charAt(i)); |
336 | // System.err.println("Tested 34"); | |
337 | } | |
338 | ||
339 | // Resetting effectively did this to whitespace chars | |
340 | // ordinaryChars('\u0000', '\u0020'); | |
341 | // Set space and table characters as whitespace | |
342 | 912 | whitespaceChars(' ', ' '); |
343 | 912 | whitespaceChars('\t', '\t'); |
344 | ||
345 | 912 | quoteChar('"'); |
346 | ||
347 | 912 | parseNumbers(); |
348 | ||
349 | // Here are some things we have effectively done by resetting | |
350 | // ordinaryChar('/'); | |
351 | // ordinaryChar('\''); | |
352 | ||
353 | // Do not do any other special processing | |
354 | 912 | eolIsSignificant(false); |
355 | 912 | lowerCaseMode(false); |
356 | 912 | slashSlashComments(false); |
357 | 912 | slashStarComments(false); |
358 | ||
359 | // Read the first token | |
360 | 912 | nextLToken(); |
361 | 912 | if (ttype == ',') { |
362 | // System.err.println("Tested 36"); | |
363 | 1 | throw new HttpHeaderException("HTTP header fields starts with comma separator"); |
364 | } | |
365 | 911 | } |
366 | ||
367 | /** | |
368 | * Same as <code>nextToken()</code> but does not throw an <code>IOException</code> | |
369 | * and handles erroneous line breaks. | |
370 | * | |
371 | * @return int value of next LToken | |
372 | * @throws HttpHeaderException Error detected in the fields. | |
373 | */ | |
374 | public int nextLToken() throws HttpHeaderException { | |
375 | int result; | |
376 | try { | |
377 | 2623 | result = nextToken(); |
378 | 2623 | if (ttype == TT_EOL) { |
379 | 0 | System.err.println("Not tested 38"); |
380 | 0 | throw new HttpHeaderException("HTTP header fields span unquoted line breaks"); |
381 | } | |
382 | // System.err.println("Tested 39"); | |
383 | 2623 | return result; |
384 | } | |
385 | 0 | catch (IOException e) { |
386 | //assert false : "We are reading from a string"; | |
387 | 0 | return 0; |
388 | } | |
389 | } | |
390 | ||
391 | /** | |
392 | * Read up to and including the next token after comma | |
393 | * separator(s) and whitespace assuming the current token is a comma. | |
394 | * | |
395 | * @return Resulting ttype. | |
396 | */ | |
397 | public final int skipCommaSeparator() throws HttpHeaderException { | |
398 | 665 | if (ttype != ',') { |
399 | 0 | throw new IllegalStateException("Not at a comma"); |
400 | } | |
401 | 667 | while (nextLToken() == ',') |
402 | 2 | ; |
403 | 665 | return ttype; |
404 | } | |
405 | ||
406 | /** | |
407 | * Read up to and including the next token after any comma | |
408 | * separator(s) and whitespace. | |
409 | * <p> | |
410 | * This is the same as {@link #skipCommaSeparator()} but it does | |
411 | * nothing if we are and EOF. | |
412 | * | |
413 | * @return Resulting ttype. | |
414 | */ | |
415 | public final int skipAnyCommaSeparator() throws HttpHeaderException { | |
416 | 1002 | if (ttype != TT_EOF) { |
417 | 665 | skipCommaSeparator(); |
418 | } | |
419 | 1002 | return ttype; |
420 | } | |
421 | ||
422 | /** | |
423 | * Convenience method to test for token or quoted string. | |
424 | * <p> | |
425 | * If this returns true then the token value is in <code>sval</code> | |
426 | * with any quotes removed. | |
427 | * @return whether token is an SVal | |
428 | */ | |
429 | public final boolean isSVal() { | |
430 | 26 | return ttype == TT_WORD || ttype == '"'; |
431 | } | |
432 | ||
433 | /** | |
434 | * Read the word token or quoted string that comes next. | |
435 | * | |
436 | * @return the SVal | |
437 | * @throws HttpHeaderException Error detected in the fields. | |
438 | */ | |
439 | public final String readSVal() throws HttpHeaderException { | |
440 | 26 | if (! isSVal()) { |
441 | 2 | throw new HttpHeaderException("Next token is not a (possibly quoted) word: " + |
442 | 2 | toString()); |
443 | } | |
444 | 24 | String result = sval; |
445 | 24 | nextLToken(); |
446 | 24 | return result; |
447 | } | |
448 | ||
449 | /** | |
450 | * Read the word token that comes next. | |
451 | * | |
452 | * @return the word as a String | |
453 | * @throws HttpHeaderException Error detected in the fields. | |
454 | */ | |
455 | public final String readWord() throws HttpHeaderException { | |
456 | 995 | if (ttype != TT_WORD) { |
457 | 1 | throw new HttpHeaderException("Next token is not a word token: " + |
458 | 1 | toString()); |
459 | } | |
460 | 994 | String result = sval; |
461 | 994 | nextLToken(); |
462 | // System.err.println("Tested 47"); | |
463 | 994 | return result; |
464 | } | |
465 | ||
466 | /** | |
467 | * Read the given word token that comes next. | |
468 | * | |
469 | * @throws HttpHeaderException Error detected in the fields. | |
470 | */ | |
471 | public final void readWord(String word) throws HttpHeaderException { | |
472 | 11 | String read = readWord(); |
473 | 10 | if (! read.equals(word)) { |
474 | // System.err.println("Tested 48 by temporary hack"); | |
475 | 1 | throw new HttpHeaderException("Expecting '" + word + |
476 | 1 | "' but encountered: " + toString()); |
477 | } | |
478 | 9 | } |
479 | ||
480 | /** | |
481 | * Read the given character that comes next. | |
482 | * | |
483 | * @throws HttpHeaderException Error detected in the fields. | |
484 | */ | |
485 | public final void readChar(char c) throws HttpHeaderException { | |
486 | 20 | if (ttype != c) { |
487 | // System.err.println("Tested 49"); | |
488 | 1 | throw new HttpHeaderException("Expecting '" + c + |
489 | "' but encountered: " + | |
490 | 1 | toString()); |
491 | } | |
492 | 19 | nextLToken(); |
493 | 19 | } |
494 | ||
495 | /** | |
496 | * Read the number token that comes next. | |
497 | * @return the number's value as a double | |
498 | * @throws HttpHeaderException Error detected in the fields. | |
499 | */ | |
500 | public final double readNVal() throws HttpHeaderException { | |
501 | 8 | if (ttype != TT_NUMBER) { |
502 | 1 | throw new HttpHeaderException("Next token is not a number: " + |
503 | 1 | toString()); |
504 | } | |
505 | 7 | double result = nval; |
506 | 7 | nextLToken(); |
507 | 7 | return result; |
508 | } | |
509 | ||
510 | /** | |
511 | * Read a token sequence of the form "; q = 0.42" and return the number. | |
512 | * @return the number's value as a float | |
513 | * | |
514 | * @throws IllegalStateException Current token not semicolon. | |
515 | * @throws HttpHeaderException Error detected in the fields. | |
516 | */ | |
517 | public final float readQValue() | |
518 | throws IllegalStateException, HttpHeaderException { | |
519 | 11 | if (ttype != ';') { |
520 | 0 | throw new IllegalStateException("Not at a semicolon"); |
521 | } | |
522 | 11 | readChar(';'); |
523 | 11 | readWord("q"); |
524 | 9 | readChar('='); |
525 | 8 | return (float)readNVal(); |
526 | } | |
527 | ||
528 | /** | |
529 | * Read a word or quoted string token optionally followed by a string | |
530 | * of the form "; q = 0.42" and initialises the given object. | |
531 | * @return current TokenAndQValue | |
532 | */ | |
533 | protected TokenAndQValue readTokenAndQValue(TokenAndQValue result) | |
534 | throws HttpHeaderException { | |
535 | 26 | result.token = readSVal(); |
536 | 24 | switch (ttype) { |
537 | case TT_EOF : | |
538 | case ',' : | |
539 | 11 | break; |
540 | case ';' : | |
541 | 11 | result.q = readQValue(); |
542 | 7 | break; |
543 | default: | |
544 | 2 | throw new HttpHeaderException("Word token: \'" + result.token + |
545 | 2 | "\' is followed by something unexpected: " + toString()); |
546 | } | |
547 | 18 | return result; |
548 | } | |
549 | ||
550 | } | |
551 | ||
552 | ||
553 | public static class HttpHeaderException extends MelatiRuntimeException { | |
554 | ||
555 | private static final long serialVersionUID = -8870151118057435290L; | |
556 | ||
557 | /** | |
558 | * Create an instance with message. | |
559 | */ | |
560 | public HttpHeaderException(String message) { | |
561 | super(message); | |
562 | } | |
563 | ||
564 | /** | |
565 | * Create an instance with message and cause. | |
566 | */ | |
567 | public HttpHeaderException(String message, Exception e) { | |
568 | super(message, e); | |
569 | } | |
570 | ||
571 | } | |
572 | ||
573 | } |