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