// Copyright 1999-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/OWAddress.h>

#import <Foundation/Foundation.h>
#import <OmniBase/OmniBase.h>
#import <OmniFoundation/OmniFoundation.h>

#import "OWContentCache.h"
#import "OWContentType.h"
#import "OWContentTypeLink.h"
#import "OWConversionPathElement.h"
#import "OWDocumentTitle.h"
#import "OWFilteredAddressProcessor.h"
#import "OWPipeline.h"
#import "OWProcessorDescription.h"
#import "OWProxyServer.h"
#import "OWSourceProcessor.h"
#import "OWTimeStamp.h"
#import "OWUnknownDataStreamProcessor.h"
#import "OWURL.h"

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

@interface OWAddress (Private)
+ (void)_readDefaults;
@end

NSString *OWAddressContentDataMethodKey = @"Content-Data";
NSString *OWAddressContentHeadersAndDataMethodKey = @"Content-Headers-And-Data";
NSString *OWAddressContentStringMethodKey = @"Content-String";
NSString *OWAddressContentTypeMethodKey = @"Content-Type";
NSString *OWAddressBoundaryMethodKey = @"Boundary";
NSString *OWAddressesToFilterDefaultName = @"OWAddressesToFilter";
NSString *OWAddressFilteringEnabledDefaultName = @"OWAddressFilteringEnabled";

static NSDictionary *_shortcutDictionary = nil;
static OFRegularExpression *_filterRegularExpression = nil;
static NSCharacterSet *nonShortcutCharacterSet;
static unsigned int uniqueKeyCount;
static NSLock *uniqueKeyCountLock;
static NSMutableDictionary *lowercaseEffectNameDictionary;

@implementation OWAddress

+ (void)initialize;
{
    OBINITIALIZE;

    // Note: If this changes, it should also be changed in OmniWeb's OWShortcutPreferences.m since it has no way of getting at it.  (Perhaps it should be a default.)  Ugly, but for now we're maintaining this character set in two places.
    nonShortcutCharacterSet = [[NSCharacterSet characterSetWithCharactersInString:@"./:"] retain];

    uniqueKeyCount = 0;
    uniqueKeyCountLock = [[NSLock alloc] init];

    lowercaseEffectNameDictionary = [[NSMutableDictionary alloc] initWithCapacity:8];

    // Must be lowercase
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectFollowInWindow] forKey:@"followinwindow"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectNewBrowserWindow] forKey:@"newbrowserwindow"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectOpenBookmarksWindow] forKey:@"openbookmarkswindow"];

    // Old effect names, for backward compatibility with OmniWeb 2
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectFollowInWindow] forKey:@"follow"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectNewBrowserWindow] forKey:@"x-popup"];
    [lowercaseEffectNameDictionary setObject:[NSNumber numberWithInt:OWAddressEffectOpenBookmarksWindow] forKey:@"x-as-list"];

    [[OFController sharedController] addObserver:self];
}

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

// Defaults

+ (NSDictionary *)shortcutDictionary;
{
    return [[_shortcutDictionary retain] autorelease];
}

+ (void)setShortcutDictionary:(NSDictionary *)newShortcutDictionary;
{
    NSUserDefaults *defaults;
    
    if (newShortcutDictionary == nil)
        return;

    [_shortcutDictionary autorelease];
    _shortcutDictionary = [newShortcutDictionary retain];
    defaults = [NSUserDefaults standardUserDefaults];
    [defaults setObject:_shortcutDictionary forKey:@"OWAddressShortcuts"];
    [defaults synchronize];
}

+ (void)reloadShortcutDictionaryFromDefaults;
{
    NSUserDefaults *defaults;
    NSMutableDictionary *mutableShortcutDictionary;
    
    defaults = [NSUserDefaults standardUserDefaults];
    mutableShortcutDictionary = [[defaults dictionaryForKey:@"OWAddressShortcuts"] mutableCopy];
    if (mutableShortcutDictionary == nil) {
        // -[NSUserDefaults dictionaryForKey:] returns nil if the stored value was originally a string, which is how we used to store everything with OFUserDefaults.  This lets us read the old format, then store the new format.
        mutableShortcutDictionary = [[[defaults stringForKey:@"OWAddressShortcuts"] propertyList] mutableCopy];
    }
    
    if ([mutableShortcutDictionary objectForKey:@"*"] == nil || [[mutableShortcutDictionary objectForKey:@"*"] isEqualToString:@""]) {
        // Localize the default '*' address, so people in Germany can end in .de instead of .com
        [mutableShortcutDictionary setObject:NSLocalizedStringFromTableInBundle(@"http://www.%@.com/", @"OWF", [OWAddress bundle], default address format to use in your country when user just types a single word) forKey:@"*"];
    }
    [_shortcutDictionary autorelease];
    _shortcutDictionary = mutableShortcutDictionary; // already retained in -mutableCopy
}

+ (void)reloadAddressFilterArrayFromDefaults;
{
    NSUserDefaults *userDefaults;
    NSArray *addressFilterArray;

    userDefaults = [NSUserDefaults standardUserDefaults];
    addressFilterArray = [userDefaults arrayForKey:OWAddressesToFilterDefaultName];
    
    if ([userDefaults boolForKey:OWAddressFilteringEnabledDefaultName] && [addressFilterArray count] > 0) {
        // leak _filterRegularExpression so we don't have to synchronize with threads using it
        _filterRegularExpression = [[OFRegularExpression alloc] initWithString:[NSString stringWithFormat:@"(%@)", [addressFilterArray componentsJoinedByString:@")|("]]];
    } else
        _filterRegularExpression = nil;
}

//

+ (NSString *)stringForEffect:(OWAddressEffect)anEffect;
{
    switch (anEffect) {
    case OWAddressEffectFollowInWindow:
	return @"FollowInWindow";
    case OWAddressEffectNewBrowserWindow:
	return @"NewBrowserWindow";
    case OWAddressEffectOpenBookmarksWindow:
	return @"OpenBookmarksWindow";
    }
    return nil;
}

+ (OWAddressEffect)effectForString:(NSString *)anEffectString;
{
    OWAddressEffect newEffect = OWAddressEffectFollowInWindow;

    if (anEffectString && ![(id)anEffectString isNull]) {
        NSNumber *effectNumber;

        effectNumber = [lowercaseEffectNameDictionary objectForKey:[anEffectString lowercaseString]];
        if (effectNumber)
            newEffect = [effectNumber intValue];
    }
    return newEffect;
}

static OWAddress *
addressForShortcut(NSString *originalString)
{
    NSDictionary *shortcutDictionary;
    NSString *shortcutFormat;
    NSRange spaceRange;
    NSString *shortcutKey, *shortcutParameter;

    /* Algorithm (more or less --- this code is weird [wim]):
        - Split the string at the first space, into a key and a parameter.
        - As long as the key contains no "non-shortcut" characters, repeatedly expand it using the shortcut dictionary.
        - When it no longer matches anything in the shortcut dictionary, if it still contains no non-shortcut characters, apply the shortcut for "*".
    
        If you end up with a string other than the one you started with, return an address for it. Otherwise return nil.
    */

    shortcutDictionary = [OWAddress shortcutDictionary];
    
    spaceRange = [originalString rangeOfString:@" "];
    if (spaceRange.location != NSNotFound) {
        shortcutKey = [[originalString substringToIndex:spaceRange.location] stringByAppendingString:@"@"];
        shortcutParameter = [originalString substringFromIndex:NSMaxRange(spaceRange)];
    } else {
        shortcutKey = originalString;
        shortcutParameter = @"";
    }

#warning Can crash on bad user input
    // [Wim] We really shouldn't use printf strings here, since the user might enter all sorts of crap (such as putting more than one %@ in the string) and cause OmniWeb to crash. 
    // [Wim] Also, the user could easily enter a set of shortcuts that causes the while() to never terminate, which would be bad. Do we really need to apply shortcuts recursively anyway?

    while ([shortcutKey rangeOfCharacterFromSet:nonShortcutCharacterSet].location == NSNotFound) {
        if ((shortcutFormat = [shortcutDictionary objectForKey:shortcutKey])) {
            // Use the matching shortcut
            shortcutKey = [NSString stringWithFormat:shortcutFormat, shortcutParameter];
            shortcutParameter = @"";
        } else {
            if ((shortcutFormat = [shortcutDictionary objectForKey:@"*"])) {
                // Use the default shortcut. Ignore the parameter.
                shortcutKey = [NSString stringWithFormat:shortcutFormat, shortcutKey];
            }
            break;
        }
    }
    if ([shortcutParameter length] || [shortcutKey isEqual:originalString])
        return nil;
    return [OWAddress addressWithURL:[OWURL urlFromDirtyString:shortcutKey]];
}

static OWAddress *
addressForObviousHostname(NSString *string)
{
    NSString *scheme;

    scheme = nil;
    if ([string hasPrefix:@"http."] || [string hasPrefix:@"www."]  || [string hasPrefix:@"home."])
        scheme = @"http://";
    else if ([string hasPrefix:@"gopher."])
        scheme = @"gopher://";
    else if ([string hasPrefix:@"ftp."])
        scheme = @"ftp://";

    if (scheme)
        return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[scheme stringByAppendingString:string]]];
    else
        return nil;
}

static OWAddress *
addressForNotSoObviousHostname(NSString *string)
{
    NSRange rangeOfColon, rangeOfSlash;
    OWAddress *address;

    rangeOfColon = [string rangeOfString:@":"];
    if (rangeOfColon.location != NSNotFound) {
        return nil;
    }

    address = addressForObviousHostname(string);
    if (address)
        return address;

    rangeOfSlash = [string rangeOfString:@"/"];
    if (rangeOfSlash.location == 0) {
        // "/System" -> "file:///System"
        return [OWAddress addressWithFilename:string];
    } else if (rangeOfSlash.location != NSNotFound) {
        NSString *host;

        // "omnigroup/products" --> "http://www.omnigroup.com/products"
        host = [string substringToIndex:rangeOfSlash.location];
        if ([host rangeOfString:@"."].location == NSNotFound) {
            address = addressForShortcut(host);
            if (address) {
                NSString *addressString;
                NSString *additionalPath;

                addressString = [address addressString];
                if (![addressString hasSuffix:@"/"])
                    addressString = [addressString stringByAppendingString:@"/"];
                additionalPath = [string substringFromIndex:NSMaxRange(rangeOfSlash)];
                return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[addressString stringByAppendingString:additionalPath]]];
            }
        }
    }
    return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[@"http://" stringByAppendingString:string]]];
}

+ (OWAddress *)addressWithURL:(OWURL *)aURL target:(NSString *)aTarget methodString:(NSString *)aMethodString methodDictionary:(NSDictionary *)aMethodDictionary effect:(OWAddressEffect)anEffect forceAlwaysUnique:(BOOL)shouldForceAlwaysUnique contextDictionary:(NSDictionary *)aContextDictionary;
{
    if (!aURL)
	return nil;
    return [[[self alloc] initWithURL:aURL target:aTarget methodString:aMethodString methodDictionary:aMethodDictionary effect:anEffect forceAlwaysUnique:shouldForceAlwaysUnique contextDictionary:aContextDictionary] autorelease];
}

+ (OWAddress *)addressWithURL:(OWURL *)aURL target:(NSString *)aTarget methodString:(NSString *)aMethodString methodDictionary:(NSDictionary *)aMethodDictionary effect:(OWAddressEffect)anEffect forceAlwaysUnique:(BOOL)shouldForceAlwaysUnique;
{
    return [self addressWithURL:aURL target:aTarget methodString:aMethodString methodDictionary:aMethodDictionary effect:anEffect forceAlwaysUnique:shouldForceAlwaysUnique contextDictionary:nil];
}

+ (OWAddress *)addressWithURL:(OWURL *)aURL target:(NSString *)aTarget effect:(OWAddressEffect)anEffect;
{
    if (!aURL)
	return nil;
    return [[[self alloc] initWithURL:aURL target:aTarget effect:anEffect] autorelease];
}

+ (OWAddress *)addressWithURL:(OWURL *)aURL;
{
    if (!aURL)
	return nil;
    return [[[self alloc] initWithURL:aURL target:nil effect:OWAddressEffectFollowInWindow] autorelease];
}

+ (OWAddress *)addressForString:(NSString *)anAddressString;
{
    if (!anAddressString)
	return nil;
    return [self addressWithURL:[OWURL urlFromString:anAddressString]];
}

+ (OWAddress *)addressForDirtyString:(NSString *)anAddressString;
{
    OWAddress *address;

    if (!anAddressString || [anAddressString length] == 0)
	return nil;
	
    // Did user enter a shortcut?  If so, use it.
    if ((address = addressForShortcut(anAddressString)))
         return address;

    // Did user type something without any ":"?  If so, prefix with "http://%@"
    if ((address = addressForNotSoObviousHostname(anAddressString)))
         return address;
         
    if ((address = [self addressWithURL:[OWURL urlFromDirtyString:anAddressString]]))
        return address;

    return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[@"http://" stringByAppendingString:anAddressString]]];
}

+ (OWAddress *)addressWithFilename:(NSString *)filename;
{
    NSString *encodedPath;
    
    if (!filename)
	return nil;
    if ([filename hasPrefix:@"/"])
	filename = [filename substringFromIndex:1];
        
    encodedPath = [NSString encodeURLString:filename encoding:kCFStringEncodingUTF8 asQuery:NO leaveSlashes:YES leaveColons:YES];
    return [self addressWithURL:[OWURL urlWithScheme:@"file" netLocation:@"" path:encodedPath params:nil query:nil fragment:nil]];
}

//

- initWithURL:(OWURL *)aURL target:(NSString *)aTarget methodString:(NSString *)aMethodString methodDictionary:(NSDictionary *)aMethodDictionary effect:(OWAddressEffect)anEffect forceAlwaysUnique:(BOOL)shouldForceAlwaysUnique contextDictionary:(NSDictionary *)aContextDictionary;
{
    if (![super init])
	return nil;

    url = [aURL retain];
    target = [aTarget retain];
    methodString = [aMethodString ? aMethodString : @"GET" retain];
    methodDictionary = [aMethodDictionary retain];
    flags.effect = anEffect;
    flags.forceAlwaysUnique = shouldForceAlwaysUnique;
    contextDictionary = [aContextDictionary retain];

    return self;
}

- initWithURL:(OWURL *)aURL target:(NSString *)aTarget methodString:(NSString *)aMethodString methodDictionary:(NSDictionary *)aMethodDictionary effect:(OWAddressEffect)anEffect forceAlwaysUnique:(BOOL)shouldForceAlwaysUnique;
{
    return [self initWithURL:aURL target:aTarget methodString:aMethodString methodDictionary:aMethodDictionary effect:anEffect forceAlwaysUnique:shouldForceAlwaysUnique contextDictionary:nil];
}

- initWithURL:(OWURL *)aURL target:(NSString *)aTarget effect:(OWAddressEffect)anEffect;
{
    return [self initWithURL:aURL target:aTarget methodString:nil methodDictionary:nil effect:anEffect forceAlwaysUnique:NO contextDictionary:nil];
}

- initWithURL:(OWURL *)aURL;
{
    return [self initWithURL:aURL target:nil methodString:nil methodDictionary:nil effect:OWAddressEffectFollowInWindow forceAlwaysUnique:NO contextDictionary:nil];
}

- initWithArchiveDictionary:(NSDictionary *)dictionary;
{
    if (![super init])
	return nil;

    url = [[OWURL urlFromString:[dictionary objectForKey:@"url" defaultObject:@""]] retain];
    target = [[dictionary objectForKey:@"target" defaultObject:@""] retain];
    methodString = [[dictionary objectForKey:@"method" defaultObject:@"GET"] retain];
    methodDictionary = [[dictionary objectForKey:@"mdict"] retain];
    flags.effect = [dictionary intForKey:@"effect" defaultValue:OWAddressEffectFollowInWindow];
    flags.forceAlwaysUnique = [dictionary boolForKey:@"unique" defaultValue:NO];
    contextDictionary = [[dictionary objectForKey:@"context"] retain];

    return self;
}

- (id)initWithCoder:(NSCoder *)aDecoder;
{
    return [self initWithArchiveDictionary:[aDecoder decodePropertyList]];
}

- (void)dealloc;
{
    [url release];
    [target release];
    [methodString release];
    [methodDictionary release];
    [cacheKey release];
    [contextDictionary release];
    [super dealloc];
}

// Queries

- (OWURL *)url;
{
    return url;
}

- (OWURL *)proxyURL;
{
    return [OWProxyServer proxyURLForURL:url];
}

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

- (NSDictionary *)methodDictionary;
{
    return methodDictionary;
}

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

- (NSString *)localFilename;
{
    NSString *scheme;
    NSString *path;

    scheme = [url scheme];
    if ([scheme isEqualToString:@"file"]) {
	path = [url path];
	if (!path)
	    path = [url schemeSpecificPart];
	path = [NSString decodeURLString:path encoding:kCFStringEncodingUTF8];
	if ([path hasPrefix:@"~"])
	    return path;
#ifdef WIN32
        return path; // file:/C:/tmp -> C:/tmp
#else
        return [@"/" stringByAppendingString:path]; // file:/tmp -> /tmp
#endif
    }
    return nil;
}

- (NSString *)addressString;
{
    return [url compositeString];
}

- (NSString *)stringValue;
{
    return [url compositeString];
}

// Effects

- (OWAddressEffect)effect;
{
    return flags.effect;
}

- (NSString *)effectString;
{
    return [OWAddress stringForEffect:flags.effect];
}

// Archiving

- (NSDictionary *)archiveDictionary;
{
    NSMutableDictionary *archiveDictionary;

    archiveDictionary = [NSMutableDictionary dictionary];
    
    [archiveDictionary setObject:[url compositeString] forKey:@"url" defaultObject:@""];
    [archiveDictionary setObject:target forKey:@"target" defaultObject:@""];
    [archiveDictionary setObject:methodString forKey:@"method" defaultObject:@"GET"];
    if (methodDictionary != nil)
        [archiveDictionary setObject:methodDictionary forKey:@"mdict"];
    [archiveDictionary setIntValue:flags.effect forKey:@"effect" defaultValue:OWAddressEffectFollowInWindow];
    [archiveDictionary setBoolValue:flags.forceAlwaysUnique forKey:@"unique" defaultValue:NO];
    if (contextDictionary != nil)
        [archiveDictionary setObject:contextDictionary forKey:@"context"];
    
    return archiveDictionary;
}

// Encoding protocol

- (void)encodeWithCoder:(NSCoder *)aCoder;
{
    [aCoder encodePropertyList:[self archiveDictionary]];
}

// Displaying an address

- (NSString *)drawLabel;
{
    return [url compositeString];
}

- (BOOL)isVisited;
{
    return [OWContentCache lookupContentCacheForAddress:self] != nil;
}

- (BOOL)isSecure;
{
    return [url isSecure];
}

// Equality and hashing

- (unsigned)hash;
{
    return [url hash];
}

// Exactly the same URL
- (BOOL)isEqual:(id)anObject;
{
    OWAddress *otherAddress;

    if (self == anObject)
	return YES;
    if (anObject == nil)
        return NO;
    otherAddress = anObject;
    if (otherAddress->isa != isa)
	return NO;
    if (flags.effect != otherAddress->flags.effect)
	return NO;
#warning TODO: why not compare target also?
    if (![url isEqual:otherAddress->url])
	return NO;
    if (![methodString isEqualToString:otherAddress->methodString])
	return NO;
    if (methodDictionary != otherAddress->methodDictionary && ![methodDictionary isEqual:otherAddress->methodDictionary])
	return NO;
    return YES;
}

// Not the same URL, but will fetch the same data. For example, if two URLs could differ only by the fragment, which would mean they have the same document.
- (BOOL)isSameDocumentAsAddress:(OWAddress *)otherAddress;
{
    if (!otherAddress)
        return NO;
    if (self == otherAddress || (self->cacheKey && (self->cacheKey == otherAddress->cacheKey)))
	return YES;
    return [[self cacheKey] isEqualToString:[otherAddress cacheKey]];
}

- (BOOL)representsFile;
{
    return [url path] ? YES : NO;
}

- (NSDictionary *)contextDictionary;
{
    return contextDictionary;
}

- (OWContentType *)probableContentTypeBasedOnPath;
{
    NSString *localFilename;
    
    if (![self representsFile])
        return [OWUnknownDataStreamProcessor unknownContentType];
    
    if ((localFilename = [self localFilename]) != nil)
        return [OWContentType contentTypeForFilename:localFilename isLocalFile:YES];
        
    return [OWContentType contentTypeForFilename:[url path] isLocalFile:NO];
}


// OWContent protocol

- (OWContentType *)contentType;
{
    OWURL *proxiedURL;
    
    if (_filterRegularExpression != nil && [_filterRegularExpression hasMatchInString:[url compositeString]])
        return [OWFilteredAddressProcessor sourceContentType];

    proxiedURL = [self proxyURL];
    
    if (proxiedURL != url) {
        // Normally, the content type of a proxied URL is the content type of the proxy it will use. This is not true for e.g. HTTPS, which requires special handling from the proxy. So, we find the processor we would normally use for this URL, and we ask it what content type a proxied URL should have.
        OWConversionPathElement *conversionPath;
        OWProcessorDescription *urlProcessor;
        
        conversionPath = [[url contentType] bestPathForTargetContentType:[OWSourceProcessor sourceContentType]];
        if (conversionPath != nil &&
            (urlProcessor = [OWProcessorDescription processorDescriptionForProcessorClassName:[[conversionPath link] processorClassName]]) != nil) {
            return [[urlProcessor processorClass] contentTypeForURL:url whenProxiedBy:proxiedURL];
        } else
            return [proxiedURL contentType];
    }
    return [url contentType];
}

- (OWContentInfo *)contentInfo;
{
    return nil;
}

- (unsigned long int)cacheSize;
{
    return 100; // Perhaps we should do something less arbitrary?
}

- (BOOL)shareable;
{
    return YES;
}


// OWAddress protocol

- (NSString *)cacheKey;
{
    if (cacheKey)
	return cacheKey;
	
    if (![self isAlwaysUnique]) {
	cacheKey = [[url cacheKey] retain];
	return cacheKey;
    }
    [uniqueKeyCountLock lock];
    cacheKey = [[NSString alloc] initWithFormat:@"%d", uniqueKeyCount++];
    [uniqueKeyCountLock unlock];
    return cacheKey;
}

- (NSString *)shortDisplayString;
{
    return [url shortDisplayString];
}

- (NSString *)bestKnownTitle;
{
    NSString *bestKnownTitle;

    bestKnownTitle = [OWDocumentTitle titleForAddress:self];
    if (bestKnownTitle)
	return bestKnownTitle;
    return [self shortDisplayString];
}

- (BOOL)isAlwaysUnique;
{
    if (flags.forceAlwaysUnique)
	return YES;
    if (methodDictionary)
	return YES;
    return NO;
}

// Getting related addresses

// If you don't use the method with pipeline, below, then #fragments won't properly be based off of the current document in the cache.  Not always a problem, but be aware of it. 
- (OWAddress *)addressForRelativeString:(NSString *)relativeAddressString;
{
    return [self addressForRelativeString:relativeAddressString inPipeline:nil target:nil effect:OWAddressEffectFollowInWindow];
}

- (OWAddress *)addressForRelativeString:(NSString *)relativeAddressString target:(NSString *)aTarget effect:(OWAddressEffect)anEffect;
{
    return [self addressForRelativeString:relativeAddressString inPipeline:nil target:aTarget effect:anEffect];
}

- (OWAddress *)addressForRelativeString:(NSString *)relativeAddressString inPipeline:(OWPipeline *)pipeline target:(NSString *)aTarget effect:(OWAddressEffect)anEffect;
{
    relativeAddressString = [[relativeAddressString stringByRemovingSurroundingWhitespace] stringByRemovingReturns];
    if (![relativeAddressString hasPrefix:@"#"]) {
        // If it's not a fragment, life is easy.
        return [OWAddress addressWithURL:[url urlFromRelativeString:relativeAddressString] target:aTarget methodString:nil methodDictionary:nil effect:anEffect forceAlwaysUnique:NO contextDictionary:contextDictionary];
    } else {
        OWAddress *relativeAddress;

        // If we're given a fragment, it should be based off of the pipeline's address, not ourself, because we may be the document's <base href=""> address, and fragments are a special case.
        if (pipeline) {
            OWAddress *pipelineAddress;

            pipelineAddress = [pipeline lastAddress];
            if (!pipelineAddress)
                pipelineAddress = [pipeline contextObjectForKey:@"HistoryAddress"];
            if (pipelineAddress)
                return [pipelineAddress addressForRelativeString:relativeAddressString inPipeline:nil target:aTarget effect:anEffect];
        }

        relativeAddress = [OWAddress addressWithURL:[url urlFromRelativeString:relativeAddressString] target:aTarget methodString:nil methodDictionary:nil effect:anEffect forceAlwaysUnique:NO contextDictionary:contextDictionary];
        // Force the new address to use the exact same document in the cache as we use, EVEN if we are unique (eg, the result of a FORM or some such).  The advantage to this is that image maps in form results that use "#mapname" will not force a second fetch, which will cause a form to post twice.  Also, relative links in form documents don't cause a refetch.
        relativeAddress->cacheKey = [[self cacheKey] retain];

        return relativeAddress;
    }
}

//

- (OWAddress *)addressForDirtyRelativeString:(NSString *)relativeAddressString;
{
    OWAddress *address;

    if (!relativeAddressString)
	return self;

    relativeAddressString = [relativeAddressString stringByRemovingSurroundingWhitespace];
    if ([relativeAddressString length] == 0)
        return self;

    address = addressForObviousHostname(relativeAddressString);
    if (address)
	return address;
	
    address = [OWAddress addressWithURL:[url urlFromRelativeString:relativeAddressString]];
    if (address)
        return address;

    return [OWAddress addressWithURL:[OWURL urlFromDirtyString:[@"http://" stringByAppendingString:relativeAddressString]]];
}

//

- (OWAddress *)addressWithGetQuery:(NSString *)query;
{
    return [OWAddress addressWithURL:[url urlForQuery:query] target:target methodString:nil methodDictionary:nil effect:OWAddressEffectFollowInWindow forceAlwaysUnique:YES contextDictionary:contextDictionary];
}

- (OWAddress *)addressWithPath:(NSString *)aPath;
{
    return [OWAddress addressWithURL:[url urlForPath:aPath] target:target methodString:methodString methodDictionary:methodDictionary effect:flags.effect forceAlwaysUnique:flags.forceAlwaysUnique contextDictionary:contextDictionary];
}

- (OWAddress *)addressWithMethodString:(NSString *)newMethodString;
{
    if (methodString == newMethodString)
	return self;
    return [OWAddress addressWithURL:url target:target methodString:newMethodString methodDictionary:nil effect:flags.effect forceAlwaysUnique:flags.forceAlwaysUnique contextDictionary:contextDictionary];
}

- (OWAddress *)addressWithMethodString:(NSString *)newMethodString
  methodDictionary:(NSDictionary *)newMethodDictionary
  forceAlwaysUnique:(BOOL)shouldForceAlwaysUnique;
{
    return [OWAddress addressWithURL:url target:target methodString:newMethodString methodDictionary:newMethodDictionary effect:flags.effect forceAlwaysUnique:shouldForceAlwaysUnique contextDictionary:contextDictionary];
}

- (OWAddress *)addressWithTarget:(NSString *)newTarget;
{
    if (target == newTarget)
	return self;
    return [OWAddress addressWithURL:url target:newTarget methodString:methodString methodDictionary:methodDictionary effect:flags.effect forceAlwaysUnique:flags.forceAlwaysUnique contextDictionary:contextDictionary];
}

- (OWAddress *)addressWithEffect:(OWAddressEffect)newEffect;
{
    if (flags.effect == newEffect)
	return self;
    return [OWAddress addressWithURL:url target:target methodString:methodString methodDictionary:methodDictionary effect:newEffect forceAlwaysUnique:flags.forceAlwaysUnique contextDictionary:contextDictionary];
}

- (OWAddress *)addressWithForceAlwaysUnique:(BOOL)shouldForceAlwaysUnique;
{
    if (flags.forceAlwaysUnique == shouldForceAlwaysUnique)
	return self;
    return [OWAddress addressWithURL:url target:target methodString:methodString methodDictionary:methodDictionary effect:flags.effect forceAlwaysUnique:shouldForceAlwaysUnique contextDictionary:contextDictionary];
}

- (OWAddress *)addressWithoutFragment;
{
    OWURL *urlWithoutFragment;

    urlWithoutFragment = [url urlWithoutFragment];
    if (url == urlWithoutFragment)
	return self;
    return [OWAddress addressWithURL:urlWithoutFragment target:target methodString:methodString methodDictionary:methodDictionary effect:flags.effect forceAlwaysUnique:flags.forceAlwaysUnique contextDictionary:contextDictionary];
}

- (OWAddress *)addressWithContextDictionary:(NSDictionary *)newContextDictionary;
{
// NSLog(@"Creating address with context dictionary %@", newContextDictionary);
    return [OWAddress addressWithURL:url target:target methodString:methodString methodDictionary:methodDictionary effect:flags.effect forceAlwaysUnique:flags.forceAlwaysUnique contextDictionary:newContextDictionary];
}

- (OWAddress *)addressWithContextObject:object forKey:(NSString *)key;
{
    NSMutableDictionary *mutableContextDictionary;
    
    mutableContextDictionary = [NSMutableDictionary dictionary];
    if (contextDictionary != nil)
        [mutableContextDictionary addEntriesFromDictionary:contextDictionary];
    [mutableContextDictionary setObject:object forKey:key];
    
    return [OWAddress addressWithURL:url target:target methodString:methodString methodDictionary:methodDictionary effect:flags.effect forceAlwaysUnique:flags.forceAlwaysUnique contextDictionary:mutableContextDictionary];
}

- (NSString *)suggestedFilename;
{
    NSString *filename;

    filename = [NSString decodeURLString:[[url path] lastPathComponent]];
    if (!filename || ![filename length]) {
        if ([[url scheme] isEqualToString:@"http"])
            filename = @"index.html";
        else
            filename = NSLocalizedStringFromTableInBundle(@"download", @"OWF", [OWAddress bundle], default suggested filename if not html);
    }

    return filename;
}

// A URL can have any crud after the last '.' characther in the last path component.  We will only accept alphanumeric characters in the file extension, if for no other reason than NSWorkspace can crash on crazy inputs.
- (NSString *)suggestedFileType;
{
    NSString *filename;
    NSString *fileType;
    NSRange range;
    
    filename = [NSString decodeURLString:[[url path] lastPathComponent]];
    fileType = [filename pathExtension];
    range = [fileType rangeOfCharacterFromSet: [[NSCharacterSet alphanumericCharacterSet] invertedSet]];
    if (range.length)
        return nil;
    return fileType;
}

// NSCopying protocol

- (id)copyWithZone:(NSZone *)zone
{
    OWURL *newURL;
    NSString *newTarget, *newMethodString;
    NSDictionary *newMethodDictionary;
    NSDictionary *newContextDictionary;
    OWAddress *newAddress;

    if (NSShouldRetainWithZone(self, zone))
        return [self retain];

    newURL = [url copyWithZone:zone];
    newTarget = [target copyWithZone:zone];
    newMethodString = [methodString copyWithZone:zone];
    newMethodDictionary = [methodDictionary copyWithZone:zone];
    newContextDictionary = [contextDictionary copyWithZone:zone];
        
    newAddress = [[isa allocWithZone:zone] initWithURL:newURL target:newTarget methodString:newMethodString methodDictionary:newMethodDictionary effect:flags.effect forceAlwaysUnique:flags.forceAlwaysUnique contextDictionary:newContextDictionary];

    [newURL release];
    [newTarget release];
    [newMethodString release];
    [newMethodDictionary release];
    
    return newAddress;
}

// Debugging

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

    debugDictionary = [super debugDictionary];
    if (url)
	[debugDictionary setObject:url forKey:@"url"];
    if (target)
	[debugDictionary setObject:target forKey:@"target"];
    if (methodString)
	[debugDictionary setObject:methodString forKey:@"methodString"];
    if (methodDictionary)
	[debugDictionary setObject:methodDictionary forKey:@"methodDictionary"];
    [debugDictionary setObject:[OWAddress stringForEffect:flags.effect] forKey:@"effect"];
    [debugDictionary setObject:flags.forceAlwaysUnique ? @"YES" : @"NO" forKey:@"forceAlwaysUnique"];

    return debugDictionary;
}

- (NSString *)shortDescription;
{
    return [[self url] shortDescription];
}

@end

@implementation OWAddress (Private)

+ (void)_readDefaults;
{
    [self reloadAddressFilterArrayFromDefaults];
    [self reloadShortcutDictionaryFromDefaults];
}

@end

// Enabling this causes the app to run some tests and then immediately exit.

#if 0

@implementation OWAddress (Test)

static void testAddress(NSString *addressString)
{
    NSLog(@"%@ -> %@", addressString, [[OWAddress addressForDirtyString:addressString] shortDescription]);
}

+ (void)didLoad;
{
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(testControllerDidInit:) name:OFControllerDidInitNotification object:nil];
}

+ (void)testControllerDidInit:(NSNotification *)notification;
{
    testAddress(@"omnigroup");
    testAddress(@"omnigroup/products");
    testAddress(@"/System");
    testAddress(@"www.omnigroup.com:80");

    exit(1);
}

@end
#endif
