Coverage Report - org.melati.util.HttpHeader
 
Classes in this File Line Coverage Branch Coverage Complexity
HttpHeader
100%
8/8
100%
2/2
2.067
HttpHeader$FieldIterator
100%
8/8
100%
2/2
2.067
HttpHeader$TokenAndQValue
100%
7/7
N/A
2.067
HttpHeader$TokenAndQValueIterator
100%
3/3
N/A
2.067
HttpHeader$Tokenizer
92%
70/76
90%
28/31
2.067
HttpHeader$WordIterator
100%
5/5
N/A
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  
 }