// Copyright 1997-2002 Omni Development, Inc.  All rights reserved.
//
// This software may only be used and reproduced according to the
// terms in the file OmniSourceLicense.html, which should be
// distributed with this project and can also be found at
// http://www.omnigroup.com/DeveloperResources/OmniSourceLicense.html.

#import <OWF/OWURL.h>

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>
#import <OmniNetworking/ONHost.h>
#import <ctype.h>

#import "OWContentType.h"
#import "OWNetLocation.h"
#import "OWHTMLToSGMLObjects.h"

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Content.subproj/Address.subproj/OWURL.m,v 1.57 2002/03/09 01:53:51 kc Exp $")

@interface OWURL (Private)
+ (void)controllerDidInitialize:(OFController *)controller;

+ (OWURL *)urlWithLowercaseScheme:(NSString *)aScheme netLocation:(NSString *)aNetLocation path:(NSString *)aPath params:(NSString *)someParams query:(NSString *)aQuery fragment:(NSString *)aFragment;
+ (OWURL *)urlWithLowercaseScheme:(NSString *)aScheme schemeSpecificPart:(NSString *)aSchemeSpecificPart fragment:(NSString *)aFragment;

- _initWithLowercaseScheme:(NSString *)aScheme;
- initWithLowercaseScheme:(NSString *)aScheme netLocation:(NSString *)aNetLocation path:(NSString *)aPath params:(NSString *)someParams query:(NSString *)aQuery fragment:(NSString *)aFragment;
- initWithLowercaseScheme:(NSString *)aScheme schemeSpecificPart:(NSString *)aSchemeSpecificPart fragment:(NSString *)aFragment;
- initWithScheme:(NSString *)aScheme schemeSpecificPart:(NSString *)aSchemeSpecificPart fragment:(NSString *)aFragment;

- (OWURL *)fakeRootURL;

- (void)_locked_parseNetLocation;
@end

@implementation OWURL

static NSArray *fakeRootURLs = nil;
static NSLock *fakeRootURLsLock;
static OFLowercaseStringCache lowercaseSchemeCache;
static NSArray *shortTopLevelDomains = nil;

// These are carefully derived from RFC1808.
// (http://www.w3.org/hypertext/WWW/Addressing/rfc1808.txt)

static OFCharacterSet *SchemeDelimiterOFCharacterSet;
static OFCharacterSet *NetLocationDelimiterOFCharacterSet;
static OFCharacterSet *PathDelimiterOFCharacterSet;
static OFCharacterSet *ParamDelimiterOFCharacterSet;
static OFCharacterSet *QueryDelimiterOFCharacterSet;
static OFCharacterSet *FragmentDelimiterOFCharacterSet;
static OFCharacterSet *SchemeSpecificPartDelimiterOFCharacterSet;
static OFCharacterSet *NonWhitespaceOFCharacterSet;
static NSMutableDictionary *ContentTypeDictionary;
static OFSimpleLockType ContentTypeDictionarySimpleLock;
static NSMutableSet *SecureSchemes;
static OFSimpleLockType SecureSchemesSimpleLock;
static BOOL NetscapeCompatibleRelativeAddresses;

#define NonBuggyCharacterSet NSMutableCharacterSet
    // NSCharacterSet was very buggy, so we switched to NSMutableCharacterSet instead.

+ (void)initialize;
{
    NSCharacterSet *AlphaSet, *DigitSet, *ReservedSet;
    NSMutableCharacterSet *UnreservedSet, *UCharSet, *PCharSet;
    NSMutableCharacterSet *SchemeSet, *NetLocationSet, *PathSet;
    NSMutableCharacterSet *ParamSet, *QuerySet, *FragmentSet;
    NSMutableCharacterSet *SchemeSpecificPartSet;

    OBINITIALIZE;

    OFLowercaseStringCacheInit(&lowercaseSchemeCache);
    
    AlphaSet = [NonBuggyCharacterSet letterCharacterSet];
    DigitSet = [NonBuggyCharacterSet characterSetWithCharactersInString:@"0123456789"];
    ReservedSet = [NonBuggyCharacterSet characterSetWithCharactersInString:@";/?:@&="];

    // This is a bit richer than the standard allows
    UnreservedSet = [[ReservedSet invertedSet] mutableCopy];
    [UnreservedSet removeCharactersInString:@"%#"];

    UCharSet = [[NSMutableCharacterSet alloc] init];
    [UCharSet formUnionWithCharacterSet:UnreservedSet];
    [UCharSet addCharactersInString:@"%"]; // escapes

    PCharSet = [[NSMutableCharacterSet alloc] init];
    [PCharSet formUnionWithCharacterSet:UCharSet];
    [PCharSet addCharactersInString:@":@&="];

    SchemeSet = [[NSMutableCharacterSet alloc] init];
    [SchemeSet formUnionWithCharacterSet:AlphaSet];
    [SchemeSet formUnionWithCharacterSet:DigitSet];
    [SchemeSet addCharactersInString:@"+-."];

    NetLocationSet = [[NSMutableCharacterSet alloc] init];
    [NetLocationSet formUnionWithCharacterSet:PCharSet];
    [NetLocationSet addCharactersInString:@";?"];
    [NetLocationSet removeCharactersInString:@"\\"]; // stupid backslash paths found on some sites

    PathSet = [[NSMutableCharacterSet alloc] init];
    [PathSet formUnionWithCharacterSet:PCharSet];
    [PathSet addCharactersInString:@"/"];

    ParamSet = [[NSMutableCharacterSet alloc] init];
    [ParamSet formUnionWithCharacterSet:PCharSet];
    [ParamSet addCharactersInString:@"/"];
    [ParamSet addCharactersInString:@";"];

    QuerySet = [[NSMutableCharacterSet alloc] init];
    [QuerySet formUnionWithCharacterSet:UCharSet];
    [QuerySet formUnionWithCharacterSet:ReservedSet];

    FragmentSet = [QuerySet retain];
    SchemeSpecificPartSet = [QuerySet retain];

    // Now, get the OFCharacterSet *representations of all those character sets
#define delimiterBitmapForSet(ofSet, set) { ofSet = [[OFCharacterSet alloc] initWithCharacterSet:set]; [ofSet invert]; }
    delimiterBitmapForSet(SchemeDelimiterOFCharacterSet, SchemeSet);
    delimiterBitmapForSet(NetLocationDelimiterOFCharacterSet, NetLocationSet);
    delimiterBitmapForSet(PathDelimiterOFCharacterSet, PathSet);
    delimiterBitmapForSet(ParamDelimiterOFCharacterSet, ParamSet);
    delimiterBitmapForSet(QueryDelimiterOFCharacterSet, QuerySet);
    delimiterBitmapForSet(FragmentDelimiterOFCharacterSet, FragmentSet);
    delimiterBitmapForSet(SchemeSpecificPartDelimiterOFCharacterSet, SchemeSpecificPartSet);
    delimiterBitmapForSet(NonWhitespaceOFCharacterSet, [NSCharacterSet whitespaceAndNewlineCharacterSet]);
#undef delimiterBitmapForSet

    [SchemeSet release];
    [NetLocationSet release];
    [PathSet release];
    [ParamSet release];
    [QuerySet release];
    [FragmentSet release];
    [SchemeSpecificPartSet release];

    OFSimpleLockInit(&ContentTypeDictionarySimpleLock);
    ContentTypeDictionary = [[NSMutableDictionary alloc] init];
    OFSimpleLockInit(&SecureSchemesSimpleLock);
    SecureSchemes = [[NSMutableSet alloc] init];

    fakeRootURLsLock = [[NSLock alloc] init];
}

+ (void)didLoad;
{
    [[OFController sharedController] addObserver:self];
}

+ (void)readDefaults;
{
    NSUserDefaults *userDefaults;
    NSArray *fakeRootURLStrings;
    unsigned int fakeRootCount;

    userDefaults = [NSUserDefaults standardUserDefaults];
    [shortTopLevelDomains release];
    shortTopLevelDomains = [[userDefaults arrayForKey:@"OWShortTopLevelDomains"] retain];
    NetscapeCompatibleRelativeAddresses = [userDefaults boolForKey:@"OWURLNetscapeCompatibleRelativeAddresses"];

    // Don't override the URL encoding --- the draft standard for internationalized URLs specifies the use of UTF-8. (Previously we used the user's default encoding as a way to guess what their favorite server might be expecting, but I'm not sure that ever helped anyone.)
#if 0
    // OmniFoundation doesn't have its own defaults, so we'll register this here
    newURLEncoding = [OWDataStreamCharacterProcessor stringEncodingForDefault:[userDefaults stringForKey:@"OWOutgoingStringEncoding"]];
    if (newURLEncoding != kCFStringEncodingInvalidId)
        [NSString setURLEncoding:newURLEncoding];
    else
        [NSString setURLEncoding:[OWDataStreamCharacterProcessor defaultStringEncoding]];
#endif

    fakeRootURLStrings = [userDefaults arrayForKey:@"OWURLFakeRootURLs"];
    fakeRootCount = [fakeRootURLStrings count];
    if (fakeRootCount > 0) {
        unsigned int fakeRootIndex;
        NSMutableArray *newFakeRootURLs;

        newFakeRootURLs = [[NSMutableArray alloc] initWithCapacity:fakeRootCount];
        for (fakeRootIndex = 0; fakeRootIndex < fakeRootCount; fakeRootIndex++) {
            [newFakeRootURLs addObject:[self urlFromDirtyString:[fakeRootURLStrings objectAtIndex:fakeRootIndex]]];
        }
        [fakeRootURLsLock lock];
        [fakeRootURLs release];
        fakeRootURLs = [[NSArray alloc] initWithArray:newFakeRootURLs];
        [fakeRootURLsLock unlock];
        [newFakeRootURLs release];
    } else {
        [fakeRootURLsLock lock];
        [fakeRootURLs release];
        fakeRootURLs = nil;
        [fakeRootURLsLock unlock];
    }
}


+ (OWURL *)urlWithScheme:(NSString *)aScheme netLocation:(NSString *)aNetLocation path:(NSString *)aPath params:(NSString *)someParams query:(NSString *)aQuery fragment:(NSString *)aFragment;
{
    return [self urlWithLowercaseScheme:OFLowercaseStringCacheGet(&lowercaseSchemeCache, aScheme) netLocation:aNetLocation path:aPath params:someParams query:aQuery fragment:aFragment];
}

+ (OWURL *)urlWithScheme:(NSString *)aScheme schemeSpecificPart:(NSString *)aSchemeSpecificPart fragment:(NSString *)aFragment;
{
    return [self urlWithLowercaseScheme:OFLowercaseStringCacheGet(&lowercaseSchemeCache, aScheme) schemeSpecificPart:aSchemeSpecificPart fragment:aFragment];
}

+ (OWURL *)urlFromString:(NSString *)aString;
{
    NSString *aScheme, *aNetLocation;
    NSString *aPath, *someParams;
    NSString *aQuery, *aFragment;
    NSString *aSchemeSpecificPart;
    OFStringScanner *scanner;

    if (!aString || [aString length] == 0)
	return nil;

    scanner = [[OFStringScanner alloc] initWithString:aString];
    scannerScanUpToCharacterInOFCharacterSet(scanner, NonWhitespaceOFCharacterSet);
    aScheme = [scanner readFullTokenWithDelimiterOFCharacterSet:SchemeDelimiterOFCharacterSet forceLowercase:YES];
    if (!aScheme || [aScheme length] == 0 ||
        scannerReadCharacter(scanner) != ':') {
        [scanner release];
        return nil;
    }
    if (scannerPeekCharacter(scanner) == '/') {
        // Scan net location or path
        BOOL pathPresent;

        scannerSkipPeekedCharacter(scanner);
        if (scannerPeekCharacter(scanner) == '/') {
            // Scan net location
            scannerSkipPeekedCharacter(scanner);
            aNetLocation = [scanner readFullTokenWithDelimiterOFCharacterSet:NetLocationDelimiterOFCharacterSet forceLowercase:NO];
            if (aNetLocation && [aNetLocation length] == 0)
                aNetLocation = @"localhost";
            pathPresent = scannerPeekCharacter(scanner) == '/' || scannerPeekCharacter(scanner) == '\\'; // some stupid sites use backslash as path delimeters
            if (pathPresent) {
                // To be consistent with the non-netLocation case, skip the '/' here, too
                scannerSkipPeekedCharacter(scanner);
            }
        } else {
            aNetLocation = nil;
            pathPresent = YES;
        }
        if (pathPresent) {
            // Scan path
            aPath = [scanner readFullTokenWithDelimiterOFCharacterSet:PathDelimiterOFCharacterSet forceLowercase:NO];
        } else {
            aPath = nil;
        }
    } else {
        // No net location
        aNetLocation = nil;
        if (scannerPeekCharacter(scanner) == '~') {
            // Scan path that starts with '~'
            //
            // I'm not sure this is actually a path URL, maybe URLs with this
            // form should just drop through to schemeSpecificParams
            aPath = [scanner readFullTokenWithDelimiterOFCharacterSet:PathDelimiterOFCharacterSet forceLowercase:NO];
        } else {
            // No path
            aPath = nil;
        }
    }

    if (scannerPeekCharacter(scanner) == ';') {
        // Scan params
        scannerSkipPeekedCharacter(scanner);
        someParams = [scanner readFullTokenWithDelimiterOFCharacterSet:ParamDelimiterOFCharacterSet forceLowercase:NO];
    } else {
        someParams = nil;
    }

    if (scannerPeekCharacter(scanner) == '?') {
        // Scan query
        scannerSkipPeekedCharacter(scanner);
        aQuery = [scanner readFullTokenWithDelimiterOFCharacterSet:QueryDelimiterOFCharacterSet forceLowercase:NO];
        if (!aQuery)
            aQuery = @"";
    } else {
        aQuery = nil;
    }

    if (!aNetLocation && !aPath && !someParams && !aQuery) {
        // Scan scheme-specific part
        aSchemeSpecificPart = [scanner readFullTokenWithDelimiterOFCharacterSet:SchemeSpecificPartDelimiterOFCharacterSet forceLowercase:NO];
    } else {
        aSchemeSpecificPart = nil;
    }

    if (scannerPeekCharacter(scanner) == '#') {
        // Scan fragment
        scannerSkipPeekedCharacter(scanner);
        aFragment = [scanner readFullTokenWithDelimiterOFCharacterSet:FragmentDelimiterOFCharacterSet forceLowercase:NO];
    } else {
        aFragment = nil;
    }

    [scanner release];

    if (aSchemeSpecificPart)
	return [self urlWithLowercaseScheme:aScheme schemeSpecificPart:aSchemeSpecificPart fragment:aFragment];
    return [self urlWithLowercaseScheme:aScheme netLocation:aNetLocation path:aPath params:someParams query:aQuery fragment:aFragment];
}

+ (OWURL *)urlFromDirtyString:(NSString *)aString;
{
    return [self urlFromString:[self cleanURLString:aString]];
}

+ (NSString *)cleanURLString:(NSString *)aString;
{
    if (aString == nil || [aString length] == 0)
	return nil;
    aString = [aString stringByRemovingSurroundingWhitespace];
    if ([aString hasPrefix:@"<"])
	aString = [aString substringFromIndex:1];
    if ([aString hasSuffix:@">"])
	aString = [aString substringToIndex:[aString length] - 1];
    if ([aString hasPrefix:@"URL:"])
	aString = [aString substringFromIndex:4];
    if ([aString containsString:@"\n"]) {
	NSArray *lines;
	NSMutableString *newString;
	unsigned int lineIndex, lineCount;

	newString = [[[NSMutableString alloc] initWithCapacity:[aString length]] autorelease];
	lines = [aString componentsSeparatedByString:@"\n"];
	lineCount = [lines count];
	for (lineIndex = 0; lineIndex < lineCount; lineIndex++)
	    [newString appendString:[[lines objectAtIndex:lineIndex] stringByRemovingSurroundingWhitespace]];
	aString = newString;
    }
    return aString;
}

// Backwards compatibility methods -- this stuff is in NSString now
+ (void)setURLEncoding:(CFStringEncoding)newURLEncoding;
{
    [NSString setURLEncoding: newURLEncoding];
}

+ (CFStringEncoding)urlEncoding
{
    return [NSString urlEncoding];
}

+ (NSString *)decodeURLString:(NSString *)encodedString encoding:(CFStringEncoding)thisUrlEncoding;
{
    return [NSString decodeURLString:encodedString encoding:thisUrlEncoding];
}

+ (NSString *)decodeURLString:(NSString *)encodedString;
{
    return [NSString decodeURLString:encodedString encoding:[NSString urlEncoding]];
}

+ (NSString *)encodeURLString:(NSString *)unencodedString asQuery:(BOOL)asQuery leaveSlashes:(BOOL)leaveSlashes leaveColons:(BOOL)leaveColons;
{
    return [NSString encodeURLString:unencodedString encoding:[NSString urlEncoding] asQuery:asQuery leaveSlashes:leaveSlashes leaveColons:leaveColons];
}

+ (NSString *)encodeURLString:(NSString *)unencodedString encoding:(CFStringEncoding)thisUrlEncoding asQuery:(BOOL)asQuery leaveSlashes:(BOOL)leaveSlashes leaveColons:(BOOL)leaveColons;
{
    return [NSString encodeURLString:unencodedString encoding:thisUrlEncoding asQuery:asQuery leaveSlashes:leaveSlashes leaveColons:leaveColons];
}

//

+ (OWContentType *)contentTypeForScheme:(NSString *)aScheme;
{
    OWContentType *aContentType;

    OFSimpleLock(&ContentTypeDictionarySimpleLock);
    aContentType = [ContentTypeDictionary objectForKey:aScheme];
    if (!aContentType) {
	aContentType = [OWContentType contentTypeForString:[@"url/" stringByAppendingString:aScheme]];
	[ContentTypeDictionary setObject:aContentType forKey:aScheme];
    }
    OFSimpleUnlock(&ContentTypeDictionarySimpleLock);
    return aContentType;
}

+ (void)registerSecureScheme:(NSString *)aScheme;
{
    OFSimpleLock(&SecureSchemesSimpleLock);
    [SecureSchemes addObject:aScheme];
    OFSimpleUnlock(&SecureSchemesSimpleLock);
}

+ (NSArray *)pathComponentsForPath:(NSString *)aPath;
{
    if (!aPath)
        return nil;

    return [aPath componentsSeparatedByString:@"/"];
}

+ (NSString *)lastPathComponentForPath:(NSString *)aPath;
{
    NSRange lastSlashRange;
    unsigned int originalLength, lengthMinusTrailingSlash;
    
    if (!aPath)
        return nil;

    originalLength = [aPath length];

    // If the last character is a slash, ignore it.
    if (originalLength > 0 && [aPath characterAtIndex:originalLength - 1] == '/')
        lengthMinusTrailingSlash = originalLength - 1;
    else
        lengthMinusTrailingSlash = originalLength;

    // If the path (minus any trailing slash) is empty, return an empty string
    if (lengthMinusTrailingSlash == 0)
        return @"";

    // Find the last slash in the path
    lastSlashRange = [aPath rangeOfString:@"/" options:NSLiteralSearch | NSBackwardsSearch range:NSMakeRange(0, lengthMinusTrailingSlash - 1)];

    // If there is none, return the existing path (minus trailing slash).
    if (lastSlashRange.length == 0)
        return originalLength == lengthMinusTrailingSlash ? aPath : [aPath substringToIndex:lengthMinusTrailingSlash];

    // Return the substring between the last slash and the end of the string (ignoring any trailing slash)
    return [aPath substringWithRange:NSMakeRange(NSMaxRange(lastSlashRange), lengthMinusTrailingSlash - NSMaxRange(lastSlashRange))];
}

+ (NSString *)stringByDeletingLastPathComponentFromPath:(NSString *)aPath;
{
    NSRange lastSlashRange;

    if (!aPath)
        return nil;

    lastSlashRange = [aPath rangeOfString:@"/" options:NSLiteralSearch | NSBackwardsSearch];
    if (lastSlashRange.length == 0)
        return @"";
    if (lastSlashRange.location == 0 && [aPath length] > 1)
        return @"/";
    return [aPath substringToIndex:lastSlashRange.location];
}

+ (unsigned int)minimumDomainComponentsForTopLevelDomain:(NSString *)aTopLevelDomain;
{
    // omnigroup.com vs. omnigroup.co.uk
    return [shortTopLevelDomains containsObject:aTopLevelDomain] ? 2 : 3;
}

+ (NSString *)domainForHostname:(NSString *)hostname;
{
    NSString *domain;
    NSArray *domainComponents;
    unsigned int domainComponentCount;
    unsigned int minimumDomainComponents;

    if (hostname == nil)
        return nil;
    domain = hostname;
    if ([ONHost isDottedQuadString:domain])
        return domain; // 198.151.161.1's domain is 198.151.161.1, not 151.161.1
    domainComponents = [domain componentsSeparatedByString:@"."];
    domainComponentCount = [domainComponents count];
    minimumDomainComponents = [OWURL minimumDomainComponentsForTopLevelDomain:[domainComponents lastObject]];
    if (domainComponentCount > minimumDomainComponents)
        domain = [[domainComponents subarrayWithRange:NSMakeRange(domainComponentCount - minimumDomainComponents, minimumDomainComponents)] componentsJoinedByString:@"."];
    return domain;
}

- (void)dealloc;
{
    [scheme release];
    [netLocation release];
    [path release];
    [params release];
    [query release];
    [fragment release];
    [schemeSpecificPart release];
    OFSimpleLockFree(&derivedAttributesSimpleLock);
    [_cachedCompositeString release];
    [_cachedShortDisplayString release];
    [_cachedParsedNetLocation release];
    [_cacheKey release];
    [super dealloc];
}

- (NSString *)scheme;
{
    return scheme;
}

- (NSString *)netLocation;
{
    return netLocation;
}

- (NSString *)path;
{
    return path;
}

- (NSString *)params;
{
    return params;
}

- (NSString *)query;
{
    return query;
}

- (NSString *)fragment;
{
    return fragment;
}

- (NSString *)schemeSpecificPart;
{
    return schemeSpecificPart;
}

- (NSString *)compositeString;
{
    OFSimpleLock(&derivedAttributesSimpleLock);
    if (_cachedCompositeString == nil) {
        NSMutableString *compositeString;

        compositeString = [[NSMutableString alloc] initWithString:scheme];
        [compositeString appendString:@":"];
    
        if (schemeSpecificPart) {
            [compositeString appendString:schemeSpecificPart];
        } else {
            if (netLocation != nil || [scheme isEqualToString:@"file"])
                [compositeString appendString:@"//"];
            if (netLocation != nil)
                [compositeString appendString:netLocation];
            [compositeString appendString:@"/"];
            if (path != nil)
                [compositeString appendString:path];
            if (params != nil) {
                [compositeString appendString:@";"];
                [compositeString appendString:params];
            }
            if (query != nil) {
                [compositeString appendString:@"?"];
                [compositeString appendString:query];
            }
        }
        if (fragment != nil) {
            [compositeString appendString:@"#"];
            [compositeString appendString:fragment];
        }
    
        // Make the cachedCompositeString immutable so that others will be able to just retain it rather than making their own immutable copy
        _cachedCompositeString = [compositeString copy];
        [compositeString release];
    }
    OFSimpleUnlock(&derivedAttributesSimpleLock);
    return _cachedCompositeString;
}

- (NSString *)cacheKey;
{
    OFSimpleLock(&derivedAttributesSimpleLock);
    if (_cacheKey == nil) {
        NSMutableString *key;
    
        key = [[NSMutableString alloc] initWithString:scheme];
        [key appendString:@":"];
    
        if (schemeSpecificPart) {
            [key appendString:schemeSpecificPart];
        } else {
            if (netLocation != nil || [scheme isEqualToString:@"file"])
                [key appendString:@"//"];
            if (netLocation)
                [key appendString:netLocation];
            [key appendString:@"/"];
            if (path)
                [key appendString:path];
            if (params) {
                [key appendString:@";"];
                [key appendString:params];
            }
            if (query) {
                [key appendString:@"?"];
                [key appendString:query];
            }
        }
    
        // Make the cacheKey immutable so that others will be able to just retain it rather than making their own immutable copy.
        _cacheKey = [key copyWithZone:[self zone]];
        [key release];
    }
    OFSimpleUnlock(&derivedAttributesSimpleLock);
    return _cacheKey;
}

// This is possibly not the best name or this method.  Basically this is just the code from -compositeString except we don't append the path, params, query or fragment.  This is used in OmniWeb in the address completion code.
- (NSString *)stringToNetLocation;
{
    NSMutableString *string;

    string = [[NSMutableString alloc] initWithString:scheme];
    [string appendString:@":"];

    if (!schemeSpecificPart) {
	if (netLocation) {
	    [string appendString:@"//"];
	    [string appendString:netLocation];
	}
	[string appendString:@"/"];
    }
    
    return string;
}

- (NSString *)fetchPath;
{
    NSMutableString *fetchPath;

    fetchPath = [NSMutableString stringWithCapacity:[path length] + 1];

    if (schemeSpecificPart) {
	[fetchPath appendString:schemeSpecificPart];
    } else {
	[fetchPath appendString:@"/"];
	if (path) {
	    if (NetscapeCompatibleRelativeAddresses && [path containsString:@".."]) {
                OWURL *siteURL, *resolvedPathURL;

                // Not the most efficient process, but I think it should work, and hopefully this happens rarely.  I didn't want to go to the trouble of abstracting out all that relative path code from -urlFromRelativeString:.
                siteURL = [self urlFromRelativeString:@"/"];
                resolvedPathURL = [siteURL urlFromRelativeString:path];
                [fetchPath appendString:[resolvedPathURL path]];
            } else
		[fetchPath appendString:path];
	}
	if (params) {
	    [fetchPath appendString:@";"];
	    [fetchPath appendString:params];
	}
	if (query) {
	    [fetchPath appendString:@"?"];
	    [fetchPath appendString:query];
	}
    }
    return fetchPath;
}

- (NSString *)proxyFetchPath;
{
    NSMutableString *proxyFetchPath;

    // Yes, this ends up looking a lot like our -cacheKey, except we're calling -fetchPath so the NetscapeCompatibleRelativeAddresses preference will kick in (and we don't want it to kick in for our -cacheKey because it's relatively expensive and -cacheKey gets called a lot more).

    proxyFetchPath = [[NSMutableString alloc] initWithString:scheme];
    [proxyFetchPath appendString:@":"];
    if (netLocation) {
        [proxyFetchPath appendString:@"//"];
        [proxyFetchPath appendString:netLocation];
    }
    [proxyFetchPath appendString:[self fetchPath]];
    return [proxyFetchPath autorelease];
}

- (NSArray *)pathComponents;
{
    return [OWURL pathComponentsForPath:path];
}

- (NSString *)lastPathComponent;
{
    return [OWURL lastPathComponentForPath:path];
}

- (NSString *)stringByDeletingLastPathComponent;
{
    return [OWURL stringByDeletingLastPathComponentFromPath:path];
}

- (OWNetLocation *)parsedNetLocation;
{
    OFSimpleLock(&derivedAttributesSimpleLock);
    if (_cachedParsedNetLocation == nil)
        [self _locked_parseNetLocation];
    OFSimpleUnlock(&derivedAttributesSimpleLock);

    return _cachedParsedNetLocation;
}

- (NSString *)hostname;
{
    return [[self parsedNetLocation] hostname];
}

- (NSString *)domain;
{
    return [OWURL domainForHostname:[[self parsedNetLocation] hostname]];
}

- (NSString *)shortDisplayString;
{
    OFSimpleLock(&derivedAttributesSimpleLock);
    if (_cachedShortDisplayString == nil) {
        NSMutableString *shortDisplayString;
    
        shortDisplayString = [[NSMutableString alloc] init];
        if (netLocation) {
            if (_cachedParsedNetLocation == nil)
                [self _locked_parseNetLocation];
            [shortDisplayString appendString:[_cachedParsedNetLocation shortDisplayString]];
            [shortDisplayString appendString:[NSString horizontalEllipsisString]];
        } else {
            [shortDisplayString appendString:scheme];
            [shortDisplayString appendString:@":"];
        }
        
        if (path) {
            [shortDisplayString appendString:[self lastPathComponent]];
            if ([path hasSuffix:@"/"])
                [shortDisplayString appendString:@"/"];
        }
        if (params) {
            [shortDisplayString appendString:@";"];
            [shortDisplayString appendString:params];
        }
        if (query) {
            [shortDisplayString appendString:@"?"];
            [shortDisplayString appendString:query];
        }
        if (fragment) {
            [shortDisplayString appendString:@"#"];
            [shortDisplayString appendString:fragment];
        }
        // Make the cacheKey immutable so that others will be able to just retain it rather than making their own immutable copy.
        _cachedShortDisplayString = [shortDisplayString copyWithZone:[self zone]];
    }
    OFSimpleUnlock(&derivedAttributesSimpleLock);
    return _cachedShortDisplayString;
}

//

- (unsigned)hash;
{
    return [[self compositeString] hash];
}

- (BOOL)isEqual:(id)anObject;
{
    OWURL *otherURL;

    if (self == anObject)
	return YES;
    if (anObject == nil)
        return NO;
    otherURL = anObject;
    if (otherURL->isa != isa)
	return NO;
    if (_cachedCompositeString == nil)
	[self compositeString];
    if (otherURL->_cachedCompositeString == nil)
	[otherURL compositeString];
    return [_cachedCompositeString isEqualToString:otherURL->_cachedCompositeString];
}

- (OWContentType *)contentType;
{
    OFSimpleLock(&derivedAttributesSimpleLock);
    if (_contentType == nil)
	_contentType = [OWURL contentTypeForScheme:scheme];
    OFSimpleUnlock(&derivedAttributesSimpleLock);
    return _contentType;
}

- (BOOL)isSecure;
{
    BOOL isSecure;

    OFSimpleLock(&SecureSchemesSimpleLock);
    isSecure = [SecureSchemes containsObject:scheme];
    OFSimpleUnlock(&SecureSchemesSimpleLock);
    return isSecure;
}

//

- (OWURL *)urlFromRelativeString:(NSString *)aString;
{
    OWURL *absoluteURL;
    NSString *aNetLocation;
    NSString *aPath, *someParams, *aQuery, *aFragment;
    OFStringScanner *scanner;

    absoluteURL = [OWURL urlFromString:aString];
    if (absoluteURL) {
        if (schemeSpecificPart) {
            // If our scheme uses a non-uniform URL syntax, relative URLs are illegal
            return absoluteURL;
        }

        if (NetscapeCompatibleRelativeAddresses && [scheme isEqualToString:[absoluteURL scheme]] && ![absoluteURL netLocation]) {
            NSString *otherFetchPath, *otherFragment;

            // For Netscape compatibility, treat "http:whatever" as a relative link to "whatever".

            otherFetchPath = [absoluteURL fetchPath];
            otherFragment = [absoluteURL fragment];
            if (otherFragment)
                aString = [NSString stringWithFormat:@"%@#%@", otherFetchPath, otherFragment];
            else
                aString = otherFetchPath;
            absoluteURL = nil;
        } else {
            return absoluteURL;
        }
    }

    if (!aString || [aString length] == 0)
	return self;

    // Relative URLs default to the current location
    aNetLocation = netLocation;
    aPath = path;
    someParams = params;
    aQuery = query;
    aFragment = fragment;

    scanner = [[OFStringScanner alloc] initWithString:aString];
    scannerScanUpToCharacterInOFCharacterSet(scanner, NonWhitespaceOFCharacterSet);
    if (scannerPeekCharacter(scanner) == '/') {
        // Scan net location or absolute path
        BOOL absolutePathPresent;

        scannerSkipPeekedCharacter(scanner);
        if (scannerPeekCharacter(scanner) == '/') {
            // Scan net location
            scannerSkipPeekedCharacter(scanner);
            aNetLocation = [scanner readFullTokenWithDelimiterOFCharacterSet:NetLocationDelimiterOFCharacterSet forceLowercase:NO];
            if (aNetLocation && [aNetLocation length] == 0)
                aNetLocation = @"localhost";
            absolutePathPresent = scannerPeekCharacter(scanner) == '/';
            if (absolutePathPresent) {
                // To be consistent with the non-netLocation case, skip the '/' here, too
                scannerSkipPeekedCharacter(scanner);
            }
        } else {
            // That slash started a path, not a net location
            absolutePathPresent = YES;
        }
        if (absolutePathPresent) {
            OWURL *fakeRootURL;

            // Scan path
            aPath = [scanner readFullTokenWithDelimiterOFCharacterSet:PathDelimiterOFCharacterSet forceLowercase:NO];
            fakeRootURL = [self fakeRootURL];
            if (fakeRootURL)
                aPath = [[fakeRootURL urlFromRelativeString:aPath] path];
        } else {
            // Reset path
            aPath = nil;
        }
        // Reset remaining parameters
        someParams = nil;
        aQuery = nil;
        aFragment = nil;
    } else if (scannerHasData(scanner) && !OFCharacterSetHasMember(PathDelimiterOFCharacterSet, scannerPeekCharacter(scanner))) {
        // Scan relative path
	NSMutableArray *pathElements;
	unsigned int preserveCount = 0, pathElementCount;
	NSArray *relativePathArray;
	unsigned int relativePathIndex, relativePathCount;
	BOOL lastElementWasDirectory = NO;

        aPath = [scanner readFullTokenWithDelimiterOFCharacterSet:PathDelimiterOFCharacterSet forceLowercase:NO];

        if (!path || [path length] == 0)
	    pathElements = [NSMutableArray arrayWithCapacity:1];
	else
            pathElements = [[[OWURL pathComponentsForPath:path] mutableCopy] autorelease];
	if ((pathElementCount = [pathElements count]) > 0) {
	    if ([[pathElements objectAtIndex:0] length] == 0)
		preserveCount = 1;
	    if (pathElementCount > preserveCount)
		[pathElements removeLastObject];
	}
        relativePathArray = [OWURL pathComponentsForPath:aPath];
	relativePathCount = [relativePathArray count];
	for (relativePathIndex = 0; relativePathIndex < relativePathCount; relativePathIndex++) {
	    NSString *pathElement;

	    pathElement = [relativePathArray objectAtIndex:relativePathIndex];
	    if ([pathElement isEqualToString:@".."]) {
		lastElementWasDirectory = YES;
		if ([pathElements count] > preserveCount)
		    [pathElements removeLastObject];
		else {
		    if (NetscapeCompatibleRelativeAddresses) {
			// Netscape doesn't preserve leading ..'s
		    } else {
			[pathElements addObject:pathElement];
			preserveCount++;
		    }
		}
	    } else if ([pathElement isEqualToString:@"."]) {
		lastElementWasDirectory = YES;
	    } else {
		lastElementWasDirectory = NO;
		[pathElements addObject:pathElement];
	    }
	}
	if (lastElementWasDirectory && [[pathElements lastObject] length] != 0) {
	    [pathElements addObject:@""];
	}
	aPath = [pathElements componentsJoinedByString:@"/"];

        // Reset remaining parameters
        someParams = nil;
        aQuery = nil;
        aFragment = nil;
    }
    if (scannerPeekCharacter(scanner) == ';') {
        // Scan params
        scannerSkipPeekedCharacter(scanner);
        someParams = [scanner readFullTokenWithDelimiterOFCharacterSet:ParamDelimiterOFCharacterSet forceLowercase:NO];

        // Reset remaining parameters
        aQuery = nil;
        aFragment = nil;
    }
    if (scannerPeekCharacter(scanner) == '?') {
        // Scan query
        scannerSkipPeekedCharacter(scanner);
        aQuery = [scanner readFullTokenWithDelimiterOFCharacterSet:QueryDelimiterOFCharacterSet forceLowercase:NO];
        if (!aQuery)
            aQuery = @"";

        // Reset remaining parameters
        aFragment = nil;
    }
    if (scannerPeekCharacter(scanner) == '#') {
        // Scan fragment
        scannerSkipPeekedCharacter(scanner);
        aFragment = [scanner readFullTokenWithDelimiterOFCharacterSet:FragmentDelimiterOFCharacterSet forceLowercase:NO];
    }

    [scanner release];

    return [OWURL urlWithLowercaseScheme:scheme netLocation:aNetLocation path:aPath params:someParams query:aQuery fragment:aFragment];
}

- (OWURL *)urlForPath:(NSString *)newPath;
{
    return [OWURL urlWithLowercaseScheme:scheme netLocation:netLocation path:newPath params:nil query:nil fragment:nil];
}

- (OWURL *)urlForQuery:(NSString *)newQuery;
{
#warning Bring this MSIE compatibility preference (appending queries) out to the UI?
    /* Some forms pages depend on this behavior */
#if 0
    /* Screwy MSIE semantics */
    if (query)
        newQuery = [query stringByAppendingString:newQuery];
#endif
    return [OWURL urlWithLowercaseScheme:scheme netLocation:netLocation path:path params:params query:newQuery fragment:nil];
}

- (OWURL *)urlWithoutFragment;
{
    if (!fragment)
	return self;
    return [OWURL urlWithLowercaseScheme:scheme netLocation:netLocation path:path params:params query:query fragment:nil];
}

- (OWURL *)urlWithFragment:(NSString *)newFragment
{
    if (newFragment == fragment ||
        [fragment isEqualToString:newFragment])
        return self;

    return [OWURL urlWithLowercaseScheme:scheme netLocation:netLocation path:path params:params query:query fragment:newFragment];
}

- (OWURL *) baseURL;
{
    BOOL hasTrailingSlash;
    NSString *basePath;
    
    hasTrailingSlash = [path hasSuffix: @"/"];
    if (!fragment && !query && hasTrailingSlash)
        return self;
    
    basePath = path;
    if (!hasTrailingSlash) {
        NSRange lastSlashRange;
        
        lastSlashRange = [path rangeOfString:@"/" options: NSBackwardsSearch];
        if (lastSlashRange.length == 1)
            // Take everything up to and including the slash
            basePath = [path substringWithRange: (NSRange){0, lastSlashRange.location + 1}];
        else {
            OBASSERT(!lastSlashRange.length);
            // No slashes, either path is empty or has one component.  We'll strip one component by stripping everything
            basePath = @"";
        }
            
    }
    return [OWURL urlWithLowercaseScheme:scheme netLocation:netLocation path:basePath params:nil query:nil fragment:nil];
}

// NSCopying protocol

- (id)copyWithZone:(NSZone *)zone
{
    OWURL *newURL;
    
    if (NSShouldRetainWithZone(self, zone))
        return [self retain];

    newURL = [[isa allocWithZone:zone] init];
    
    newURL->scheme = [scheme copyWithZone:zone];
    newURL->netLocation = [netLocation copyWithZone:zone];
    newURL->path = [path copyWithZone:zone];
    newURL->params = [params copyWithZone:zone];
    newURL->query = [query copyWithZone:zone];
    newURL->fragment = [fragment copyWithZone:zone];
    newURL->schemeSpecificPart = [schemeSpecificPart copyWithZone:zone];
        
    return newURL;
}

// Debugging

- (NSMutableDictionary *)debugDictionary;
{
    NSMutableDictionary *debugDictionary;

    debugDictionary = [super debugDictionary];

    [debugDictionary setObject:scheme forKey:@"scheme"];
    if (netLocation)
	[debugDictionary setObject:netLocation forKey:@"netLocation"];
    if (path)
	[debugDictionary setObject:path forKey:@"path"];
    if (params)
	[debugDictionary setObject:params forKey:@"params"];
    if (query)
	[debugDictionary setObject:query forKey:@"query"];
    if (fragment)
	[debugDictionary setObject:fragment forKey:@"fragment"];
    if (schemeSpecificPart)
	[debugDictionary setObject:schemeSpecificPart forKey:@"schemeSpecificPart"];

    [debugDictionary setObject:[self compositeString] forKey:@"compositeString"];

    return debugDictionary;
}

- (NSString *)shortDescription;
{
    return [NSString stringWithFormat:@"<URL:%@>", [self compositeString]];
}

@end

@implementation OWURL (Private)

+ (void)controllerDidInitialize:(OFController *)controller;
{
    [self readDefaults];
}

+ (OWURL *)urlWithLowercaseScheme:(NSString *)aScheme netLocation:(NSString *)aNetLocation path:(NSString *)aPath params:(NSString *)someParams query:(NSString *)aQuery fragment:(NSString *)aFragment;
{
    if (!aScheme)
	return nil;
    return [[[self alloc] initWithLowercaseScheme:aScheme netLocation:aNetLocation path:aPath params:someParams query:aQuery fragment:aFragment] autorelease];
}

+ (OWURL *)urlWithLowercaseScheme:(NSString *)aScheme schemeSpecificPart:(NSString *)aSchemeSpecificPart fragment:(NSString *)aFragment;
{
    if (!aScheme)
	return nil;

    return [[[self alloc] initWithLowercaseScheme:aScheme schemeSpecificPart:aSchemeSpecificPart fragment:aFragment] autorelease];
}

- _initWithLowercaseScheme:(NSString *)aScheme;
{
    if (![super init])
	return nil;

    if (aScheme == nil) {
	[self release];
	return nil;
    }
    scheme = [aScheme retain];
    OFSimpleLockInit(&derivedAttributesSimpleLock);
    return self;
}

- initWithLowercaseScheme:(NSString *)aScheme netLocation:(NSString *)aNetLocation path:(NSString *)aPath params:(NSString *)someParams query:(NSString *)aQuery fragment:(NSString *)aFragment;
{
    if (![self _initWithLowercaseScheme:aScheme])
        return nil;

    netLocation = [aNetLocation retain];
    path = [aPath retain];
    params = [someParams retain];
    query = [aQuery retain];
    fragment = [aFragment retain];

    return self;
}

- initWithScheme:(NSString *)aScheme netLocation:(NSString *)aNetLocation path:(NSString *)aPath params:(NSString *)someParams query:(NSString *)aQuery fragment:(NSString *)aFragment;
{
    return [self initWithLowercaseScheme:OFLowercaseStringCacheGet(&lowercaseSchemeCache, aScheme) netLocation:aNetLocation path:aPath params:someParams query:aQuery fragment:aFragment];
}

- initWithLowercaseScheme:(NSString *)aScheme schemeSpecificPart:(NSString *)aSchemeSpecificPart fragment:(NSString *)aFragment;
{
    if (![self _initWithLowercaseScheme:aScheme])
        return nil;
    
    schemeSpecificPart = [aSchemeSpecificPart retain];
    fragment = [aFragment retain];
    
    return self;
}

- initWithScheme:(NSString *)aScheme schemeSpecificPart:(NSString *)aSchemeSpecificPart fragment:(NSString *)aFragment;
{
    return [self initWithLowercaseScheme:OFLowercaseStringCacheGet(&lowercaseSchemeCache, aScheme) schemeSpecificPart:aSchemeSpecificPart fragment:aFragment];
}

- (OWURL *)fakeRootURL;
{
    OWURL *fakeRootURL;
    unsigned int fakeRootIndex, fakeRootCount;

    if (fakeRootURLs == nil)
        return nil;

    [fakeRootURLsLock lock];
    fakeRootCount = [fakeRootURLs count];
    for (fakeRootIndex = 0, fakeRootURL = nil; fakeRootIndex < fakeRootCount && fakeRootURL == nil; fakeRootIndex++) {
        OWURL *someFakeRootURL;

        someFakeRootURL = [fakeRootURLs objectAtIndex:fakeRootIndex];
        if ([[self compositeString] hasPrefix:[someFakeRootURL compositeString]]) {
            fakeRootURL = [[someFakeRootURL retain] autorelease];
        }
    }
    [fakeRootURLsLock unlock];
    return fakeRootURL;
}

- (void)_locked_parseNetLocation;
{
    OBPRECONDITION(_cachedParsedNetLocation == nil);
    _cachedParsedNetLocation = [[OWNetLocation netLocationWithString:netLocation != nil ? netLocation : schemeSpecificPart] retain];
}

@end

// Enabling this causes the app to do a run a bunch of URL tests and then immediately exit.

#if 0
@implementation OWURL (Test)

static inline void testURL(NSString *urlString)
{
    NSLog(@"%@ -> %@", urlString, [[OWURL urlFromDirtyString:urlString] shortDescription]);
}

static inline void testRelativeURL(NSString *urlString)
{
    static OWURL *baseURL = nil;
    
    if (!baseURL) {
	baseURL = [OWURL urlFromDirtyString:@"<URL:http://a/b/c/d;p?q#f>"];
	NSLog(@"Base: %@", [baseURL shortDescription]);
    }

    NSLog(@"%@ = %@", [urlString stringByPaddingToLength:13], [[baseURL urlFromRelativeString:urlString] shortDescription]);
}

static inline void testDomain(NSString *urlString)
{
    NSLog(@"'%@' is in the '%@' domain", urlString, [[OWURL urlFromDirtyString:urlString] domain]);
}


+ (void)didLoad;
{
    testDomain(@"http://omnigroup.com:8080");
    // Note: This next test executes before com is registered as a short top-level domain, so it returns www.omnigroup.com.
    testDomain(@"http://www.omnigroup.com");
    testDomain(@"http://omnigroup.co.uk");
    testDomain(@"http://www.omnigroup.co.uk");
    exit(1);
    testURL(@"http://www.omnigroup.com/Test/path.html");
    testURL(@"file:/LocalLibrary/Web/");
    testURL(@"http://www.omnigroup.com/blegga.cgi?blah");
    testURL(@"<URL:ftp://ds.internic.net/rfc/rfc1436.txt;type=a>");
    testURL(@"<URL:ftp://info.cern.ch/pub/www/doc;\n      type=d>");
    testURL(@"<URL:ftp://info.cern.ch/pub/www/doc;\n      type=d>");
    testURL(@"<URL:ftp://ds.in\n      ternic.net/rfc>");
    testURL(@"<URL:http://ds.internic.\n      net/instructions/overview.html#WARNING>");
    testURL(@"index.html");
    testURL(@"../index.html");

    testRelativeURL(@"g:h");
    testRelativeURL(@"g");
    testRelativeURL(@"./g");
    testRelativeURL(@"g/");
    testRelativeURL(@"/g");
    testRelativeURL(@"//g");
    testRelativeURL(@"?y");
    testRelativeURL(@"g?y");
    testRelativeURL(@"g?y/./x");
    testRelativeURL(@"#s");
    testRelativeURL(@"g#s");
    testRelativeURL(@"g#s/./x");
    testRelativeURL(@"g?y#s");
    testRelativeURL(@";x");
    testRelativeURL(@"g;x");
    testRelativeURL(@"g;x?y#s");
    testRelativeURL(@".");
    testRelativeURL(@"./");
    testRelativeURL(@"..");
    testRelativeURL(@"../");
    testRelativeURL(@"../g");
    testRelativeURL(@"../..");
    testRelativeURL(@"../../");
    testRelativeURL(@"../../g");

    testRelativeURL(@"");
    testRelativeURL(@"../../../g");
    testRelativeURL(@"../../../../g");
    testRelativeURL(@"/./g");
    testRelativeURL(@"/../g");
    testRelativeURL(@"g.");
    testRelativeURL(@".g");
    testRelativeURL(@"g..");
    testRelativeURL(@"..g");
    testRelativeURL(@"./../g");
    testRelativeURL(@"./g/.");
    testRelativeURL(@"g/./h");
    testRelativeURL(@"g/../h");
    testRelativeURL(@"http:g");
    testRelativeURL(@"http:");

    exit(1);
}

@end
#endif
