Coverage Report - org.melati.util.HttpHeader
 
Classes in this File Line Coverage Branch Coverage Complexity
HttpHeader
100%
6/6
N/A
2.167
HttpHeader$FieldIterator
100%
8/8
100%
2/2
2.167
HttpHeader$TokenAndQValue
100%
7/7
N/A
2.167
HttpHeader$TokenAndQValueIterator
100%
3/3
N/A
2.167
HttpHeader$Tokenizer
91%
66/72
89%
34/38
2.167
HttpHeader$WordIterator
100%
5/5
N/A
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  
 }