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

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