/*
  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 "SoWebDAVRenderer.h"
#include "SoWebDAVValue.h"
#include "SoObject+SoDAV.h"
#include "EOFetchSpecification+SoDAV.h"
#include "NSException+HTTP.h"
#include <NGObjWeb/WOContext.h>
#include <NGObjWeb/WOResponse.h>
#include <NGObjWeb/WORequest.h>
#include <NGObjWeb/WOElement.h>
#include <SaxObjC/XMLNamespaces.h>
#include <NGExtensions/NSString+Ext.h>
#include "common.h"

/*
  What HotMail uses for responses:
    <?xml version="1.0" encoding="Windows-1252"?>
    Headers:
      Server:              Microsoft-IIS/5.0
      X-Timestamp:         folders=1035823428, ACTIVE=1035813212
      Client-Response-Num: 1
      Client-Date:         <date>
      Expires:             ...
      P3P:                 BUS CUR CONo FIN IVDo ONL OUR PHY SAMo TELo
*/

#define XMLNS_INTTASK \
@"{http://schemas.microsoft.com/mapi/id/{00062003-0000-0000-C000-000000000046}/}"

@interface SoWebDAVRenderer(Privates)
- (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
  inContext:(WOContext *)_ctx; 
@end

@implementation SoWebDAVRenderer

static NSDictionary *predefinedNamespacePrefixes = nil;
static NSTimeZone   *gmt         = nil;
static BOOL         debugOn      = NO;
static BOOL         formatOutput = NO;

+ (void)initialize {
  NSUserDefaults *ud = [NSUserDefaults standardUserDefaults];
  static BOOL didInit = NO;
  if (didInit) return;
  didInit = YES;
  if (gmt == nil) 
    gmt = [[NSTimeZone timeZoneWithAbbreviation:@"GMT"] retain];

  if (predefinedNamespacePrefixes == nil) {
    predefinedNamespacePrefixes = 
      [[ud objectForKey:@"SoPreferredNamespacePrefixes"] copy];
  }
  formatOutput = [ud boolForKey:@"SoWebDAVFormatOutput"];
  
  if ((debugOn = [ud boolForKey:@"SoRendererDebugEnabled"]))
    NSLog(@"enabled debugging in SoWebDAVRenderer (SoRendererDebugEnabled)");
}

+ (id)sharedRenderer {
  static SoWebDAVRenderer *r = nil; // THREAD
  if (r == nil) r = [[SoWebDAVRenderer alloc] init];
  return r;
}

- (NSString *)preferredPrefixForNamespace:(NSString *)_uri {
  return [predefinedNamespacePrefixes objectForKey:_uri];
}

/* key render entry-point */

- (void)_fixupResponse:(WOResponse *)_r inContext:(WOContext *)_ctx {
  NSDate   *now;
  NSString *nowHttpString;
  id tmp;
  
  if ((tmp = [_r headerForKey:@"server"]) == nil) {
    // TODO: add application name as primary name
    static NSString *server = nil;

    if (server == nil) {
      server = [[NSString alloc] initWithFormat:@"SOPE %i.%i.%i/WebDAV",
				 SOPE_MAJOR_VERSION, SOPE_MINOR_VERSION,
				 SOPE_SUBMINOR_VERSION];
    }
    
    [_r setHeader:server forKey:@"server"];
  }
  
  [_r setHeader:@"close" forKey:@"connection"];
  [_r setHeader:@"DAV"   forKey:@"Ms-Author-Via"];
  
  // what program uses that header ?
  [_r setHeader:@"200 No error" forKey:@"X-Dav-Error"];

  if ((tmp = [_r headerForKey:@"content-type"]) == nil)
    [_r setHeader:@"text/xml" forKey:@"content-type"];
    
  now = [NSDate date];
  nowHttpString = [now descriptionWithCalendarFormat:
			 @"%a, %d %b %Y %H:%M:%S GMT"
		       timeZone:gmt
		       locale:nil];
    
  if ((tmp = [_r headerForKey:@"date"]) == nil)
    [_r setHeader:nowHttpString forKey:@"date"];

#if 0 /* currently none of the clients allows zipping, retry later ... */
  /* try zipping */
  if ([_r shouldZipResponseToRequest:nil]) {
    [self logWithFormat:@"zipping DAV result ..."];
    [_r zipResponse];
  }
#endif
}

- (NSString *)mimeTypeForData:(NSData *)_data inContext:(WOContext *)_ctx {
  /* should check extension for MIME type */
  return @"application/octet-stream";
}
- (NSString *)mimeTypeForString:(NSString *)_str inContext:(WOContext *)_ctx {
  /* should check extension for MIME type */

  if ([_str hasPrefix:@"<?xml"])
    return @"text/xml; charset=\"utf-8\"";
  if ([_str hasPrefix:@"<html"])
    return @"text/html; charset=\"utf-8\"";
  
  return @"text/plain; charset=\"utf-8\"";
}

- (BOOL)renderObjectBodyResult:(id)_object inContext:(WOContext *)_ctx 
  onlyHead:(BOOL)_onlyHead
{
  WOResponse *r;
  NSString *tmp;
  unsigned char buf[128];
  
  r = [_ctx response];
  
  /*
    TODO: implement proper etag support. This probably implies that we need
          to pass in some structure or store the etag in the context?
	  We cannot use davEntityTag on the input parameter, since this is
	  usually the plain object.
  */
  if ((tmp = [r headerForKey:@"etag"]) == nil) {
    tmp = @"0"; // fallback, cannot use the thing above
    [r setHeader:tmp forKey:@"etag"]; // required for WebFolder PUTs
  }
  
  if ([_object isKindOfClass:[NSData class]]) {
    NSString *s;
    
    [r setHeader:[self mimeTypeForData:_object inContext:_ctx]
       forKey:@"content-type"];
    
    sprintf((char *)buf, "%d", [_object length]);
    s = [[NSString alloc] initWithCString:(const char *)buf];
    if (s != nil) [r setHeader:s forKey:@"content-length"];
    [s release];
    if (!_onlyHead) [r setContent:_object];
    return YES;
  }
  
  if ([_object isKindOfClass:[NSString class]]) {
    NSData   *data;
    NSString *s;
    
    [r setHeader:[self mimeTypeForString:_object inContext:_ctx]
       forKey:@"content-type"];
    
    data = [_object dataUsingEncoding:NSUTF8StringEncoding];
    sprintf((char *)buf, "%d", [data length]);
    s = [[NSString alloc] initWithCString:(const char *)buf];
    [r setHeader:s forKey:@"content-length"];
    [s release];
    [r setContent:data];
    return YES;
  }
  
  if ([_object respondsToSelector:@selector(appendToResponse:inContext:)]) {
    unsigned len;
    NSData   *data;
    
    [_object appendToResponse:r inContext:_ctx];
    
    data = [r content];
    if ([[r headerForKey:@"content-type"] length] == 0) {
      [r setHeader:[self mimeTypeForData:data inContext:_ctx]
         forKey:@"content-type"];
    }
    len = [data length];
    if (_onlyHead) [r setContent:nil];
    data = nil;
    [r setHeader:[NSString stringWithFormat:@"%d", len]
       forKey:@"content-length"];
    return YES;
  }
  
  [self errorWithFormat:@"don't know how to render: %@", _object];
  return NO;
}

- (NSException *)renderObject:(id)_object inContext:(WOContext *)_ctx {
  NSString *m;
  unichar  c1;
  BOOL ok;
  
  if ([_object isKindOfClass:[WOResponse class]]) {
    if (_object != [_ctx response]) {
      [self logWithFormat:@"response mismatch"];
      return [NSException exceptionWithHTTPStatus:500 /* internal error */];
    }
    [self _fixupResponse:_object inContext:_ctx];
    return nil;
  }
  
  m = [[_ctx request] method];
  if ([m length] == 0) {
    return [NSException exceptionWithHTTPStatus:400 /* bad request */
			reason:@"missing method name!"];
  }
  c1 = [m characterAtIndex:0];

  ok = NO;
  switch (c1) {
  case 'B':
    if ([m isEqualToString:@"BPROPFIND"])
      ok = [self renderSearchResult:_object inContext:_ctx];
    break;
  case 'C':
    if ([m isEqualToString:@"COPY"]) {
      ok = [self renderStatusResult:_object 
		 withDefaultStatus:201 /* Created */
		 inContext:_ctx];
    }
    break;
  case 'D':
    if ([m isEqualToString:@"DELETE"])
      ok = [self renderDeleteResult:_object inContext:_ctx];
    break;
  case 'G':
    if ([m isEqualToString:@"GET"])
      ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:NO];
    break;
  case 'H':
    if ([m isEqualToString:@"HEAD"])
      ok = [self renderObjectBodyResult:_object inContext:_ctx onlyHead:YES];
    break;
  case 'L':
    if ([m isEqualToString:@"LOCK"])
      ok = [self renderLockToken:_object inContext:_ctx];
    break;
  case 'M':
    if ([m isEqualToString:@"MKCOL"])
      ok = [self renderMkColResult:_object inContext:_ctx];
    else if ([m isEqualToString:@"MOVE"]) {
      ok = [self renderStatusResult:_object 
		 withDefaultStatus:201 /* Created */
		 inContext:_ctx];
    }
    break;
  case 'O':
    if ([m isEqualToString:@"OPTIONS"])
      ok = [self renderOptions:_object inContext:_ctx];
    break;
  case 'P':
    if ([m isEqualToString:@"PUT"])
      ok = [self renderUploadResult:_object inContext:_ctx];
    else if ([m isEqualToString:@"PROPFIND"])
      ok = [self renderSearchResult:_object inContext:_ctx];
    else if ([m isEqualToString:@"PROPPATCH"])
      ok = [self renderPropPatchResult:_object inContext:_ctx];
    else if ([m isEqualToString:@"POLL"])
      ok = [self renderPollResult:_object inContext:_ctx];
    break;
  case 'S':
    if ([m isEqualToString:@"SEARCH"])
      ok = [self renderSearchResult:_object inContext:_ctx];
    else if ([m isEqualToString:@"SUBSCRIBE"])
      ok = [self renderSubscription:_object inContext:_ctx];
    break;
    
  default:
    ok = NO;
    break;
  }
  
  if (ok) [self _fixupResponse:[_ctx response] inContext:_ctx];
  return ok
    ? nil
    : [NSException exceptionWithHTTPStatus:500 /* server error */];
}

- (BOOL)canRenderObject:(id)_object inContext:(WOContext *)_ctx {
  if ([_object isKindOfClass:[NSException class]])
    return NO;
  return YES;
}

- (NSString *)stringForResourceType:(id)_value ofProperty:(NSString *)_prop 
  prefixes:(NSDictionary *)_prefixes
{
  NSString *davNS;

  davNS = [_prefixes objectForKey:@"DAV:"];
  
  if ([_value isKindOfClass:[NSArray class]]) {
    /*
	Use arrays to allow for something like this:
          <collection/>
          <C:todos xmlns:C="urn:ietf:params:xml:ns:caldav"/>
	Item Format:
	  ( TAG )             => tag in DAV: namespace
	  ( TAG, NS )         => tag in NS namespace
	  ( TAG, NS, PREFIX ) => tag in NS namespace with PREFIX
    */
    NSMutableString *ms;
    NSEnumerator *e;
    id item;

    if ([_value count] == 0)
      return nil;
      
    ms = [NSMutableString stringWithCapacity:16];
    e  = [_value objectEnumerator];
    while ((item = [e nextObject]) != nil) {
      unsigned count;

      if (![item isKindOfClass:[NSArray class]]) {
	item = [item stringValue];
	if ([item length] == 0) continue;
	[ms appendFormat:@"<%@:%@ />", davNS, item];
	continue;
      }

      /* process array tags */
	  
      if ((count = [item count]) == 0)
	continue;
	  
      if (count == 1)
	[ms appendFormat:@"<%@:%@ />", davNS, [item objectAtIndex:0]];
      else if (count == 2) {
	/* 0=tag, 1=nsuri */
	[ms appendFormat:@"<%@ xmlns=\"%@\" />", 
	      [item objectAtIndex:0], [item objectAtIndex:1]];
      }
      else {
	/* 0=tag, 1=nsuri, 2=nsprefix */
	[ms appendFormat:@"<%@:%@ xmlns:%@=\"%@\" />", 
	      [item objectAtIndex:2], [item objectAtIndex:0],
	      [item objectAtIndex:2], [item objectAtIndex:1]];
      }
    }
    return ms;
  }
    
  _value = [_value stringValue];
  if ([_value length] == 0) return nil;
  
  return [NSString stringWithFormat:@"<%@:%@/>", davNS, _value];
}
- (NSString *)stringForValue:(id)_value ofProperty:(NSString *)_prop 
  prefixes:(NSDictionary *)_prefixes
{
  /* seems like this is the default date value */
  NSString *datefmt = @"%a, %d %b %Y %H:%M:%S GMT";
  
  if (_value == nil)
    return nil;
  if (![_value isNotNull])
    return nil;
  
  /* special processing for some properties */
  
  if ([_prop isEqualToString:@"{DAV:}resourcetype"]) {
    return [self stringForResourceType:_value ofProperty:_prop
		 prefixes:_prefixes];
  }
  else if ([_prop isEqualToString:@"{DAV:}creationdate"])
    datefmt = @"%Y-%m-%dT%H:%M:%S%zZ";

  /* special processing for some properties  */
  
  // TODO: move this to user-level code ! 
  //   HH: what is that ? it does not do anything anyway ?
  if ([_prop hasPrefix:XMLNS_INTTASK]) {
    if ([_prop hasSuffix:@"}0x00008102"]) {
    }
  }
  
  /* special processing for some classes */
  
  if ([_value isKindOfClass:[NSString class]])
    return [_value stringByEscapingXMLString];
  
  if ([_value isKindOfClass:[NSNumber class]])
    return [_value stringValue];
  
  if ([_value isKindOfClass:[NSDate class]]) {
    return [_value descriptionWithCalendarFormat:datefmt
		   timeZone:gmt
		   locale:nil];
  }
  
  return [[_value stringValue] stringByEscapingXMLString];
}

- (NSString *)baseURLForContext:(WOContext *)_ctx {
  /*
    Note: Evolution doesn't correctly transfer the "Host:" header, it
    misses the port argument :-(
  */
  NSString  *baseURL;
  WORequest *rq;
  NSString  *hostport;
  id tmp;
  
  rq = [_ctx request];
  
  if ((tmp = [rq headerForKey:@"x-webobjects-server-name"])) {
    hostport = tmp;
    if ((tmp = [rq headerForKey:@"x-webobjects-server-port"]))
      hostport = [NSString stringWithFormat:@"%@:%@", hostport, tmp];
  }
  else if ((tmp = [rq headerForKey:@"host"]))
    hostport = tmp;
  else
    hostport = [[NSHost currentHost] name];
  
  baseURL = [NSString stringWithFormat:@"http://%@%@", hostport, [rq uri]];
  return baseURL;
}

- (NSString *)tidyHref:(id)_href baseURL:(id)baseURL {
  NSString *href;
  
  href = [_href stringValue];
  
  if (debugOn) {
    // TODO: this happens if we access using Goliath
    if ([href hasPrefix:@"http:/"] && ![href hasPrefix:@"http://"]) {
      [self logWithFormat:@"BROKEN URL: %@", _href];
      return nil;
    }
  }
  
  if (href == nil) {
    if (debugOn) {
      [self warnWithFormat:
              @"using baseURL for href, entry did not provide a URL: %@",
              baseURL];
    }
    href = [baseURL stringValue];
  }
  else if (![href isAbsoluteURL]) { // maybe only check for http[s]:// ?
    // TODO: use "real" URL processing
    href = [baseURL stringByAppendingPathComponent:href];
  }
  return href;
}
- (id)tidyStatus:(id)stat {
  if (stat == nil)
    stat = @"HTTP/1.1 200 OK";
  else if ([stat isKindOfClass:[NSException class]]) {
    int i;
    
    if ((i = [stat httpStatus]) > 0)
      stat = [NSString stringWithFormat:@"HTTP/1.1 %i %@", i, [stat reason]];
    else {
      stat = [(NSException *)stat name];
      stat = [@"HTTP/1.1 500 " stringByAppendingString:stat];
    }
  }
  return stat;
}

- (void)renderSearchResultEntry:(id)entry inContext:(WOContext *)_ctx 
  namesOnly:(BOOL)_namesOnly
  attributes:(NSArray *)_attrs
  propertyMap:(NSDictionary *)_propMap
  baseURL:(NSString *)baseURL
  tagToPrefix:(NSDictionary *)extNameCache
  nsToPrefix:(NSDictionary *)nsToPrefix
{
  /* Note: the entry is an NSArray in case _namesOnly is requested! */
  // TODO: use -valueForKey: to improve NSNull handling ?
  WOResponse   *r;
  NSEnumerator *keys;
  NSString     *key;
  id   href = nil;
  id   stat = nil;
  BOOL isBrief;
  
  r = [_ctx response];
  isBrief = [[[_ctx request] headerForKey:@"brief"] hasPrefix:@"t"] ? YES : NO;
  
  if (debugOn) {
    [self debugWithFormat:@"    render entry: 0x%08X<%@>%s%s",
	  entry, NSStringFromClass([entry class]),
	  isBrief    ? " brief"      : "",
	  _namesOnly ? " names-only" : ""];
  }

  /* we do not map these DAV properties because they are very special */
  if (!_namesOnly) {
    if ((href = [entry valueForKey:@"{DAV:}href"]) == nil) {
      if ((key = [_propMap objectForKey:@"{DAV:}href"]) != nil) {
        if ((href = [entry valueForKey:key]) == nil) {
          if (debugOn) {
            [self warnWithFormat:
                    @"no value for {DAV:}href key '%@': %@", key, entry];
  	}
        }
      }
      else if (debugOn) {
        [self warnWithFormat:@"no key for {DAV:}href in property map !"];
      }
    }
    if ((stat = [entry valueForKey:@"{DAV:}status"]) == nil) {
      if ((key = [_propMap objectForKey:@"{DAV:}status"]))
        stat = [entry valueForKey:key];
    }

    /* tidy href */
    href = [self tidyHref:href baseURL:baseURL];
    
    /* tidy status */
    stat = [self tidyStatus:stat];
  }
  else { /* propnames only */
    href = [baseURL stringValue];
    stat = @"HTTP/1.1 200 OK";
  }
  
  if (debugOn) {
    [self debugWithFormat:@"    status: %@", stat];
    [self debugWithFormat:@"    href:   %@", href];
  }
  
  /* generate */
  [r appendContentString:@"<D:response>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  if ([href isNotNull]) {
    [r appendContentString:@"<D:href>"];
    /*
      TODO: need to find out what is appropriate! While Cadaver and ZideLook
            (both Neon+Expat) seem to be fine with this, OSX reports invalid
            characters (displayed as '#') for umlauts.
            It might be that we are supposed to use *URL* escaping in any 
            case! (notably entering a directory with an umlaut doesn't seem
            to work in Cadaver either because of a URL mismatch!)
      Note: we cannot apply URL encoding in this place, because it will encode
            all URL special chars ... where are URLs escaped?
      Note: we always need to apply XML escaping (even though higher-level
            characters might be already encoded)!
    */
    [r appendContentXMLString:[href stringValue]];
    [r appendContentString:@"</D:href>"];
    if (formatOutput) [r appendContentCharacter:'\n'];
  }
  else {
    [self warnWithFormat:@"WebDAV result entry has no valid href: %@", entry];
  }
  
  [r appendContentString:@"<D:propstat>"];
  [r appendContentString:@"<D:prop>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  /* now the properties */
  
  keys = [_attrs objectEnumerator] ;
  while ((key = [keys nextObject])) {
    NSString *extName;
    NSString *okey;
    id value;
    
#if 0 /* this filter probably doesn't make sense ? */
    /* filter out predefined props */
    if ([key isEqualToString:@"{DAV:}href"])   continue;
    if ([key isEqualToString:@"{DAV:}status"]) continue;
#endif
    
    extName = [extNameCache objectForKey:key];
    
    if (_namesOnly) {
      [r appendContentCharacter:'<'];
      [r appendContentString:extName];
      [r appendContentString:@"/>"];
      if (formatOutput) [r appendContentCharacter:'\n'];
      continue;
    }
    
    // TODO: we should support property status (eg encode 404 on NSNull)
      
    if ((okey = [_propMap objectForKey:key]) == nil)
      okey = key;
      
    if ([key isEqualToString:@"{DAV:}href"])
      value = href;
    else
      value = [entry valueForKey:okey];
    
    if ([value isNotNull]) {
      NSString *s;
	
      if ([value isKindOfClass:[SoWebDAVValue class]]) {
	  s = [value stringForTag:key rawName:extName
		     inContext:_ctx prefixes:nsToPrefix];
	  [r appendContentString:s];
      }
      else {
	  [r appendContentCharacter:'<'];
	  [r appendContentString:extName];
	  [r appendContentCharacter:'>'];
          
	  s = [self stringForValue:value ofProperty:key prefixes:nsToPrefix];
	  [r appendContentString:s];
          
	  [r appendContentString:@"</"];
	  [r appendContentString:extName];
	  [r appendContentString:@">"];
          if (formatOutput) [r appendContentCharacter:'\n'];
      }
      continue;
    }
    
    if (!isBrief) { 
      /* 
         Not sure whether this is correct, do we need to encode null attrs?
         Seems like Evo gets confused on that.
         TODO: probably add a 404 property status for that!
      */
      [r appendContentCharacter:'<'];
      [r appendContentString:extName];
      [r appendContentString:@"/>"];
      if (formatOutput) [r appendContentCharacter:'\n'];
    }
  }
      
  [r appendContentString:@"</D:prop>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  if (stat != nil) {
    [r appendContentString:@"<D:status>"];
    [r appendContentXMLString:[stat stringValue]];
    [r appendContentString:@"</D:status>"];
  }
  
  [r appendContentString:@"</D:propstat></D:response>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
}

- (void)buildPrefixMapForAttributes:(NSArray *)_attrs
  tagToExtName:(NSMutableDictionary *)_tagToExtName
  nsToPrefix:(NSMutableDictionary *)_nsToPrefix
{
  unichar autoPrefix[2] = { ('a' - 1), 0 };
  NSEnumerator *e;
  NSString     *fqn;
  
  e = [_attrs objectEnumerator];
  while ((fqn = [e nextObject])) {
    NSString *ns, *localName, *prefix, *extName;
    
    if ([_tagToExtName objectForKey:fqn]) continue;
    
    if (![fqn xmlIsFQN]) {
      /* hm, no namespace given :-(, using DAV */
      ns        = @"DAV:";
      localName = fqn;
    }
    else {
      ns        = [fqn xmlNamespaceURI];
      localName = [fqn xmlLocalName];
    }
    
    if ((prefix = [_nsToPrefix objectForKey:ns]) == nil) {
      if ((prefix = [self preferredPrefixForNamespace:ns]) == nil) {
	(autoPrefix[0])++;
	prefix = [NSString stringWithCharacters:&(autoPrefix[0]) length:1];
      }
      [_nsToPrefix setObject:prefix forKey:ns];
    }
    
    extName = [NSString stringWithFormat:@"%@:%@", prefix, localName];
    [_tagToExtName setObject:extName forKey:fqn];
  }
}

- (NSString *)nsDeclsForMap:(NSDictionary *)_nsToPrefix {
  NSMutableString *ms;
  NSEnumerator *nse;
  NSString *ns;
  
  ms = [NSMutableString stringWithCapacity:256];
  nse = [_nsToPrefix keyEnumerator];
  while ((ns = [nse nextObject])) {
    [ms appendString:@" xmlns:"];
    [ms appendString:[_nsToPrefix objectForKey:ns]];
    [ms appendString:@"=\""];
    [ms appendString:ns];
    [ms appendString:@"\""];
  }
  return ms;
}

- (void)renderSearchResult:(id)_entries inContext:(WOContext *)_ctx 
  namesOnly:(BOOL)_namesOnly
  attributes:(NSArray *)_attrs
  propertyMap:(NSDictionary *)_propMap
{
  NSMutableDictionary *extNameCache = nil;
  NSMutableDictionary *nsToPrefix   = nil;
  NSAutoreleasePool   *pool;
  WOResponse *r;
  unsigned   entryCount;
  
  pool = [[NSAutoreleasePool alloc] init];
  r = [_ctx response];
  
  if (![_entries isKindOfClass:[NSEnumerator class]]) {
    if ([_entries isKindOfClass:[NSArray class]]) {
      [self debugWithFormat:@"  render %i entries", [_entries count]];
      _entries = [_entries objectEnumerator];
    }
    else {
      [self debugWithFormat:@"  render a single object ..."];
      _entries = [[NSArray arrayWithObject:_entries] objectEnumerator];
    }
  }
  
  /* collect used namespaces */
  
  nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
  [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
  
  /* 
     the extNameCache is used to map fully qualified tag names to their
     prefixed external representation 
  */
  extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
  
  // TODO: only walk attrs, if available
  /*
    Walk all attributes of all entries to collect names. We might be able
    to take a look at just the first record if it is guaranteed, that all
    records have all properties (even if the value is NSNull) ?
  */
  [self buildPrefixMapForAttributes:_attrs
	tagToExtName:extNameCache
	nsToPrefix:nsToPrefix];
  
  /* generate multistatus */
   
  [r setStatus:207 /* multistatus */];
  [r setContentEncoding:NSUTF8StringEncoding];
  [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
  [r setHeader:@"no-cache" forKey:@"pragma"];
  [r setHeader:@"no-cache" forKey:@"cache-control"];
  
  [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
  [r appendContentString:@"<D:multistatus"];
  [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
  [r appendContentString:@">"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  {
    NSString *baseURL;
    NSString *range;
    id entry;
    
    baseURL = [self baseURLForContext:_ctx];
    [self debugWithFormat:@"  baseURL: %@", baseURL];
    
    entryCount = 0; /* Note: this will clash with streamed output later */
    while ((entry = [_entries nextObject])) {
      [self renderSearchResultEntry:entry inContext:_ctx
	    namesOnly:_namesOnly attributes:_attrs propertyMap:_propMap
	    baseURL:baseURL tagToPrefix:extNameCache nsToPrefix:nsToPrefix];
      entryCount++;
    }
    [self debugWithFormat:@"  rendered %i entries", entryCount];
    
    /*
      If we got a "rows" range header, we report back the actual rows
      delivered. Since we do not really support ranges in the moment,
      we just report all rows ... 
      TODO: support for row ranges.
    */
    if ((range = [[[_ctx request] headerForKey:@"range"] stringValue])) {
      /* sample: "Content-Range: rows 0-143; total=144" */
      NSString *v;
      
      v = [[NSString alloc] initWithFormat:@"rows 0-%i; total=%i", 
                              entryCount>0?(entryCount - 1):0, entryCount];
      [r setHeader:v forKey:@"content-range"];
      [v release];
    }
  }
  [r appendContentString:@"</D:multistatus>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  [pool release];
}

- (BOOL)renderSearchResult:(id)_object inContext:(WOContext *)_ctx {
  EOFetchSpecification *fs;
  NSDictionary *propMap;
  
  if ((fs = [_ctx objectForKey:@"DAVFetchSpecification"]) == nil)
    return NO;
  
  if ((propMap = [_ctx objectForKey:@"DAVPropertyMap"]) == nil)
    propMap = [_object davAttributeMapInContext:_ctx];

  if (debugOn) {
    [self debugWithFormat:@"render search result 0x%08X<%@>",
            _object, NSStringFromClass([_object class])];
  }
  
  [self renderSearchResult:_object inContext:_ctx
	namesOnly:[fs queryWebDAVPropertyNamesOnly]
	attributes:[fs selectedWebDAVPropertyNames]
	propertyMap:propMap];
  
  if (debugOn) 
    [self debugWithFormat:@"finished rendering."];
  return YES;
}

- (BOOL)renderLockToken:(id)_object inContext:(WOContext *)_ctx {
  /* TODO: this is a fake ! */
  WOResponse *r;
  
  if (_object == nil) return NO;
  
  r = [_ctx response];
  
  [r setStatus:200 /* OK */];
  [r setContentEncoding:NSUTF8StringEncoding];
  [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
  [r setHeader:[_object stringValue]          forKey:@"lock-token"];
  [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
  [r appendContentString:@"<D:prop xmlns:D=\"DAV:\">"];
  [r appendContentString:@"<D:lockdiscovery>"];
  [r appendContentString:@"<D:activelock>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  /* this is the href of the lock, not of the locked resource */
  [r appendContentString:@"<D:locktoken><D:href>"];
  [r appendContentString:[_object stringValue]];
  [r appendContentString:@"</D:href></D:locktoken>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  // TODO: locktype,  eg <D:locktype><D:write/></D:locktype>
  // TODO: lockscope, eg <D:lockscope><D:exclusive/></D:lockscope>
  // TODO: depth,     eg <D:depth>Infinitiy</D:depth>
  // TODO: owner,     eg <D:owner><D:href>...</D:href></D:owner>
  // TODO: timeout,   eg <D:timeout>Second-604800</D:timeout>
  
  [r appendContentString:@"</D:activelock>"];
  [r appendContentString:@"</D:lockdiscovery>"];
  [r appendContentString:@"</D:prop>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  return YES;
}

- (BOOL)renderOptions:(id)_object inContext:(WOContext *)_ctx {
  WOResponse *r;
  
  r = [_ctx response];
  [r setStatus:200 /* OK */];
  [r setHeader:@"1,2" forKey:@"DAV"]; // TODO: select protocol level
  //[r setHeader:@"" forKey:@"Etag"]; 
  
  if (![_object isNotNull])
    ;
  else if ([_object isKindOfClass:[NSArray class]]) {
    /* DEPRECATED */
    [r setHeader:[_object componentsJoinedByString:@", "] forKey:@"allow"];
  }
  else {
    [self logWithFormat:@"ERROR: unexpected options result: %@ (class=%@)", 
	    _object, [_object class]];
  }
  return YES;
}

- (BOOL)renderSubscription:(id)_object inContext:(WOContext *)_ctx {
  // TODO: this is fake, mirrors request
  WOResponse *r = [_ctx response];
  WORequest  *rq;
  NSString   *callback;
  NSString   *notificationType;
  NSString   *lifetime;
  
  rq                = [_ctx request];
  callback          = [rq headerForKey:@"call-back"];
  notificationType  = [rq headerForKey:@"notification-type"];
  lifetime          = [rq headerForKey:@"subscription-lifetime"];
  
  [r setStatus:200 /* OK */];
  if (notificationType != nil)
    [r setHeader:notificationType forKey:@"notification-type"];
  if (lifetime != nil)
    [r setHeader:lifetime         forKey:@"subscription-lifetime"];
  if (callback != nil)
    [r setHeader:callback         forKey:@"callback"];
  [r setHeader:[self baseURLForContext:_ctx] forKey:@"content-location"];
  [r setHeader:_object forKey:@"subscription-id"];
  return YES;
}

- (BOOL)renderPropPatchResult:(id)_object inContext:(WOContext *)_ctx {
  NSMutableDictionary *extNameCache = nil;
  NSMutableDictionary *nsToPrefix   = nil;
  WOResponse *r = [_ctx response];
  
  if (_object == nil) return NO;
  
  nsToPrefix = [NSMutableDictionary dictionaryWithCapacity:16];
  [nsToPrefix setObject:@"D" forKey:XMLNS_WEBDAV];
  extNameCache = [NSMutableDictionary dictionaryWithCapacity:32];
  [self buildPrefixMapForAttributes:_object
	tagToExtName:extNameCache
	nsToPrefix:nsToPrefix];
  
  [r setStatus:207 /* multistatus */];
  [r setContentEncoding:NSUTF8StringEncoding];
  [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];
  [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
  [r appendContentString:@"<D:multistatus"];
  [r appendContentString:[self nsDeclsForMap:nsToPrefix]];
  [r appendContentString:@">"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  [r appendContentString:@"<D:response>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  [r appendContentString:@"<D:href>"];
  [r appendContentString:[[_ctx request] uri]];
  [r appendContentString:@"</D:href>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  [r appendContentString:@"<D:propstat>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  [r appendContentString:@"<D:prop>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  /* encode properties */
  {
    NSEnumerator *e;
    NSString *tag;
    
    e = [_object objectEnumerator];
    while ((tag = [e nextObject])) {
      NSString *extName;
      
      extName = [extNameCache objectForKey:tag];
      [r appendContentCharacter:'<'];
      [r appendContentString:extName];
      [r appendContentString:@"/>"];
      if (formatOutput) [r appendContentCharacter:'\n'];
    }
  }
  
  [r appendContentString:@"</D:prop>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  [r appendContentString:@"<D:status>HTTP/1.1 200 OK</D:status>"];
  [r appendContentString:@"</D:propstat></D:response>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  [r appendContentString:@"</D:multistatus>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  return YES;
}

- (BOOL)renderDeleteResult:(id)_object inContext:(WOContext *)_ctx {
  WOResponse *r = [_ctx response];
  
  if (_object == nil || [_object boolValue]) {
    [r setStatus:204 /* no content */];
    //[r appendContentString:@"object was deleted."];
    return YES;
  }
  
  if ([_object isKindOfClass:[NSNumber class]]) {
    [r setStatus:[_object intValue]];
    if ([r status] != 204 /* No Content */)
      [r appendContentString:@"object could not be deleted."];
  }
  else {
    [r setStatus:500 /* server error */];
    [r appendContentString:@"object could not be deleted. reason: "];
    [r appendContentHTMLString:[_object stringValue]];
  }
  return YES;
}

- (BOOL)renderStatusResult:(id)_object withDefaultStatus:(int)_defStatus
  inContext:(WOContext *)_ctx 
{
  WOResponse *r = [_ctx response];
  
  if (_object == nil) {
    [r setStatus:_defStatus /* no content */];
    return YES;
  }
  
  if ([_object isKindOfClass:[NSNumber class]]) {
    if ([_object intValue] < 100) {
      [r setStatus:_defStatus /* no content */];
      return YES;
    }
    else {
      [r setStatus:[_object intValue]];
    }
  }
  else {
    [r setStatus:_defStatus /* no content */];
  }
  return YES;
}
- (BOOL)renderUploadResult:(id)_object inContext:(WOContext *)_ctx {
  WOResponse *r = [_ctx response];
  
  if (_object == nil) {
    [r setStatus:204 /* no content */];
    return YES;
  }
  
  if ([_object isKindOfClass:[NSNumber class]]) {
    if ([_object intValue] < 100) {
      [r setStatus:204 /* no content */];
      return YES;
    }
    
    [r setStatus:[_object intValue]];
    if ([_object intValue] >= 300) {
      [r setHeader:@"text/html" forKey:@"content-type"];
      [r appendContentString:@"object could not be stored."];
    }
    
    return YES;
  }
  
  [r setStatus:204 /* no content */];
  return YES;
}

- (void)renderPollList:(NSArray *)_sids code:(int)_code
  inContext:(WOContext *)_ctx 
{
  WOResponse   *r = [_ctx response];
  NSEnumerator *e;
  NSString *sid;
  NSString *href;
  
  if ([_sids count] == 0) return;
  href = [self baseURLForContext:_ctx];

  [r appendContentString:@"<D:response>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  [r appendContentString:@"<D:href>"];
  [r appendContentString:href];
  [r appendContentString:@"</D:href>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  [r appendContentString:@"<D:status>HTTP/1.1 "];
  if (_code == 200)
    [r appendContentString:@"200 OK"];
  else if (_code == 204)
    [r appendContentString:@"204 No Content"];
  else {
    NSString *s;
    s = [NSString stringWithFormat:@"%i code%i"];
    [r appendContentString:s];
  }
  [r appendContentString:@"</D:status>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  
  [r appendContentString:@"<E:subscriptionID>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  e = [_sids objectEnumerator];
  while ((sid = [e nextObject])) {
    if (formatOutput) [r appendContentString:@"  "];
    [r appendContentString:@"<li>"];
    [r appendContentString:sid];
    [r appendContentString:@"</li>"];
    if (formatOutput) [r appendContentCharacter:'\n'];
  }
  [r appendContentString:@"</E:subscriptionID>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
  [r appendContentString:@"</D:response>"];
  if (formatOutput) [r appendContentCharacter:'\n'];
}

- (BOOL)renderPollResult:(id)_object inContext:(WOContext *)_ctx {
  WOResponse *r = [_ctx response];
  
  if (_object == nil) {
    [r setStatus:204 /* no content */];
    return YES;
  }

  if ([_object isKindOfClass:[NSDictionary class]]) {
    NSArray  *pending, *inactive;
    
    pending  = [(NSDictionary *)_object objectForKey:@"pending"];
    inactive = [(NSDictionary *)_object objectForKey:@"inactive"];
    
    [r setStatus:207 /* Multi-Status */];
    [r setContentEncoding:NSUTF8StringEncoding];
    [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];

    [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
    [r appendContentString:@"<D:multistatus "];
    [r appendContentString:@" xmlns:D=\"DAV:\""];
    [r appendContentString:
         @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
    [r appendContentString:@">"];
    if (formatOutput) [r appendContentCharacter:'\n'];
    
    [self renderPollList:pending  code:200 inContext:_ctx];
    [self renderPollList:inactive code:204 inContext:_ctx];
    
    [r appendContentString:@"</D:multistatus>"];
    if (formatOutput) [r appendContentCharacter:'\n'];
  }
  else if ([_object isKindOfClass:[NSArray class]]) {
    [r setStatus:207 /* Multi-Status */];
    [r setContentEncoding:NSUTF8StringEncoding];
    [r setHeader:@"text/xml; charset=\"utf-8\"" forKey:@"content-type"];

    [r appendContentString:@"<?xml version=\"1.0\" encoding=\"utf-8\"?>\n"];
    [r appendContentString:@"<D:multistatus "];
    [r appendContentString:@" xmlns:D=\"DAV:\""];
    [r appendContentString:
         @" xmlns:E=\"http://schemas.microsoft.com/Exchange/\""];
    [r appendContentString:@">"];
    if (formatOutput) [r appendContentCharacter:'\n'];
    
    [self renderPollList:_object code:200 inContext:_ctx];
    
    [r appendContentString:@"</D:multistatus>"];
    if (formatOutput) [r appendContentCharacter:'\n'];
  }
  else {
    [r setStatus:204 /* no content */];
    //[r appendContentString:@"object was stored."];
  }
  return YES;
}

- (BOOL)renderMkColResult:(id)_object inContext:(WOContext *)_ctx {
  WOResponse *r = [_ctx response];
  
  if (_object == nil || [_object boolValue]) {
    [r setStatus:201 /* Created */];
    return YES;
  }
  
  if ([_object isKindOfClass:[NSNumber class]]) {
    [r setStatus:[_object intValue]];
    [r appendContentString:@"object could not be created."];
  }
  else {
    [r setStatus:500 /* server error */];
    [r appendContentString:@"object could not be deleted. reason: "];
    [r appendContentHTMLString:[_object stringValue]];
  }
  return YES;
}

/* debugging */

- (BOOL)isDebuggingEnabled {
  return debugOn;
}

@end /* SoWebDAVRenderer */
