Source for gnu.java.net.protocol.http.Request

   1: /* Request.java --
   2:    Copyright (C) 2004, 2005, 2006 Free Software Foundation, Inc.
   3: 
   4: This file is part of GNU Classpath.
   5: 
   6: GNU Classpath is free software; you can redistribute it and/or modify
   7: it under the terms of the GNU General Public License as published by
   8: the Free Software Foundation; either version 2, or (at your option)
   9: any later version.
  10:  
  11: GNU Classpath is distributed in the hope that it will be useful, but
  12: WITHOUT ANY WARRANTY; without even the implied warranty of
  13: MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
  14: General Public License for more details.
  15: 
  16: You should have received a copy of the GNU General Public License
  17: along with GNU Classpath; see the file COPYING.  If not, write to the
  18: Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA
  19: 02110-1301 USA.
  20: 
  21: Linking this library statically or dynamically with other modules is
  22: making a combined work based on this library.  Thus, the terms and
  23: conditions of the GNU General Public License cover the whole
  24: combination.
  25: 
  26: As a special exception, the copyright holders of this library give you
  27: permission to link this library with independent modules to produce an
  28: executable, regardless of the license terms of these independent
  29: modules, and to copy and distribute the resulting executable under
  30: terms of your choice, provided that you also meet, for each linked
  31: independent module, the terms and conditions of the license of that
  32: module.  An independent module is a module which is not derived from
  33: or based on this library.  If you modify this library, you may extend
  34: this exception to your version of the library, but you are not
  35: obligated to do so.  If you do not wish to do so, delete this
  36: exception statement from your version. */
  37: 
  38: 
  39: package gnu.java.net.protocol.http;
  40: 
  41: import gnu.java.net.BASE64;
  42: import gnu.java.net.LineInputStream;
  43: 
  44: import java.io.IOException;
  45: import java.io.InputStream;
  46: import java.io.OutputStream;
  47: import java.net.ProtocolException;
  48: import java.security.MessageDigest;
  49: import java.security.NoSuchAlgorithmException;
  50: import java.text.DateFormat;
  51: import java.text.ParseException;
  52: import java.util.Calendar;
  53: import java.util.Date;
  54: import java.util.HashMap;
  55: import java.util.Iterator;
  56: import java.util.Map;
  57: import java.util.Properties;
  58: import java.util.zip.GZIPInputStream;
  59: import java.util.zip.InflaterInputStream;
  60: 
  61: /**
  62:  * A single HTTP request.
  63:  *
  64:  * @author Chris Burdess (dog@gnu.org)
  65:  */
  66: public class Request
  67: {
  68: 
  69:   /**
  70:    * The connection context in which this request is invoked.
  71:    */
  72:   protected final HTTPConnection connection;
  73: 
  74:   /**
  75:    * The HTTP method to invoke.
  76:    */
  77:   protected final String method;
  78: 
  79:   /**
  80:    * The path identifying the resource.
  81:    * This string must conform to the abs_path definition given in RFC2396,
  82:    * with an optional "?query" part, and must be URI-escaped by the caller.
  83:    */
  84:   protected final String path;
  85: 
  86:   /**
  87:    * The headers in this request.
  88:    */
  89:   protected final Headers requestHeaders;
  90: 
  91:   /**
  92:    * The request body provider.
  93:    */
  94:   protected RequestBodyWriter requestBodyWriter;
  95: 
  96:   /**
  97:    * Map of response header handlers.
  98:    */
  99:   protected Map responseHeaderHandlers;
 100: 
 101:   /**
 102:    * The authenticator.
 103:    */
 104:   protected Authenticator authenticator;
 105: 
 106:   /**
 107:    * Whether this request has been dispatched yet.
 108:    */
 109:   private boolean dispatched;
 110: 
 111:   /**
 112:    * Constructor for a new request.
 113:    * @param connection the connection context
 114:    * @param method the HTTP method
 115:    * @param path the resource path including query part
 116:    */
 117:   protected Request(HTTPConnection connection, String method,
 118:                     String path)
 119:   {
 120:     this.connection = connection;
 121:     this.method = method;
 122:     this.path = path;
 123:     requestHeaders = new Headers();
 124:     responseHeaderHandlers = new HashMap();
 125:   }
 126: 
 127:   /**
 128:    * Returns the connection associated with this request.
 129:    * @see #connection
 130:    */
 131:   public HTTPConnection getConnection()
 132:   {
 133:     return connection;
 134:   }
 135: 
 136:   /**
 137:    * Returns the HTTP method to invoke.
 138:    * @see #method
 139:    */
 140:   public String getMethod()
 141:   {
 142:     return method;
 143:   }
 144: 
 145:   /**
 146:    * Returns the resource path.
 147:    * @see #path
 148:    */
 149:   public String getPath()
 150:   {
 151:     return path;
 152:   }
 153: 
 154:   /**
 155:    * Returns the full request-URI represented by this request, as specified
 156:    * by HTTP/1.1.
 157:    */
 158:   public String getRequestURI()
 159:   {
 160:     return connection.getURI() + path;
 161:   }
 162: 
 163:   /**
 164:    * Returns the headers in this request.
 165:    */
 166:   public Headers getHeaders()
 167:   {
 168:     return requestHeaders;
 169:   }
 170: 
 171:   /**
 172:    * Returns the value of the specified header in this request.
 173:    * @param name the header name
 174:    */
 175:   public String getHeader(String name)
 176:   {
 177:     return requestHeaders.getValue(name);
 178:   }
 179: 
 180:   /**
 181:    * Returns the value of the specified header in this request as an integer.
 182:    * @param name the header name
 183:    */
 184:   public int getIntHeader(String name)
 185:   {
 186:     return requestHeaders.getIntValue(name);
 187:   }
 188: 
 189:   /**
 190:    * Returns the value of the specified header in this request as a date.
 191:    * @param name the header name
 192:    */
 193:   public Date getDateHeader(String name)
 194:   {
 195:     return requestHeaders.getDateValue(name);
 196:   }
 197: 
 198:   /**
 199:    * Sets the specified header in this request.
 200:    * @param name the header name
 201:    * @param value the header value
 202:    */
 203:   public void setHeader(String name, String value)
 204:   {
 205:     requestHeaders.put(name, value);
 206:   }
 207: 
 208:   /**
 209:    * Convenience method to set the entire request body.
 210:    * @param requestBody the request body content
 211:    */
 212:   public void setRequestBody(byte[] requestBody)
 213:   {
 214:     setRequestBodyWriter(new ByteArrayRequestBodyWriter(requestBody));
 215:   }
 216: 
 217:   /**
 218:    * Sets the request body provider.
 219:    * @param requestBodyWriter the handler used to obtain the request body
 220:    */
 221:   public void setRequestBodyWriter(RequestBodyWriter requestBodyWriter)
 222:   {
 223:     this.requestBodyWriter = requestBodyWriter;
 224:   }
 225: 
 226:   /**
 227:    * Sets a callback handler to be invoked for the specified header name.
 228:    * @param name the header name
 229:    * @param handler the handler to receive the value for the header
 230:    */
 231:   public void setResponseHeaderHandler(String name,
 232:                                        ResponseHeaderHandler handler)
 233:   {
 234:     responseHeaderHandlers.put(name, handler);
 235:   }
 236: 
 237:   /**
 238:    * Sets an authenticator that can be used to handle authentication
 239:    * automatically.
 240:    * @param authenticator the authenticator
 241:    */
 242:   public void setAuthenticator(Authenticator authenticator)
 243:   {
 244:     this.authenticator = authenticator;
 245:   }
 246: 
 247:   /**
 248:    * Dispatches this request.
 249:    * A request can only be dispatched once; calling this method a second
 250:    * time results in a protocol exception.
 251:    * @exception IOException if an I/O error occurred
 252:    * @return an HTTP response object representing the result of the operation
 253:    */
 254:   public Response dispatch()
 255:     throws IOException
 256:   {
 257:     if (dispatched)
 258:       {
 259:         throw new ProtocolException("request already dispatched");
 260:       }
 261:     final String CRLF = "\r\n";
 262:     final String HEADER_SEP = ": ";
 263:     final String US_ASCII = "US-ASCII";
 264:     final String version = connection.getVersion();
 265:     Response response;
 266:     int contentLength = -1;
 267:     boolean retry = false;
 268:     int attempts = 0;
 269:     boolean expectingContinue = false;
 270:     if (requestBodyWriter != null)
 271:       {
 272:         contentLength = requestBodyWriter.getContentLength();
 273:         String expect = getHeader("Expect");
 274:         if (expect != null && expect.equals("100-continue"))
 275:           {
 276:             expectingContinue = true;
 277:           }
 278:         else
 279:           {
 280:             setHeader("Content-Length", Integer.toString(contentLength));
 281:           }
 282:       }
 283:     
 284:     try
 285:       {
 286:         // Loop while authentication fails or continue
 287:         do
 288:           {
 289:             retry = false;
 290:             
 291:             // Get socket output and input streams
 292:             OutputStream out = connection.getOutputStream();
 293: 
 294:             // Request line
 295:             String requestUri = path;
 296:             if (connection.isUsingProxy() &&
 297:                 !"*".equals(requestUri) &&
 298:                 !"CONNECT".equals(method))
 299:               {
 300:                 requestUri = getRequestURI();
 301:               }
 302:             String line = method + ' ' + requestUri + ' ' + version + CRLF;
 303:             out.write(line.getBytes(US_ASCII));
 304:             // Request headers
 305:             for (Iterator i = requestHeaders.iterator(); i.hasNext(); )
 306:               {
 307:                 Headers.HeaderElement elt = (Headers.HeaderElement)i.next();
 308:                 line = elt.name + HEADER_SEP + elt.value + CRLF;
 309:                 out.write(line.getBytes(US_ASCII));
 310:               }
 311:             out.write(CRLF.getBytes(US_ASCII));
 312:             // Request body
 313:             if (requestBodyWriter != null && !expectingContinue)
 314:               {
 315:                 byte[] buffer = new byte[4096];
 316:                 int len;
 317:                 int count = 0;
 318:                 
 319:                 requestBodyWriter.reset();
 320:                 do
 321:                   {
 322:                     len = requestBodyWriter.write(buffer);
 323:                     if (len > 0)
 324:                       {
 325:                         out.write(buffer, 0, len);
 326:                       }
 327:                     count += len;
 328:                   }
 329:                 while (len > -1 && count < contentLength);
 330:               }
 331:             out.flush();
 332:             // Get response
 333:             while(true)
 334:             {
 335:               response = readResponse(connection.getInputStream());
 336:               int sc = response.getCode();
 337:               if (sc == 401 && authenticator != null)
 338:                 {
 339:                   if (authenticate(response, attempts++))
 340:                     {
 341:                       retry = true;
 342:                     }
 343:                 }
 344:               else if (sc == 100)
 345:                 {
 346:                   if (expectingContinue)
 347:                     {
 348:                       requestHeaders.remove("Expect");
 349:                       setHeader("Content-Length",
 350:                                 Integer.toString(contentLength));
 351:                       expectingContinue = false;
 352:                       retry = true;
 353:                     }
 354:                   else
 355:                     {
 356:                       // A conforming server can send an unsoliceted
 357:                       // Continue response but *should* not (RFC 2616
 358:                       // sec 8.2.3).  Ignore the bogus Continue
 359:                       // response and get the real response that
 360:                       // should follow
 361:                       continue;
 362:                     }
 363:                 }
 364:               break;
 365:             }
 366:           }
 367:         while (retry);
 368:       }
 369:     catch (IOException e)
 370:       {
 371:         connection.close();
 372:         throw e;
 373:       }
 374:     return response;
 375:   }
 376:     
 377:   Response readResponse(InputStream in)
 378:     throws IOException
 379:   {
 380:     String line;
 381:     int len;
 382:     
 383:     // Read response status line
 384:     LineInputStream lis = new LineInputStream(in);
 385: 
 386:     line = lis.readLine();
 387:     if (line == null)
 388:       {
 389:         throw new ProtocolException("Peer closed connection");
 390:       }
 391:     if (!line.startsWith("HTTP/"))
 392:       {
 393:         throw new ProtocolException(line);
 394:       }
 395:     len = line.length();
 396:     int start = 5, end = 6;
 397:     while (line.charAt(end) != '.')
 398:       {
 399:         end++;
 400:       }
 401:     int majorVersion = Integer.parseInt(line.substring(start, end));
 402:     start = end + 1;
 403:     end = start + 1;
 404:     while (line.charAt(end) != ' ')
 405:       {
 406:         end++;
 407:       }
 408:     int minorVersion = Integer.parseInt(line.substring(start, end));
 409:     start = end + 1;
 410:     end = start + 3;
 411:     int code = Integer.parseInt(line.substring(start, end));
 412:     String message = line.substring(end + 1, len - 1);
 413:     // Read response headers
 414:     Headers responseHeaders = new Headers();
 415:     responseHeaders.parse(lis);
 416:     notifyHeaderHandlers(responseHeaders);
 417:     InputStream body = null;
 418:     
 419:     switch (code)
 420:       {
 421:       case 100:
 422:       case 204:
 423:       case 205:
 424:       case 304:
 425:         break;
 426:       default:
 427:         body = createResponseBodyStream(responseHeaders, majorVersion,
 428:                                         minorVersion, in);
 429:       }
 430: 
 431:     // Construct response
 432:     Response ret = new Response(majorVersion, minorVersion, code,
 433:                                 message, responseHeaders, body);
 434:     return ret;
 435:   }
 436: 
 437:   void notifyHeaderHandlers(Headers headers)
 438:   {
 439:     for (Iterator i = headers.iterator(); i.hasNext(); )
 440:       {
 441:         Headers.HeaderElement entry = (Headers.HeaderElement) i.next();
 442:         // Handle Set-Cookie
 443:         if ("Set-Cookie".equalsIgnoreCase(entry.name))
 444:             handleSetCookie(entry.value);
 445: 
 446:         ResponseHeaderHandler handler =
 447:           (ResponseHeaderHandler) responseHeaderHandlers.get(entry.name);
 448:         if (handler != null)
 449:             handler.setValue(entry.value);
 450:       }
 451:   }
 452: 
 453:   private InputStream createResponseBodyStream(Headers responseHeaders,
 454:                                                int majorVersion,
 455:                                                int minorVersion,
 456:                                                InputStream in)
 457:     throws IOException
 458:   {
 459:     long contentLength = -1;
 460:     Headers trailer = null;
 461:     
 462:     // Persistent connections are the default in HTTP/1.1
 463:     boolean doClose = "close".equalsIgnoreCase(getHeader("Connection")) ||
 464:       "close".equalsIgnoreCase(responseHeaders.getValue("Connection")) ||
 465:       (connection.majorVersion == 1 && connection.minorVersion == 0) ||
 466:       (majorVersion == 1 && minorVersion == 0);
 467: 
 468:     String transferCoding = responseHeaders.getValue("Transfer-Encoding");
 469:     if ("chunked".equalsIgnoreCase(transferCoding))
 470:       {
 471:         in = new LimitedLengthInputStream(in, -1, false, connection, doClose);
 472:           
 473:         in = new ChunkedInputStream(in, responseHeaders);
 474:       } 
 475:     else
 476:       {
 477:         contentLength = responseHeaders.getLongValue("Content-Length");
 478: 
 479:         if (contentLength < 0)
 480:           doClose = true;  // No Content-Length, must close.
 481: 
 482:         in = new LimitedLengthInputStream(in, contentLength,
 483:                                           contentLength >= 0,
 484:                                           connection, doClose);
 485:       }
 486:     String contentCoding = responseHeaders.getValue("Content-Encoding");
 487:     if (contentCoding != null && !"identity".equals(contentCoding))
 488:       {
 489:         if ("gzip".equals(contentCoding))
 490:           {
 491:             in = new GZIPInputStream(in);
 492:           }
 493:         else if ("deflate".equals(contentCoding))
 494:           {
 495:             in = new InflaterInputStream(in);
 496:           }
 497:         else
 498:           {
 499:             throw new ProtocolException("Unsupported Content-Encoding: " +
 500:                                         contentCoding);
 501:           }
 502:     // Remove the Content-Encoding header because the content is
 503:     // no longer compressed.
 504:     responseHeaders.remove("Content-Encoding");
 505:       }
 506:     return in;
 507:   }
 508: 
 509:   boolean authenticate(Response response, int attempts)
 510:     throws IOException
 511:   {
 512:     String challenge = response.getHeader("WWW-Authenticate");
 513:     if (challenge == null)
 514:       {
 515:         challenge = response.getHeader("Proxy-Authenticate");
 516:       }
 517:     int si = challenge.indexOf(' ');
 518:     String scheme = (si == -1) ? challenge : challenge.substring(0, si);
 519:     if ("Basic".equalsIgnoreCase(scheme))
 520:       {
 521:         Properties params = parseAuthParams(challenge.substring(si + 1));
 522:         String realm = params.getProperty("realm");
 523:         Credentials creds = authenticator.getCredentials(realm, attempts);
 524:         String userPass = creds.getUsername() + ':' + creds.getPassword();
 525:         byte[] b_userPass = userPass.getBytes("US-ASCII");
 526:         byte[] b_encoded = BASE64.encode(b_userPass);
 527:         String authorization =
 528:           scheme + " " + new String(b_encoded, "US-ASCII");
 529:         setHeader("Authorization", authorization);
 530:         return true;
 531:       }
 532:     else if ("Digest".equalsIgnoreCase(scheme))
 533:       {
 534:         Properties params = parseAuthParams(challenge.substring(si + 1));
 535:         String realm = params.getProperty("realm");
 536:         String nonce = params.getProperty("nonce");
 537:         String qop = params.getProperty("qop");
 538:         String algorithm = params.getProperty("algorithm");
 539:         String digestUri = getRequestURI();
 540:         Credentials creds = authenticator.getCredentials(realm, attempts);
 541:         String username = creds.getUsername();
 542:         String password = creds.getPassword();
 543:         connection.incrementNonce(nonce);
 544:         try
 545:           {
 546:             MessageDigest md5 = MessageDigest.getInstance("MD5");
 547:             final byte[] COLON = { 0x3a };
 548:             
 549:             // Calculate H(A1)
 550:             md5.reset();
 551:             md5.update(username.getBytes("US-ASCII"));
 552:             md5.update(COLON);
 553:             md5.update(realm.getBytes("US-ASCII"));
 554:             md5.update(COLON);
 555:             md5.update(password.getBytes("US-ASCII"));
 556:             byte[] ha1 = md5.digest();
 557:             if ("md5-sess".equals(algorithm))
 558:               {
 559:                 byte[] cnonce = generateNonce();
 560:                 md5.reset();
 561:                 md5.update(ha1);
 562:                 md5.update(COLON);
 563:                 md5.update(nonce.getBytes("US-ASCII"));
 564:                 md5.update(COLON);
 565:                 md5.update(cnonce);
 566:                 ha1 = md5.digest();
 567:               }
 568:             String ha1Hex = toHexString(ha1);
 569:             
 570:             // Calculate H(A2)
 571:             md5.reset();
 572:             md5.update(method.getBytes("US-ASCII"));
 573:             md5.update(COLON);
 574:             md5.update(digestUri.getBytes("US-ASCII"));
 575:             if ("auth-int".equals(qop))
 576:               {
 577:                 byte[] hEntity = null; // TODO hash of entity body
 578:                 md5.update(COLON);
 579:                 md5.update(hEntity);
 580:               }
 581:             byte[] ha2 = md5.digest();
 582:             String ha2Hex = toHexString(ha2);
 583:             
 584:             // Calculate response
 585:             md5.reset();
 586:             md5.update(ha1Hex.getBytes("US-ASCII"));
 587:             md5.update(COLON);
 588:             md5.update(nonce.getBytes("US-ASCII"));
 589:             if ("auth".equals(qop) || "auth-int".equals(qop))
 590:               {
 591:                 String nc = getNonceCount(nonce);
 592:                 byte[] cnonce = generateNonce();
 593:                 md5.update(COLON);
 594:                 md5.update(nc.getBytes("US-ASCII"));
 595:                 md5.update(COLON);
 596:                 md5.update(cnonce);
 597:                 md5.update(COLON);
 598:                 md5.update(qop.getBytes("US-ASCII"));
 599:               }
 600:             md5.update(COLON);
 601:             md5.update(ha2Hex.getBytes("US-ASCII"));
 602:             String digestResponse = toHexString(md5.digest());
 603:             
 604:             String authorization = scheme + 
 605:               " username=\"" + username + "\"" +
 606:               " realm=\"" + realm + "\"" +
 607:               " nonce=\"" + nonce + "\"" +
 608:               " uri=\"" + digestUri + "\"" +
 609:               " response=\"" + digestResponse + "\"";
 610:             setHeader("Authorization", authorization);
 611:             return true;
 612:           }
 613:         catch (NoSuchAlgorithmException e)
 614:           {
 615:             return false;
 616:           }
 617:       }
 618:     // Scheme not recognised
 619:     return false;
 620:   }
 621: 
 622:   Properties parseAuthParams(String text)
 623:   {
 624:     int len = text.length();
 625:     String key = null;
 626:     StringBuilder buf = new StringBuilder();
 627:     Properties ret = new Properties();
 628:     boolean inQuote = false;
 629:     for (int i = 0; i < len; i++)
 630:       {
 631:         char c = text.charAt(i);
 632:         if (c == '"')
 633:           {
 634:             inQuote = !inQuote;
 635:           }
 636:         else if (c == '=' && key == null)
 637:           {
 638:             key = buf.toString().trim();
 639:             buf.setLength(0);
 640:           }
 641:         else if (c == ' ' && !inQuote)
 642:           {
 643:             String value = unquote(buf.toString().trim());
 644:             ret.put(key, value);
 645:             key = null;
 646:             buf.setLength(0);
 647:           }
 648:         else if (c != ',' || (i <(len - 1) && text.charAt(i + 1) != ' '))
 649:           {   
 650:             buf.append(c);
 651:           }
 652:       }
 653:     if (key != null)
 654:       {
 655:         String value = unquote(buf.toString().trim());
 656:         ret.put(key, value);
 657:       }
 658:     return ret;
 659:   }
 660: 
 661:   String unquote(String text)
 662:   {
 663:     int len = text.length();
 664:     if (len > 0 && text.charAt(0) == '"' && text.charAt(len - 1) == '"')
 665:       {
 666:         return text.substring(1, len - 1);
 667:       }
 668:     return text;
 669:   }
 670: 
 671:   /**
 672:    * Returns the number of times the specified nonce value has been seen.
 673:    * This always returns an 8-byte 0-padded hexadecimal string.
 674:    */
 675:   String getNonceCount(String nonce)
 676:   {
 677:     int nc = connection.getNonceCount(nonce);
 678:     String hex = Integer.toHexString(nc);
 679:     StringBuilder buf = new StringBuilder();
 680:     for (int i = 8 - hex.length(); i > 0; i--)
 681:       {
 682:         buf.append('0');
 683:       }
 684:     buf.append(hex);
 685:     return buf.toString();
 686:   }
 687: 
 688:   /**
 689:    * Client nonce value.
 690:    */
 691:   byte[] nonce;
 692: 
 693:   /**
 694:    * Generates a new client nonce value.
 695:    */
 696:   byte[] generateNonce()
 697:     throws IOException, NoSuchAlgorithmException
 698:   {
 699:     if (nonce == null)
 700:       {
 701:         long time = System.currentTimeMillis();
 702:         MessageDigest md5 = MessageDigest.getInstance("MD5");
 703:         md5.update(Long.toString(time).getBytes("US-ASCII"));
 704:         nonce = md5.digest();
 705:       }
 706:     return nonce;
 707:   }
 708: 
 709:   String toHexString(byte[] bytes)
 710:   {
 711:     char[] ret = new char[bytes.length * 2];
 712:     for (int i = 0, j = 0; i < bytes.length; i++)
 713:       {
 714:         int c =(int) bytes[i];
 715:         if (c < 0)
 716:           {
 717:             c += 0x100;
 718:           }
 719:         ret[j++] = Character.forDigit(c / 0x10, 0x10);
 720:         ret[j++] = Character.forDigit(c % 0x10, 0x10);
 721:       }
 722:     return new String(ret);
 723:   }
 724: 
 725:   /**
 726:    * Parse the specified cookie list and notify the cookie manager.
 727:    */
 728:   void handleSetCookie(String text)
 729:   {
 730:     CookieManager cookieManager = connection.getCookieManager();
 731:     if (cookieManager == null)
 732:       {
 733:         return;
 734:       }
 735:     String name = null;
 736:     String value = null;
 737:     String comment = null;
 738:     String domain = connection.getHostName();
 739:     String path = this.path;
 740:     int lsi = path.lastIndexOf('/');
 741:     if (lsi != -1)
 742:       {
 743:         path = path.substring(0, lsi);
 744:       }
 745:     boolean secure = false;
 746:     Date expires = null;
 747: 
 748:     int len = text.length();
 749:     String attr = null;
 750:     StringBuilder buf = new StringBuilder();
 751:     boolean inQuote = false;
 752:     for (int i = 0; i <= len; i++)
 753:       {
 754:         char c =(i == len) ? '\u0000' : text.charAt(i);
 755:         if (c == '"')
 756:           {
 757:             inQuote = !inQuote;
 758:           }
 759:         else if (!inQuote)
 760:           {
 761:             if (c == '=' && attr == null)
 762:               {
 763:                 attr = buf.toString().trim();
 764:                 buf.setLength(0);
 765:               }
 766:             else if (c == ';' || i == len || c == ',')
 767:               {
 768:                 String val = unquote(buf.toString().trim());
 769:                 if (name == null)
 770:                   {
 771:                     name = attr;
 772:                     value = val;
 773:                   }
 774:                 else if ("Comment".equalsIgnoreCase(attr))
 775:                   {
 776:                     comment = val;
 777:                   }
 778:                 else if ("Domain".equalsIgnoreCase(attr))
 779:                   {
 780:                     domain = val;
 781:                   }
 782:                 else if ("Path".equalsIgnoreCase(attr))
 783:                   {
 784:                     path = val;
 785:                   }
 786:                 else if ("Secure".equalsIgnoreCase(val))
 787:                   {
 788:                     secure = true;
 789:                   }
 790:                 else if ("Max-Age".equalsIgnoreCase(attr))
 791:                   {
 792:                     int delta = Integer.parseInt(val);
 793:                     Calendar cal = Calendar.getInstance();
 794:                     cal.setTimeInMillis(System.currentTimeMillis());
 795:                     cal.add(Calendar.SECOND, delta);
 796:                     expires = cal.getTime();
 797:                   }
 798:                 else if ("Expires".equalsIgnoreCase(attr))
 799:                   {
 800:                     DateFormat dateFormat = new HTTPDateFormat();
 801:                     try
 802:                       {
 803:                         expires = dateFormat.parse(val);
 804:                       }
 805:                     catch (ParseException e)
 806:                       {
 807:                         // if this isn't a valid date, it may be that
 808:                         // the value was returned unquoted; in that case, we
 809:                         // want to continue buffering the value
 810:                         buf.append(c);
 811:                         continue;
 812:                       }
 813:                   }
 814:                 attr = null;
 815:                 buf.setLength(0);
 816:                 // case EOL
 817:                 if (i == len || c == ',')
 818:                   {
 819:                     Cookie cookie = new Cookie(name, value, comment, domain,
 820:                                                path, secure, expires);
 821:                     cookieManager.setCookie(cookie);
 822:                   }
 823:                 if (c == ',')
 824:                   {
 825:                     // Reset cookie fields
 826:                     name = null;
 827:                     value = null;
 828:                     comment = null;
 829:                     domain = connection.getHostName();
 830:                     path = this.path;
 831:                     if (lsi != -1)
 832:                       {
 833:                         path = path.substring(0, lsi);
 834:                       }
 835:                     secure = false;
 836:                     expires = null;
 837:                   }
 838:               }
 839:             else
 840:               {
 841:                 buf.append(c);
 842:               }
 843:           }
 844:         else
 845:           {
 846:             buf.append(c);
 847:           }
 848:       }
 849:   }
 850: 
 851: }