/*
  Copyright (C) 2000-2005 SKYRIX Software AG

  This file is part of SOPE.

  SOPE is free software; you can redistribute it and/or modify it under
  the terms of the GNU Lesser General Public License as published by the
  Free Software Foundation; either version 2, or (at your option) any
  later version.

  SOPE is distributed in the hope that it will be useful, but WITHOUT ANY
  WARRANTY; without even the implied warranty of MERCHANTABILITY or
  FITNESS FOR A PARTICULAR PURPOSE.  See the GNU Lesser General Public
  License for more details.

  You should have received a copy of the GNU Lesser General Public
  License along with SOPE; see the file COPYING.  If not, write to the
  Free Software Foundation, 59 Temple Place - Suite 330, Boston, MA
  02111-1307, USA.
*/

#include "WOHttpTransaction.h"
#include "WORecordRequestStream.h"
#include "WOHttpAdaptor.h"
#include "WORequest+Adaptor.h"
#include "NGHttp+WO.h"
#include "WOSimpleHTTPParser.h"
#include <NGObjWeb/WOCoreApplication.h>
#include <NGObjWeb/WORequest.h>
#include <NGObjWeb/WOResponse.h>
#include <NGObjWeb/WOCookie.h>
#include <NGExtensions/NSData+gzip.h>
#include <NGHttp/NGHttp.h>
#include "common.h"

#include <sys/time.h>

@interface WORequest(UsedPrivates)
- (NSCalendarDate *)startDate;
- (id)startStatistics;
@end

int      WOAsyncResponseStatus = 20001;
NSString *WOAsyncResponseTokenKey = @"WOAsyncResponseToken";
NSString *WOAsyncResponseReadyNotificationName =
  @"WOAsyncResponseReadyNotification";
NSString *WOAsyncResponse = @"WOAsyncResponse";

static int HTTP_PERFLOG = -1;

@interface WOCoreApplication(SimpleParserSelection)

- (BOOL)shouldUseSimpleHTTPParserForTransaction:(id)_tx;

@end

@implementation WOHttpTransaction

static NSMutableDictionary *pendingTransactions = nil; // THREAD
static BOOL useSimpleParser = YES;
static int  debugOn = -1;
static int  doCore  = -1;
static NSString *adLogPath = nil;

+ (int)version {
  return 2;
}
+ (void)initialize {
  static BOOL didInit = NO;
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  if (didInit) return;
  didInit = YES;
  
  useSimpleParser = [ud boolForKey:@"WOHttpTransactionUseSimpleParser"];
  debugOn = [[ud objectForKey:@"WODebugHttpTransaction"] boolValue] ? 1 : 0;
  doCore = [[ud objectForKey:@"WOCoreOnHTTPAdaptorException"] boolValue]?1:0;
  
  adLogPath = [[ud stringForKey:@"WOAdaptorLogPath"] copy];
  if (adLogPath == nil) adLogPath = @"";
}

- (BOOL)optionLogStream {
  return [WOHttpAdaptor optionLogStream];
}
- (BOOL)optionLogPerf {
  return HTTP_PERFLOG;
}

- (BOOL)isDebuggingEnabled {
  return debugOn ? YES : NO;
}

- (id)initWithSocket:(id<NGActiveSocket>)_socket
  application:(WOCoreApplication *)_app
{
  if (HTTP_PERFLOG == -1)
    HTTP_PERFLOG = [WOHttpAdaptor optionLogPerf] ? 1 : 0;
  
  NSAssert(_socket, @"missing socket ...");
  NSAssert(_app,    @"missing application ...");
  self->socket      = [_socket retain];
  self->application = [_app    retain];
  return self;
}

- (void)dealloc {
#if 0
  [self debugWithFormat:@"dealloc ..."];
#endif
  [self reset];
  [self->socket        release];
  [self->lastException release];
  [self->application   release];
  [super dealloc];
}

/* state management */

- (void)reset {
  if (self->asyncResponseToken) {
    [self logWithFormat:
            @"WARNING: resetting while async response pending ... (%@)",
            self->asyncResponseToken];
    abort();
    
    [[self notificationCenter] removeObserver:self];
  }
  [self->asyncResponseToken release]; self->asyncResponseToken = nil;
  [self->io         release]; self->io         = nil;
  [self->log        release]; self->log        = nil;
  [self->startDate  release]; self->startDate  = nil;
  [self->woRequest  release]; self->woRequest  = nil;
  [self->woResponse release]; self->woResponse = nil;
}

- (BOOL)closeConnectionAfterDelivery {
  return YES;
}

- (void)start {
  self->startDate = [[NSDate alloc] init];
  self->t = [self->startDate timeIntervalSince1970];
}
- (void)finish {
  if (self->woResponse == nil)
    return;

  [self logResponse:self->woResponse
	toRequest:self->woRequest
	connection:self->socket];
    
  if (HTTP_PERFLOG) {
      struct timeval tv;
      gettimeofday(&tv, NULL);
      self->t = (((double)tv.tv_sec) * ((double)tv.tv_usec) / 1000.0)  - 
	self->t;
      NSLog(@"processing of request took %4.3fs.", 
	    self->t < 0.0 ? -self->t : self->t);
  }
}

- (BOOL)_setupStreamsForSocket {
  if ([self optionLogStream]) {
    self->log = [(WORecordRequestStream *)[WORecordRequestStream alloc] 
      initWithSource:self->socket];
    self->io  = [(NGBufferedStream *)[NGBufferedStream alloc] 
      initWithSource:self->log];
  }
  else {
    self->log = nil;
    self->io = [(NGBufferedStream *)[NGBufferedStream alloc] 
      initWithSource:self->socket];
  }
  return self->io != nil ? YES : NO;
}

static int logCounter = 0;

- (NSString *)currentRecordingPath:(NSString *)_suffix {
  static NSString *s = nil;
  NSString *p;
  
  if (s == nil) {
    s = [[self->application recordingPath] copy];
    if (s == nil) s = @"";
  }
  if ([s length] == 0) return nil;
  
  p = [NSString stringWithFormat:@"%04i-%@", logCounter, _suffix];
  return [s stringByAppendingPathComponent:p];
}

- (void)logRequestData:(NSData *)_data {
  NSString *logPath = nil;
  
  if (![self optionLogStream]) return;
  logCounter++;
  
  if ([adLogPath length] > 0)
    logPath = adLogPath;
  else if ([logPath length] == 0)
    logPath = [self currentRecordingPath:@"request"];
  
  if ([logPath length] == 0)
    logPath = @"/tmp/woadaptor.log";
  
  [_data writeToFile:logPath atomically:NO];
  
#if 1
  NSLog(@"request is\n");
  fflush(stderr);
  fwrite([_data bytes], 1, [_data length], stderr);
  fflush(stderr);
  fprintf(stderr,"\n");
  fflush(stderr);
#endif
}

- (void)logResponse:(WOResponse *)_response
  toRequest:(WORequest *)_request
  data:(NSData *)_data
{
  NSString *logPath;
  
  if (_data == nil) return;
  
  if ((int)[_response status] == (int)WOAsyncResponseStatus)
    return;
  
#if 1
  NSLog(@"response is\n");
  fflush(stderr);
  fwrite([_data bytes], 1, [_data length], stderr);
  fflush(stderr);
  fprintf(stderr,"\n");
  fflush(stderr);
#endif
  
  if ((logPath = [self currentRecordingPath:@"response"]) == nil)
    return;
  
  [_data writeToFile:logPath atomically:NO];
}

- (void)applyAdaptorHeadersWithHttpRequest:(NGHttpRequest *)request {
  /* apply some adaptor headers in direct-connect mode  */

  if (woRequest == nil) return;
  
  if ([woRequest headerForKey:@"x-webobjects-server-url"] == nil) {
    NSString *tmp;
    
    if ((tmp = [woRequest headerForKey:@"host"])) {
      if ([tmp hasSuffix:@":0"] && ([tmp length] > 2)) // TODO: bad bad bad
	tmp = [tmp substringToIndex:([tmp length] - 2)];
      tmp = [@"http://" stringByAppendingString:tmp];
      [woRequest setHeader:tmp forKey:@"x-webobjects-server-url"];
    }
  }
  if ([woRequest headerForKey:@"x-webobjects-server-name"] == nil) {
    NSString *tmp;
    
    if ((tmp = [woRequest headerForKey:@"host"])) {
      NSRange r = [tmp rangeOfString:@":"];
      if (r.length > 0) tmp = [tmp substringToIndex:r.location];
      [woRequest setHeader:tmp forKey:@"x-webobjects-server-name"];
    }
  }
  if ([[woRequest headerForKey:@"x-webobjects-server-port"] intValue] < 1) {
    id tmp;
    
    if ((tmp = [woRequest headerForKey:@"host"])) {
      NSRange r = [tmp rangeOfString:@":"];
      if (r.length > 0) tmp = [tmp substringFromIndex:r.location + r.length];
      tmp = [NSNumber numberWithInt:[tmp intValue]];
      [woRequest setHeader:tmp forKey:@"x-webobjects-server-port"];
    }
  }
  
  if ([woRequest headerForKey:@"x-webobjects-remote-host"] == nil) {
    id<NGSocketAddress> remote = nil;
    NSString *remoteHost = nil;
    
    remote = [self->socket remoteAddress];
    
    if ([remote isKindOfClass:[NGInternetSocketAddress class]])
      remoteHost = [(NGInternetSocketAddress *)remote hostName];
#if !defined(__MINGW32__)
    else if ([remote isKindOfClass:[NGLocalSocketAddress class]])
      remoteHost = @"local";
#endif

    if ([remoteHost length] > 0)
      [woRequest setHeader:remoteHost forKey:@"x-webobjects-remote-host"];
  }
        
  if ([woRequest headerForKey:@"x-webobjects-remote-user"] == nil) {
    id auth;
          
    auth = [[request valuesOfHeaderFieldWithName:@"authorization"]
                     nextObject];
    if (auth) {
      if (![auth isKindOfClass:[NGHttpCredentials class]]) {
        auth =
          [NGHttpCredentials credentialsWithString:[auth stringValue]];
      }
            
      [woRequest setHeader:[auth userName]
                 forKey:@"x-webobjects-remote-user"];
      [woRequest setHeader:[auth scheme]
                 forKey:@"x-webobjects-auth-type"];
    }
  }
}

- (WOResponse *)generateMissingResponse {
  WOResponse *mr;
  NSString   *accept;

  mr = [WOResponse alloc];
  mr = [mr initWithRequest:self->woRequest];
  [mr setHTTPVersion:[woRequest httpVersion]];
  [mr setStatus:500];
  
  accept = [woRequest headerForKey:@"accept"];
  
  if ([accept rangeOfString:@"text/html"].length > 0) {
    const unsigned char *txt = "could not perform request !<br />";
    [mr setHeader:@"text/html" forKey:@"content-type"];
    [mr setContent:[NSData dataWithBytes:txt length:strlen(txt)]];
  }
  return [mr autorelease];
}

- (BOOL)_readRequest {
  id request = nil;

  if (self->woRequest)
    [self logWithFormat:@"WARNING: woRequest already set ???"];
  
  if ([self->application shouldUseSimpleHTTPParserForTransaction:self]) {
    WOSimpleHTTPParser *parser;
    
    parser = [[WOSimpleHTTPParser alloc] initWithStream:self->io];
    self->woRequest = [[parser parseRequest] retain];
    
    if (self->woRequest == nil) {
      ASSIGN(self->lastException, [parser lastException]);
      [self logWithFormat:@"failed to parse request: %@", self->lastException];
    }
    [parser release];
  }
  else {
    if ((request = [self parseRequestFromStream:self->io]) == nil)
      return NO;
    
#if DEBUG
    NSAssert([request isKindOfClass:[NGHttpRequest class]],
	     @"invalid request class");
#endif
    self->woRequest = [[request woRequest] retain];
  }
  [self logRequestData:[log readLog]];
  [log resetReadLog];
  
  if ([self->woRequest isCodeRedAttack]) {
    NSLog(@"WOHttpAdaptor: detected 'Code Red' request: '%@', blocking.",
          [self->woRequest uri]);
    ASSIGN(self->woRequest, (id)nil);
    return NO;
  }
  
  [self->woRequest takeStartDate:self->startDate];
  
  /* apply some adaptor headers in direct-connect mode  */
  [self applyAdaptorHeadersWithHttpRequest:request];
  
  if (HTTP_PERFLOG) {
    NSTimeInterval rt;
    self->requestFinishTime = [[NSDate date] timeIntervalSince1970];
    rt = self->requestFinishTime - self->t;
    NSLog(@"[httpperf]: decoding of request took %4.3fs.",
          rt < 0.0 ? -1.0 : rt);
  }
  
  return self->woRequest ? YES : NO;
}

- (BOOL)_sendResponse {
  if (HTTP_PERFLOG) {
    NSTimeInterval rt;
    self->dispatchFinishTime = [[NSDate date] timeIntervalSince1970];
    rt = self->dispatchFinishTime - self->requestFinishTime;
    NSLog(@"[httpperf]: dispatch of request took %4.3fs.",
          rt < 0.0 ? -1.0 : rt);
  }
  
  if (self->woResponse) {
    [self deliverResponse:self->woResponse
          toRequest:self->woRequest
          onStream:self->io];
    
    if (HTTP_PERFLOG) {
      NSTimeInterval rt;
      rt = [[NSDate date] timeIntervalSince1970] - dispatchFinishTime;
      NSLog(@"[httpperf]: delivery of request took %4.3fs.",
            rt < 0.0 ? -1.0 : rt);
    }
  }
  else if (self->woRequest) {
    [self logWithFormat:
            @"ERROR: got no response for request %@ ..",
            self->woRequest];
      
    self->woResponse = [[self generateMissingResponse] retain];
    
    [self deliverResponse:self->woResponse
          toRequest:self->woRequest
          onStream:self->io];
  }
  
  if (![self->io flush]) {
    ASSIGN(self->lastException, [self->io lastException]);
    return NO;
  }
  
  if ([self closeConnectionAfterDelivery]) {
    [self debugWithFormat:@"close connection: %@", self->io];
    if (![self->io close]) {
      ASSIGN(self->lastException, [self->io lastException]);
      [self debugWithFormat:@"close failed: %@", self->lastException];
      return NO;
    }
  }
  else
    [self debugWithFormat:@"not closing connection ..."];
  
  return YES;
}

- (NSNotificationCenter *)notificationCenter {
  return [NSNotificationCenter defaultCenter];
}

- (void)responseReady:(NSNotification *)_notification {
  WOResponse *response;
  
  if ([self->asyncResponseToken length] == 0) {
    [self logWithFormat:
            @"got response ready notification (%@), "
            @"but no async HTTP transaction is in progress ...",
            _notification];
    return;
  }
  if (![self->asyncResponseToken isEqual:[_notification object]]) {
    [self logWithFormat:
            @"got response ready notification (%@) for a different "
            @"token (%@ vs %@) !",
            _notification, self->asyncResponseToken, [_notification object]];
    return;
  }
  
  /* OK, everything seems to be correct, so we received a response .. */
  
  [[self retain] autorelease];
  [pendingTransactions removeObjectForKey:self->asyncResponseToken];
  
  response = [[_notification userInfo] objectForKey:WOAsyncResponse];
  ASSIGN(self->woResponse, response);
  
  [[self notificationCenter] removeObserver:self];
  [self->asyncResponseToken release]; self->asyncResponseToken = nil;
  
  /* send response */
  
  [self debugWithFormat:@"sending async response: %@", self->woResponse];
  [self _sendResponse];
  
  [self debugWithFormat:@"logging async response: %@", self->woResponse];
  [self logResponse:self->woResponse
        toRequest:self->woRequest
        data:[log writeLog]];
  [log resetWriteLog];
  
  [self finish];
  [self reset];

  [self->io  release]; self->io  = nil;
  [self->log release]; self->log = nil;
}

- (BOOL)_enterAsyncMode:(WOResponse *)_response {
  NSString *token;
  
  [self debugWithFormat:@"enter async mode ..."];
  
  if (pendingTransactions == nil)
    pendingTransactions = [[NSMutableDictionary alloc] initWithCapacity:16];
  
  [self debugWithFormat:@"PENDING: %@", pendingTransactions];
  
  NSAssert1((int)[_response status] == (int)WOAsyncResponseStatus,
            @"passed in an invalid response %@ ...", _response);
  
  token = [[_response userInfo] objectForKey:WOAsyncResponseTokenKey];
  if ([token length] == 0) {
    [self logWithFormat:@"missing async response token in response %@",
            _response];
    return NO;
  }

  [self debugWithFormat:@"using token: %@", token];
  ASSIGN(self->asyncResponseToken, token);
  
  [pendingTransactions setObject:self forKey:self->asyncResponseToken];
  
  [[self notificationCenter]
         addObserver:self selector:@selector(responseReady:)
         name:WOAsyncResponseReadyNotificationName
         object:self->asyncResponseToken];
  
  return YES;
}

- (BOOL)_run {
  if (![self _setupStreamsForSocket])
    return NO;
  
  if (![self _readRequest])
    return NO;
  
  /* dispatch request */
  
  if (self->woRequest)
    self->woResponse = [[self->application dispatchRequest:woRequest] retain];
  else
    self->woResponse = nil;
  
  if (self->woResponse) {
    if ((int)[self->woResponse status] == (int)WOAsyncResponseStatus) {
      /* switch to async mode ... */
      if ([self _enterAsyncMode:self->woResponse]) {
        [self logResponse:self->woResponse
              toRequest:self->woRequest
              data:nil];
        return YES;
      }
    }
  }
  
  /* send response */
  
  [self _sendResponse];
  
  [self logResponse:self->woResponse
        toRequest:self->woRequest
        data:[self->log writeLog]];
  [self->log resetWriteLog];
    
  [self->io  release]; self->io  = nil;
  [self->log release]; self->log = nil;
  return YES;
}

- (NSException *)lastException {
  return self->lastException;
}

- (BOOL)_catchedException:(NSException *)localException {
  if ([localException isKindOfClass:[NGSocketShutdownException class]])
    return YES;
  
  ASSIGN(self->lastException, localException);
  
#if DEBUG
  if (doCore) abort();
#endif
  return NO;
}

- (BOOL)run {
  BOOL ok = YES;
  
  [self reset];
  [self start];
  
  NS_DURING {
    if (![self _run])
      ok = NO;
  }
  NS_HANDLER
    ok = [self _catchedException:localException];
  NS_ENDHANDLER;
  
  if (self->asyncResponseToken == nil) {
    [self finish];
    [self reset];
  }
  
  return ok;
}

- (NGHttpRequest *)parseRequestFromStream:(id<NGStream>)_in {
  NGHttpMessageParser *parser = nil;
  volatile id request = nil;
  NSString *format = @"parsing of request failed with exception: %@";
  
  NS_DURING {
    *(&parser) = [[NGHttpMessageParser alloc] init];
    [parser setDelegate:self];
    
    request = [parser parseRequestFromStream:_in];
    
    [parser release]; parser = nil;
  }
  NS_HANDLER {
    [self logWithFormat:format, localException];
    [parser release]; parser = nil;
    [localException raise];
  }
  NS_ENDHANDLER;

  return request;
}

- (const unsigned char *)_reasonForStatus:(unsigned int)_status {
  const unsigned char *reason;
  
  switch (_status) {
    case 200: reason = "Ok";           break;
    case 201: reason = "Created";      break;
    case 204: reason = "No Content";   break;
    case 207: reason = "Multi-Status"; break;
    
    case 302: reason = "Found"; break;
      
    case 401: reason = "Authorization Required"; break;
    case 402: reason = "Payment Required";       break;
    case 403: reason = "Forbidden";              break;
    case 404: reason = "Not found";              break;
    case 405: reason = "Method not allowed";     break;
    case 409: reason = "Conflict";               break;
    case 412: reason = "Precondition Failed";    break;
    case 415: reason = "Unsupported Media Type"; break;
    case 424: reason = "Failed Dependency";      break;
    
    case 507: reason = "Insufficient Storage";   break;
    
    default:
      if (_status < 300)
        reason = "Request was sucessful";
      else
        reason = "Request failed";
      break;
  }
  return reason;
}

- (void)_httpValidateResponse:(WOResponse *)_response {
  /* check HTTP validity */
  if ([_response status] == NGHttpStatusCode_Unauthorized) {
    if ([_response headerForKey:@"www-authenticate"] == nil) {
      [self logWithFormat:
              @"WARNING: response is %i, "
              @"but no www-authenticate header is set.",
              NGHttpStatusCode_Unauthorized];
    }
  }
}

- (void)deliverResponse:(WOResponse *)_response
  toRequest:(WORequest *)_request
  onStream:(id<NGStream>)_out
{
  /*
    Profiling OSX: - takes 29% of tx -run
      12% CTextStream writeString
        ... to sendto 5.1% (half+ of the performance lost on the way)
      4.6% WOMessage   headersForKey
      4.4% TextStream  writeFormat
    => TODO(perf) reduce usage of writeFormat/writeString
  */
  NGCTextStream *out;
  static NSString *disconnectError =
    @"client disconnected during delivery of response for %@ (len=%i): %@";
  static NSString *deliveryError =
    @"delivering of response failed with exception: %@";

  *(&out) = nil;
  
  [self _httpValidateResponse:_response];
  
  out = [(NGCTextStream *)[NGCTextStream alloc] initWithSource:_out];
  
  NS_DURING {
    unsigned char buf[1024];
    NSString *t1;
    id   body;
    BOOL doZip;
    BOOL isok = YES;
    
    doZip = [_response shouldZipResponseToRequest:_request];
    
    /* response line */
    if (isok) {
      unsigned int slen, rlen;
      const unsigned char *r;
      int s = [_response status];
      t1 = [_response httpVersion];
      r  = [self _reasonForStatus:s];
      
      slen = [t1 cStringLength];
      rlen = strlen(r);
      if ((slen + rlen + 8) < 1000) {
        [t1 getCString:buf];
        sprintf(&(buf[slen]), " %i %s\r\n", s, r);
        isok = [_out safeWriteBytes:buf count:strlen(buf)];
      }
      else
        isok = [out writeFormat:@"%@ %i %s\r\n", t, s, r];
    }
    if (isok) isok = [out flush];
    
    /* zip */
    body = (doZip) 
      ? [_response zipResponse]
      : [_response content];
    
    /* add content length header */
    
    sprintf(buf, "%d", [body length]);
    t1 = [[NSString alloc] initWithCString:buf];
    [_response setHeader:t1 forKey:@"content-length"];
    [t1 release]; t1 = nil;
    
    /* write headers */
    if (isok) {
      /* collect in string to reduce string IO */
      NSEnumerator    *fields;
      NSString        *fieldName;
      NSMutableString *header;
      BOOL hasConnectionHeader;
      IMP  addStr;
      
      hasConnectionHeader = NO;
      header = [[NSMutableString alloc] initWithCapacity:4096];
      addStr = [header methodForSelector:@selector(appendString:)];
      fields = [[_response headerKeys] objectEnumerator];
      
      while ((fieldName = [fields nextObject]) && isok) {
        NSEnumerator *values;
        NSString *value;
	
	if (!hasConnectionHeader) {
	  if ([fieldName caseInsensitiveCompare:@"connection"]==NSOrderedSame)
	    hasConnectionHeader = YES;
	}
	
        values = [[_response headersForKey:fieldName] objectEnumerator];
	
        while ((value = [values nextObject]) && isok) {
          addStr(header, @selector(appendString:), fieldName);
          addStr(header, @selector(appendString:), @": ");
          addStr(header, @selector(appendString:), value);
          addStr(header, @selector(appendString:), @"\r\n");
        }
      }
      isok = [out writeString:header];
      [header release];

#if 0
#warning TODO: experimental, need to check for direct connect
      if (!hasConnectionHeader && isok)
	isok = [out writeString:@"connection: close\r\n"];
#endif
    }
    
    /* write cookie headers */
    if (isok) {
      NSEnumerator *cookies;
      WOCookie     *cookie;
      
      cookies = [[_response cookies] objectEnumerator];
      while ((cookie = [cookies nextObject]) && isok) {
        unsigned clen;
        
        t1   = [cookie stringValue];
        clen = [t1 cStringLength];
        
        if (isok) isok = [_out safeWriteBytes:"set-cookie: " count:12];
        if (isok) {
          if (clen > 1000)
            isok = [out writeString:t1];
          else {
            [t1 getCString:buf];
            [_out safeWriteBytes:buf count:clen];
          }
        }
        if (isok) isok = [_out safeWriteBytes:"\r\n" count:2];
      }
    }
    
    if (isok) isok = [_out safeWriteBytes:"\r\n" count:2];
    if (isok) isok = [out flush];
    
    /* write body */
    
    if (![[_request method] isEqualToString:@"HEAD"] && isok) {
      if ((body != nil) && isok) {
        if (![body isKindOfClass:[NSData class]]) {
          if (![body isKindOfClass:[NSString class]])
            body = [body description];
          
          body = [body dataUsingEncoding:[_response contentEncoding]
                       allowLossyConversion:NO];
        }
        isok = [_out safeWriteBytes:[body bytes] count:[body length]];
        if (isok) isok = [_out flush];
      }
    }
    
    if (!isok) {
      NSException *e;
      
      e = [out lastException];
      if ([e isKindOfClass:[NGSocketShutdownException class]]) {
        [self logWithFormat:disconnectError,
              _request,
              [[_response content] length],
              [e reason]];
      }
      else
        [e raise];
    }
  }
  NS_HANDLER {
    if ([localException isKindOfClass:[NGSocketShutdownException class]]) {
      [self logWithFormat:disconnectError,
              _request,
              [[_response content] length],
              [localException reason]];
    }
    else {
      [self logWithFormat:deliveryError, localException];
      [out release]; out = nil; // the buffer will be flushed ..

      [localException raise];
    }
  }
  NS_ENDHANDLER;
  
  [out release]; out = nil; // the buffer will be flushed ..
}

static __inline__ const unsigned char *monthAbbr(int m) {
  switch (m) {
    case  1:  return "Jan"; case  2:  return "Feb"; case  3:  return "Mar";
    case  4:  return "Apr"; case  5:  return "May"; case  6:  return "Jun";
    case  7:  return "Jul"; case  8:  return "Aug"; case  9:  return "Sep";
    case 10:  return "Oct"; case 11:  return "Nov"; case 12:  return "Dec";
    default: return "UNKNOWN MONTH !";
  }
}

- (void)logResponse:(WOResponse *)_response
  toRequest:(WORequest *)_request
  connection:(id<NGActiveSocket>)_connection
{
  /* 
    Profiling: this method takes 0.95% of -run if the output is piped to
    /dev/null on OSX, morphing caldate to string is 0.79% of that.
  */
  NSString       *remoteHost = @"-";
  NSNumber       *zippedLen;
  NSCalendarDate *now;
  NSDate         *lstartDate;
  NSDictionary   *startStats;
  
  lstartDate = [_request startDate];
  startStats = [_request startStatistics];
  zippedLen  = [[_response userInfo] objectForKey:@"WOResponseZippedLength"];
  
  // host and date
  if ((remoteHost = [_request headerForKey:@"x-webobjects-remote-host"]))
    ;
  else if ((remoteHost = [_request headerForKey:@"x-webobjects-remote-addr"]))
    ;
  else {
    id<NGSocketAddress> remote = nil;

    remote = [_connection remoteAddress];

    if ([remote isKindOfClass:[NGInternetSocketAddress class]])
      remoteHost = [(NGInternetSocketAddress *)remote hostName];
#if !defined(__MINGW32__)
    else if ([remote isKindOfClass:[NGLocalSocketAddress class]])
      remoteHost = @"local";
#endif
  }
  
  // this is supposed to be in GMT ! TODO: explicitly set GMT
  now = [NSCalendarDate calendarDate];
  
  /* print standard info */
  printf("%s - - [%02i/%s/%04i:%02i:%02i:%02i GMT] \"%s %s %s\" %i %i",
         remoteHost ? [remoteHost cString] : "-",
         [now dayOfMonth],
         monthAbbr([now monthOfYear]),
         [now yearOfCommonEra],
         [now hourOfDay], [now minuteOfHour], [now secondOfMinute],
         [[_request method] cString],
         [[_request uri] cString],
         [[_request httpVersion] cString],
         [_response status],
         [[_response content] length]);
  
  /* print duration */
  if (lstartDate)
    printf(" %.3f", [now timeIntervalSinceDate:lstartDate]);
  else
    printf(" -");
  
  /* print zip level */
  if (zippedLen) {
    double p;
    double unzippedLen;
    
    unzippedLen = 
      [[[_response userInfo] objectForKey:@"WOResponseUnzippedLength"] 
	           unsignedIntValue];
    
    printf(" %d", (unsigned int)unzippedLen);

    if ([zippedLen unsignedIntValue] == unzippedLen) {
      printf(" -");
    }
    else {
      p = unzippedLen / 100.0; // one percent
      p = [zippedLen doubleValue] / p;
      p = 100.0 - p;
      printf(" %-2d%%", (unsigned int)p);
    }
  }
  else {
    /* content was not zipped */
    printf(" - -");
  }
  
  /* print statistics */
  
  if (startStats) {
    static NSProcessInfo *pi = nil;
    NSDictionary *currentStats;
    
    if (pi == nil) pi = [[NSProcessInfo processInfo] retain];
    
    if ((currentStats = [pi procStatDictionary])) {
      int old, new, diff;
      
      old = [[startStats   objectForKey:@"rss"] intValue];
      new = [[currentStats objectForKey:@"rss"] intValue];
      diff = new - old; /* number of pages (4KB on ix86 ..) */
      
      diff *= 4; /* in KB */
      if (diff == 0)
	printf(" 0");
      else if (diff > 999)
	printf(" %iM", diff / 1024);
      else
	printf(" %iK", diff);
    }
    else
      printf(" ?");
  }
  else
    printf(" -");
  
  /* flush log */
  puts("");
  fflush(stdout);
}

/* NGHttpMessageParserDelegate */

- (BOOL)httpParserWillParseRequest:(NGHttpMessageParser *)_parser {
  return YES;
}
- (void)httpParser:(NGHttpMessageParser *)_parser
  didParseRequest:(NGHttpRequest *)_request {
}

- (BOOL)parser:(NGMimePartParser *)_parser
  keepHeaderField:(NSString *)_name
  value:(id)_value
{
  return YES;
}
- (void)parser:(NGMimePartParser *)_parser didParseHeader:(NGHashMap *)_header {
}

@end /* WOHttpAdaptor */

@implementation WOCoreApplication(SimpleParserSelection)

- (BOOL)shouldUseSimpleHTTPParserForTransaction:(id)_tx {
  return useSimpleParser;
}

@end /* WOCoreApplication(SimpleParserSelection) */
