// 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/OWPipeline.h>

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

#import <OWF/OWAddress.h> // Only for heuristic for compositeTypeString
#import <OWF/OWContentCache.h>
#import <OWF/OWContentContainer.h>
#import <OWF/OWContentInfo.h>
#import <OWF/OWContentProtocol.h>
#import <OWF/OWContentType.h>
#import <OWF/OWContentTypeLink.h>
#import <OWF/OWHeaderDictionary.h>
#import <OWF/OWURL.h> // Only for heuristic for compositeTypeString
#import <OWF/OWPipelineCoordinator.h>
#import <OWF/OWProcessor.h>
#import <OWF/OWSourceProcessor.h>  // for the Omni/Source content type

RCS_ID("$Header: /Network/Source/CVS/OmniGroup/Frameworks/OWF/Pipelines.subproj/OWPipeline.m,v 1.89 2002/03/09 01:53:51 kc Exp $")

#define OWF_Bundle [OWPipeline bundle]

@interface OWPipeline (Private)
// Status monitors
+ (void)_updateStatusMonitors:(NSTimer *)timer;
// Methods managing the targetPipelinesMapTable
+ (void)_addPipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
+ (void)_startPipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
+ (void)_removePipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
+ (void)_target:(id <OWTarget>)aTarget acceptedContentFromPipeline:(OWPipeline *)acceptedPipeline;

//
- (NSSet *)_cachedContentTypesMinusProcessed;
- (id <OWContent>)_availableContentOfType:(OWContentType *)searchType;
- (BOOL)_readyContentType:(OWContentType *)aContentType;
- (void)_processedContentType:(OWContentType *)aContentType;
- (void)_startUnstartedProcessors;
- (void)_deactivateIfPipelineHasNoProcessors;
- (void)_releasePipelineCoordinator;
- (void)_cleanupPipelineIfDead;
- (void)_pipelineBuilt;
- (BOOL)_hasNoProcessors;
- (void)_createNextProcessor;

// Target stuff
- (void)_notifyTargetOfTreeActivation;
- (void)_notifyTargetOfTreeDeactivation;
- (void)_notifyTargetOfTreeActivation:(id <OWTarget>)aTarget;
- (void)_notifyTargetOfTreeDeactivation:(id <OWTarget>)aTarget;
- (void)_updateStatusOnTarget:(id <OWTarget>)target;
- (void)_rebuildCompositeTypeString;
@end

@implementation OWPipeline

NSString *OWPipelineHasErrorNotificationName = @"OWPipelineHasError";
NSString *OWPipelineWillDeallocateNotificationName = @"OWPipelineWillDeallocate";
NSString *OWPipelineHasErrorNotificationPipelineKey = @"pipeline";
NSString *OWPipelineHasErrorNotificationProcessorKey = @"processor";
NSString *OWPipelineHasErrorNotificationErrorNameKey = @"errorName";
NSString *OWPipelineHasErrorNotificationErrorReasonKey = @"errorReason";
NSString *OWPipelineTreeActivationNotificationName = @"OWPipelineTreeActivation";
NSString *OWPipelineTreeDeactivationNotificationName = @"OWPipelineTreeDeactivation";
NSString *OWPipelineTreePeriodicUpdateNotificationName = @"OWPipelineTreePeriodicUpdateNotificationName";

enum {
    PipelineProcessorConditionNoProcessors, PipelineProcessorConditionSomeProcessors,
};

static BOOL OWPipelineDebug = NO;
static NSNotificationCenter *fetchedContentNotificationCenter;
static NSNotificationCenter *pipelineErrorNotificationCenter;
static NSString *fetchedContentContentKey = @"content";
static NSString *permanentRedirectionContentKey = @"redirected-to";
static OFSimpleLockType targetPipelinesMapTableLock;
static NSMapTable *targetPipelinesMapTable;
static BOOL activeTreeHasUndisplayedChanges;
static NSTimer *activeStatusUpdateTimer;

/* TODO: The fetchedContentContentKey and permanentRedirectionContentKey keys should be visible outside of this class (instead of using literal strings elsewhere) */

#define DEFAULT_SIMULTANEOUS_TARGET_CAPACITY (128)

+ (void)initialize;
{
    OBINITIALIZE;

    fetchedContentNotificationCenter = [[NSNotificationCenter alloc] init];
    pipelineErrorNotificationCenter = [[NSNotificationCenter alloc] init];
    OFSimpleLockInit(&targetPipelinesMapTableLock);
    targetPipelinesMapTable = NSCreateMapTable(NSNonRetainedObjectMapKeyCallBacks, NSObjectMapValueCallBacks, DEFAULT_SIMULTANEOUS_TARGET_CAPACITY);

    // Status monitor
    activeTreeHasUndisplayedChanges = NO;
    activeStatusUpdateTimer = nil;
}

+ (void)setDebug:(BOOL)debug;
{
    OWPipelineDebug = debug;
}

// For notification of pipeline errors

+ (NSNotificationCenter *)pipelineErrorNotificationCenter;
{
    return pipelineErrorNotificationCenter;
}

// For notification of pipeline fetches

+ (void)addObserver:(id)anObserver selector:(SEL)aSelector address:(id <OWAddress>)anAddress;
{
    [fetchedContentNotificationCenter addObserver:anObserver selector:aSelector name:[anAddress cacheKey] object:nil];
}

- (void)addObserver:(id)anObserver selector:(SEL)aSelector
{
    [fetchedContentNotificationCenter addObserver:anObserver selector:aSelector name:nil object:self];
}

+ (void)removeObserver:(id)anObserver address:(id <OWAddress>)anAddress;
{
    [fetchedContentNotificationCenter removeObserver:anObserver name:[anAddress cacheKey] object:nil];
}

+ (void)removeObserver:(id)anObserver;
{
    [fetchedContentNotificationCenter removeObserver:anObserver];
}

// Target management

+ (void)invalidatePipelinesForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;

    OMNI_POOL_START {
        while ((pipelines = [self pipelinesForTarget:aTarget])) {
            unsigned int pipelineIndex, pipelineCount;

            pipelineCount = [pipelines count];
            for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
                [[pipelines objectAtIndex:pipelineIndex] invalidate];
            }
        }
    } OMNI_POOL_END;
}

+ (void)abortTreeActivityForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;

    pipelines = [self pipelinesForTarget:aTarget];
    if (pipelines) {
        unsigned int pipelineIndex, pipelineCount;

        pipelineCount = [pipelines count];
        for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
            [[pipelines objectAtIndex:pipelineIndex] abortTreeActivity];
        }
    }
}

+ (void)abortPipelinesForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;

    pipelines = [self pipelinesForTarget:aTarget];
    if (pipelines) {
        unsigned int pipelineIndex, pipelineCount;

        pipelineCount = [pipelines count];
        for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
            [[pipelines objectAtIndex:pipelineIndex] abortTask];
        }
    }
}

+ (OWPipeline *)currentPipelineForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;

    pipelines = [self pipelinesForTarget:aTarget];
    if (pipelines == nil || [pipelines count] == 0)
        return nil;
    return [pipelines objectAtIndex:0]; // Is in effect autoreleased, since the whole array is autoreleased
}

+ (NSArray *)pipelinesForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines, *pipelinesSnapshot;

    OBPRECONDITION(aTarget != nil);
    OFSimpleLock(&targetPipelinesMapTableLock); {
        pipelines = NSMapGet(targetPipelinesMapTable, aTarget);
        pipelinesSnapshot = pipelines != nil ? [NSArray arrayWithArray:pipelines] : nil;
    } OFSimpleUnlock(&targetPipelinesMapTableLock);
    return pipelinesSnapshot;
}

+ (OWPipeline *)firstActivePipelineForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;
    unsigned int pipelineIndex, pipelineCount;

    pipelines = [self pipelinesForTarget:aTarget];
    if (!pipelines)
        return nil;
    
    pipelineCount = [pipelines count];
    for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
        OWPipeline *aPipeline;

        aPipeline = [pipelines objectAtIndex:pipelineIndex];
        if ([aPipeline treeHasActiveChildren])
            return aPipeline; // Is in effect autoreleased, since the whole array is autoreleased
    }
    return nil;
}

+ (OWPipeline *)lastActivePipelineForTarget:(id <OWTarget>)aTarget;
{
    NSArray *pipelines;
    unsigned int pipelineIndex;

    pipelines = [self pipelinesForTarget:aTarget];
    if (!pipelines)
        return nil;

    pipelineIndex = [pipelines count];
    while (pipelineIndex--) {
        OWPipeline *aPipeline;

        aPipeline = [pipelines objectAtIndex:pipelineIndex];
        if ([aPipeline treeHasActiveChildren])
            return aPipeline; // Is in effect autoreleased, since the whole array is autoreleased
    }
    return nil;
}


// Status Monitoring

+ (void)activeTreeHasChanged;
{
    if (activeTreeHasUndisplayedChanges)
        return;
    activeTreeHasUndisplayedChanges = YES;
    [self queueSelectorOnce:@selector(_updateStatusMonitors:) withObject:nil];
}

+ (void)startActiveStatusUpdateTimer;
{
    OBPRECONDITION(activeStatusUpdateTimer == nil);

    activeStatusUpdateTimer = [[NSTimer scheduledTimerWithTimeInterval:0.5 target:self selector:@selector(_updateStatusMonitors:) userInfo:nil repeats:YES] retain];
}

+ (void)stopActiveStatusUpdateTimer;
{
    OBPRECONDITION(activeStatusUpdateTimer != nil);
    [activeStatusUpdateTimer invalidate];
    [activeStatusUpdateTimer release];
    activeStatusUpdateTimer = nil;
}


// Init and dealloc

+ (void)startPipelineWithContent:(id <OWContent>)aContent target:(id <OWTarget, OFWeakRetain, NSObject>)aTarget;
{
    OWPipeline *pipeline;

    pipeline = [[self alloc] initWithContent:aContent target:aTarget useCachedErrorContent:NO];
    [pipeline startProcessingContent];
    [pipeline release];
}

- initWithContent:(id <OWContent>)aContent target:(id <OWTarget, OFWeakRetain, NSObject>)aTarget useCachedErrorContent:(BOOL)useError;
{
    OBPRECONDITION(aContent != nil);

    if (![super init])
	return nil;

    OFWeakRetainConcreteImplementation_INIT;

    state = PipelineInit;
    flags.contentError = NO;
    flags.everHadContentError = NO;
    flags.disableContentCache = NO;
    flags.useCachedErrorContent = useError;
    flags.processingError = NO;
    processorArray = [[NSMutableArray alloc] initWithCapacity:2];
    unstartedProcessors = [[NSMutableArray alloc] init];
    processorArrayConditionLock = [[NSConditionLock alloc] initWithCondition:PipelineProcessorConditionNoProcessors];
    _lastContentLock = [[NSLock alloc] init];
    lastContentType = [_lastContent contentType];
    context = [[NSMutableDictionary alloc] init];
    contextLock = [[NSLock alloc] init];
    targetLock = [[NSLock alloc] init];
    [self addContent:aContent];
    if (([aContent conformsToProtocol:@protocol(OWAddress)]))
	[self setLastAddress:(id <OWAddress>)aContent];

    // This has the side effect of setting our parentContentInfo
    [self setTarget:aTarget];

    if (OWPipelineDebug)
	NSLog(@"%@: init", [self shortDescription]);

    return self;
}

- initWithContent:(id <OWContent>)aContent target:(id <OWTarget, OFWeakRetain, NSObject>)aTarget;
{
    return [self initWithContent:aContent target:aTarget useCachedErrorContent:NO];
}

- (void)dealloc;
{
    if (OWPipelineDebug)
	NSLog(@"%@: dealloc", [self shortDescription]);

    OFWeakRetainConcreteImplementation_DEALLOC;

    [_lastContent release];
    [_lastContentLock release];

    [targetTypeFormatString release];
    [lastAddress release];
    [context release];
    [contextLock release];
    [targetLock release];
    [headerDictionary release];
    [headerURL release];
    [processorArray release];
    [unstartedProcessors release];
    [processorArrayConditionLock release];
    [processedContentTypes release];
    [contentCacheForLastAddress release];
    [errorNameString release];
    [errorReasonString release];
    [super dealloc];
}


// OWTask subclass

- (id <OWAddress>)lastAddress;
{
    id <OWAddress> addressAutoreleased;

    OFSimpleLock(&displayablesSimpleLock); {
        addressAutoreleased = [[lastAddress retain] autorelease];
    } OFSimpleUnlock(&displayablesSimpleLock);
    return addressAutoreleased;
}

- (BOOL)treeHasActiveChildren;
{
    return (state != PipelineInit && state != PipelineDead) || [super treeHasActiveChildren];
}

- (void)activateInTree;
{
    if (OWPipelineDebug)
        NSLog(@"%@: tree activation", [self shortDescription]);
    
    [super activateInTree];
    [self _notifyTargetOfTreeActivation];
}

- (void)deactivateInTree;
{
    if (OWPipelineDebug)
        NSLog(@"%@: tree deactivation", [self shortDescription]);
    
    if (flags.everHadContentError && !flags.delayedForError) {
        // We had an error, wait around to display it.

        // Note that we've already been deactivated so we don't do this again next time.
        flags.delayedForError = YES;

        // Schedule another deactivate test for 8 seconds from now.
        [[OFScheduler mainScheduler] scheduleSelector:@selector(deactivateInTree) onObject:self withObject:nil afterTime:8.0];
    } else {
        // No error, or we've already delayed to display it.
        [super deactivateInTree];
        
        [self _notifyTargetOfTreeDeactivation];
        [self _cleanupPipelineIfDead];
    }
}

- (void)abortTask;
{
    NSArray *processorsCopy;
    NSEnumerator *processorEnumerator;
    OWProcessor *processor;

    if (state == PipelineAborting || state == PipelineDead)
        return;

    [processorArrayConditionLock lock]; {
        state = PipelineAborting;
        [processorArray removeObjectsInArray:unstartedProcessors];
        if ([processorArray count])
            firstProcessor = [processorArray objectAtIndex:0];
        else
            firstProcessor = nil;
        [unstartedProcessors removeAllObjects];
        [unstartedProcessors release];
        unstartedProcessors = nil;
        processorsCopy = [[NSArray alloc] initWithArray:processorArray];
        processorEnumerator = [processorsCopy objectEnumerator];
        [processorsCopy release];
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];

    // Half-processed content isn't valid
    flags.everHadContentError = YES;
    // We don't need to wait to display this, since the user knows why we've stopped
    flags.delayedForError = YES;

    [pipelineCoordinator pipelineAbort:self];
    [self _releasePipelineCoordinator];
    [contentCacheForLastAddress flushCachedContent];

    if (errorNameString == nil)
        errorNameString = [@"Stopped" retain];
    if (errorReasonString == nil)
        errorReasonString = [NSLocalizedStringFromTableInBundle(@"Stopped by user", @"OWF", OWF_Bundle, pipeline status) retain];

    while ((processor = [processorEnumerator nextObject])) {
        if (OWPipelineDebug)
            NSLog(@"%@: aborting %@", [self shortDescription], [processor shortDescription]);
        [processor abortProcessing];
    }

    [self updateStatusOnTarget];

    [self _deactivateIfPipelineHasNoProcessors];
}

- (NSTimeInterval)estimatedRemainingTimeInterval;
{
    NSDate *firstBytesDate;
    int workDone, workToBeDone;

    [processorArrayConditionLock lock]; {
        firstBytesDate = [firstProcessor firstBytesDate];
        workDone = [firstProcessor bytesProcessed];
        workToBeDone = [firstProcessor totalBytes];
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
    
    if (!firstBytesDate || workDone == 0 || workToBeDone == 0 || workDone >= workToBeDone)
        return 0.0;

    return -[firstBytesDate timeIntervalSinceNow] * (workToBeDone - workDone) / workDone;
}

- (BOOL)hadError;
{
    return flags.everHadContentError || flags.processingError;
}

- (BOOL)isRunning;
{
    if ([self hadError])
        return NO;
    switch (state) {
        case PipelineInit:
        case PipelinePaused:
        case PipelineAborting:
        case PipelineDead:
            return NO;
        default:
            return YES;
    }
}

- (BOOL)hasThread;
{
    BOOL hasThread = NO;
    
    [processorArrayConditionLock lock]; {
        if (firstProcessor)
            hasThread = [firstProcessor status] == OWProcessorRunning;
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];

    return hasThread;
}

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

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

- (NSString *)compositeTypeString;
{
    NSString *string;

    OFSimpleLock(&displayablesSimpleLock); {
        string = [[compositeTypeString retain] autorelease];
    } OFSimpleUnlock(&displayablesSimpleLock);

    return string;
}

- (unsigned int)workDone;
{
    unsigned int result;

    [processorArrayConditionLock lock]; {
        result = [firstProcessor bytesProcessed];
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
    if (result == 0)
        result = maximumWorkToBeDone;
    return result;
}

- (unsigned int)workToBeDone;
{
    unsigned int result;

    [processorArrayConditionLock lock]; {
        result = [firstProcessor totalBytes];
    }[processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
    if (!result)
        result = maximumWorkToBeDone;
    else if (result > maximumWorkToBeDone)
        maximumWorkToBeDone = result;
    return result;
}


- (NSString *)statusString;
{
    NSString *string = nil;
    
    [processorArrayConditionLock lock]; {
        if (errorReasonString)
            string = errorReasonString;
        else if (firstProcessor)
            string = [firstProcessor statusString];
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];

    if (string)
        return string;
    
    switch (state) {
        case PipelineInit:
            return NSLocalizedStringFromTableInBundle(@"Pipeline created", @"OWF", OWF_Bundle, pipeline status);
        case PipelineBuilding:
            return NSLocalizedStringFromTableInBundle(@"Pipeline building", @"OWF", OWF_Bundle, pipeline status);
        case PipelineRunning:
            return NSLocalizedStringFromTableInBundle(@"Pipeline running", @"OWF", OWF_Bundle, pipeline status);
        case PipelinePaused:
            return NSLocalizedStringFromTableInBundle(@"Pipeline paused", @"OWF", OWF_Bundle, pipeline status);
        case PipelineAborting:
            return NSLocalizedStringFromTableInBundle(@"Pipeline stopping", @"OWF", OWF_Bundle, pipeline status);
        case PipelineDead:
            return [super statusString];
    }
    return nil; // NOTREACHED
}

- (void)setContentInfo:(OWContentInfo *)newContentInfo;
{
    if ([self contentInfo] == newContentInfo)
        return;
    [super setContentInfo:newContentInfo];
    [self _rebuildCompositeTypeString];
}

- (void)deepFlushContentCache;
{
    [super deepFlushContentCache];
    [self flushContentCache];
}

- (unsigned int)taskPriority;
{
    // Give higher priority to longer pipelines, since we really want to finish what we start before we start another pipeline.  This fixes OmniWeb so if you hit a page with, say, 50 inline images, you don't have to wait for all the images to load before any of them start to display.  With this hack, images that are loaded will immediately start imaging.
    return [parentContentInfo baseTaskPriority] - addedContentCount;
}



// Pipeline management

- (void)startProcessingContent;
{
    id <OWContent> lastContent;

    if (state == PipelineAborting || state == PipelineDead)
	return;
    
    [processorArrayConditionLock lock]; {
        NSAutoreleasePool *pool;
        id <OWTarget> targetSnapshot;
        
        if (state == PipelineAborting || state == PipelineDead) {
            [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
            return;
        }
    
        pool = [[NSAutoreleasePool alloc] init];
        targetSnapshot = [self target];
        
        // If another (later) pipeline has grabbed our target before we got a chance to even start, well, c'est la vie.  We suicide rather than fight for her.
        if (targetSnapshot == nil) {
            [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
            [self abortTask];
            [pool release];
            return;
        }
    
        if (state == PipelineInit) {
            id <OWContent> targetContent = nil;
    
            [isa _startPipeline:self forTarget:targetSnapshot];
    
            // Special short-circuit:  if we already have the result of our pipeline cached before we even start the pipeline, deliver it instantly.
            NS_DURING {
                targetContent = [self _availableContentOfType:targetContentType];
            } NS_HANDLER {
            } NS_ENDHANDLER;
            if (targetContent != nil) {
                [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
                [self addContent:targetContent];
                [self _pipelineBuilt];
                [pool release];
                return;
            }
        }
    
        state = PipelineBuilding;
        [pool release];
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];

    OMNI_POOL_START {
        if (OWPipelineDebug)
            NSLog(@"%@: startProcessingContent %@", [self shortDescription], [lastContentType shortDescription]);
        lastContent = [self lastContent];
        if ([lastContent conformsToProtocol:@protocol(OWAddress)])
            [self setLastAddress:(id <OWAddress>)lastContent];
        if (pipelineCoordinator == nil) // We're not locking pipelineCoordinator because we assume multiple threads won't compete for sending a pipeline its first -startProcessing message
            pipelineCoordinator = [[OWPipelineCoordinator pipelineCoordinatorForAddress:lastAddress] retain];
        if (OWPipelineDebug)
            NSLog(@"%@: startProcessingContent: coordinator %@", [self shortDescription], [pipelineCoordinator shortDescription]);
        [self treeActiveStatusMayHaveChanged];
        [pipelineCoordinator buildPipeInPipeline:self];
    } OMNI_POOL_END;
}

- (void)fetch;
{
    if (state == PipelineInit)
        [self startProcessingContent];
    else
        [self refetchContentFromLastAddress];
}

- (BOOL)canRefetchContentFromLastAddress;
{
    return state == PipelineInit || state == PipelineDead;
}

- (void)refetchContentFromLastAddress;
{
    return [self refetchContentFromLastAddressOrSource:NO];
}

- (void)refetchContentFromLastAddressOrSource:(BOOL)useSource;
{
    if (useSource)
        [contentCacheForLastAddress flushCachedContentExceptTypes:[NSSet setWithObject:[OWSourceProcessor sourceContentType]]];
    else
        [contentCacheForLastAddress flushCachedContent];

    if (![self canRefetchContentFromLastAddress]) {
        OWPipeline *clonePipeline;

        clonePipeline = [self copy];
        [clonePipeline startProcessingContent];
        [clonePipeline release];
        return;
    }

    state = PipelineInit;
    [self addContent:lastAddress];
    [self setLastAddress:lastAddress];
    flags.contentError = NO;
    flags.everHadContentError = NO;
    flags.disableContentCache = NO;
    flags.processingError = NO;
    [errorNameString release];
    errorNameString = nil;
    [errorReasonString release];
    errorReasonString = nil;

    [processorArrayConditionLock lock]; {
        if (!unstartedProcessors)
            unstartedProcessors = [[NSMutableArray alloc] init];
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
    
    [self startProcessingContent];
}

// Target

- (id <OWTarget, OFWeakRetain, NSObject>)target;
{
    id <OWTarget, OFWeakRetain, NSObject> autoreleasedTarget;
    
    [targetLock lock]; {
        autoreleasedTarget = [[target retain] autorelease];
    } [targetLock unlock];
    return autoreleasedTarget;
}

- (void)setTarget:(id <OWTarget, OFWeakRetain, NSObject>)newTarget;
{
    id <OWTarget, OWOptionalTarget, OFWeakRetain, NSObject> targetSnapshot;
    id <OWTarget, OFWeakRetain, NSObject> oldTarget;
    NSException *raisedException = nil;

    flags.contentError = NO;
    [targetLock lock]; {
        if (target != nil && newTarget == nil) {
#ifdef DEBUG
            NSLog(@"PROGRAM ERROR: setTarget:nil called to nullify the target of a OWPipeline!");
            OBASSERT(target == nil || newTarget != nil);
#endif
            [targetLock unlock];
            [self invalidate];
            return;
        }

        if (target == newTarget) {
            [targetLock unlock];
            return;
        }
    
        oldTarget = target;
        if (oldTarget != nil) {
            [(NSObject *)oldTarget weakAutorelease];
            [isa _removePipeline:self forTarget:oldTarget];
        }

        target = [(NSObject *)newTarget weakRetain];
        // We're taking a snapshot of the target in case it changes after we unlock targetLock on the next line...  Of course, when that happens you can end up telling a target that it deactivated before you end up telling it that it activated, and other such bizarreness--maybe we should just prevent that from ever happening?
        targetSnapshot = [target retain];
    } [targetLock unlock];

    NS_DURING {
        [isa _addPipeline:self forTarget:targetSnapshot];
        [self setParentContentInfo:[targetSnapshot parentContentInfo]];
        OBASSERT(parentContentInfo != nil);
    
        targetContentType = [targetSnapshot targetContentType];
        [targetTypeFormatString release];
        targetTypeFormatString = [[targetSnapshot targetTypeFormatString] retain];
        [self _rebuildCompositeTypeString];
    
        if ([targetSnapshot respondsToSelector:@selector(pipelineDidBegin:)])
            [targetSnapshot pipelineDidBegin:self];
    
        [self _notifyTargetOfTreeActivation:targetSnapshot];
        [self _notifyTargetOfTreeDeactivation:oldTarget];
    } NS_HANDLER {
        raisedException = localException;
    } NS_ENDHANDLER;
    [targetSnapshot release];
    if (raisedException != nil)
        [raisedException raise];
}

- (void)invalidate;
{
    if (OWPipelineDebug)
        NSLog(@"%@: invalidate", [self shortDescription]);
    flags.contentError = NO;
    [targetLock lock]; {
        if (target != nil) {
            [isa _removePipeline:self forTarget:target];
            [(NSObject *)target weakAutorelease];
            target = nil;
            [self setParentContentInfo:[OWContentInfo orphanParentContentInfo]];
        }
    } [targetLock unlock];

    [_lastContentLock lock];
    [_lastContent release];
    _lastContent = nil;
    [_lastContentLock unlock];
    [self _cleanupPipelineIfDead];
    OBPOSTCONDITION(target == nil);
}

- (void)parentContentInfoLostContent;
{
    id <OWTarget, OWOptionalTarget, NSObject> targetSnapshot;
    
    OMNI_POOL_START {
        targetSnapshot = (id)[self target];
        if ([targetSnapshot respondsToSelector:@selector(parentContentInfoLostContent)])
            [targetSnapshot parentContentInfoLostContent];
    } OMNI_POOL_END;
}

- (void)updateStatusOnTarget;
{
    OMNI_POOL_START {
        [self _updateStatusOnTarget:[self target]];
    } OMNI_POOL_END;
}


// Content

- (id <OWContent>)lastContent;
{
    id <OWContent> lastContent;

    [_lastContentLock lock];
    lastContent = [[_lastContent retain] autorelease];
    [_lastContentLock unlock];
    return lastContent;
}

- (OWContentType *)lastContentType;
{
    return lastContentType;
}

- (OWContentCache *)contentCacheForLastAddress;
{
    return contentCacheForLastAddress;
}

- (void)addContent:(id <OWContent>)newContent;
{
    OWContentType *newContentType;

    if (state == PipelineDead)
        return;
    newContentType = [newContent contentType];
    [_lastContentLock lock];
    if (newContent != _lastContent) {
        [_lastContent release];
        _lastContent = [newContent retain];
        addedContentCount++;
    }
    lastContentType = newContentType;
    [_lastContentLock unlock];
    [self setContentInfo:[newContent contentInfo]];
}

- (void)contentError;
{
    if (OWPipelineDebug)
        NSLog(@"%@: contentError", [self shortDescription]);
    flags.contentError = YES;
    flags.everHadContentError = YES;
    flags.delayedForError = NO;
    [contentCacheForLastAddress setContentIsError:YES];
    [contentCacheForLastAddress expireErrorContentAfterTimeInterval:5.0];
}

- (void)disableContentCache;
{
    flags.disableContentCache = YES;
}

- (void)cacheContent;
{
    if (flags.disableContentCache || !contentCacheForLastAddress)
	return;
    [contentCacheForLastAddress registerContent:[self lastContent]];
}

- (void)permanentRedirection:(id <OWAddress>)redirectTo;
{
    NSDictionary *userInfo;
    /* TODO: This method may be unneeded --- its functionality might be best subsumed into -cacheContent --- see the comment in OWHTTPSession. */

    userInfo = [[NSDictionary alloc] initWithObjectsAndKeys:redirectTo, permanentRedirectionContentKey, nil];

    [fetchedContentNotificationCenter postNotificationName:[lastAddress cacheKey] object:self userInfo:userInfo];
}

- (void)flushContentCache;
{
    [contentCacheForLastAddress flushCachedContent];
}

- (id)contextObjectForKey:(NSString *)key;
{
    id object;

    [contextLock lock]; {
        object = [context objectForKey:key];
    } [contextLock unlock];

    return object;
}

- (void)setContextObject:(id)anObject forKey:(NSString *)key;
{
    [contextLock lock]; {
        if (anObject)
            [context setObject:anObject forKey:key];
        else
            [context removeObjectForKey:key];
    } [contextLock unlock];
}

- (id)setContextObjectNoReplace:(id)anObject forKey:(NSString *)key;
{
    id existingObject;
    
    [contextLock lock]; {
        existingObject = [context objectForKey:key];
        if (anObject && !existingObject) {
            [context setObject:anObject forKey:key];
            existingObject = anObject;
        }
    } [contextLock unlock];
    return existingObject;
}

- (NSDictionary *)contextDictionary;
{
    NSDictionary *snapshotContextDictionary;

    [contextLock lock];
    snapshotContextDictionary = [[NSDictionary alloc] initWithDictionary:context];
    [contextLock unlock];
    return [snapshotContextDictionary autorelease];
}

- (OWHeaderDictionary *)headerDictionary;
{
    if (headerDictionary == nil)
        headerDictionary = [[OWHeaderDictionary alloc] init];

    return headerDictionary;
}

- (void)setHeaderDictionary:(OWHeaderDictionary *)newHeaderDictionary fromURL:(OWURL *)newHeaderURL;
{
    if (headerDictionary != newHeaderDictionary) {
        [headerDictionary release];
        headerDictionary = [newHeaderDictionary retain];
    }
    if (headerURL != newHeaderURL) {
        [headerURL release];
        headerURL = [newHeaderURL retain];
    }
}

- (void)addHeader:(NSString *)headerName value:(NSString *)headerValue;
{
    [[self headerDictionary] addString:headerValue forKey:headerName];
}

//

- (OWProcessor *)firstProcessor;
{
    OWProcessor *processor;

    [processorArrayConditionLock lock]; {
        processor = [[firstProcessor retain] autorelease];
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
    return processor;
}

- (void)addProcessor:(OWProcessor *)aProcessor;
{
    if (state == PipelineAborting || state == PipelineDead)
	return;
    [processorArrayConditionLock lock]; {
        if (state != PipelineAborting && state != PipelineDead) {
            [processorArray addObject:aProcessor];
            [unstartedProcessors addObject:aProcessor];
            firstProcessor = [processorArray objectAtIndex:0];
        }
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
}

- (void)removeProcessor:(OWProcessor *)aProcessor;
{
    [[self retain] autorelease];
    [processorArrayConditionLock lock]; {
        [processorArray removeObject:aProcessor];
        if ([processorArray count])
            firstProcessor = [processorArray objectAtIndex:0];
        else
            firstProcessor = nil;
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
    [self _deactivateIfPipelineHasNoProcessors];
}

- (void)processorDidRetire:(OWProcessor *)aProcessor;
{
    [self removeProcessor:aProcessor];
}

- (void)processorStatusChanged:(OWProcessor *)aProcessor ;
{
    [self updateStatusOnTarget];
}

- (void)processor:(OWProcessor *)aProcessor hasErrorName:(NSString *)name;
{
    [self processor:aProcessor hasErrorName:name reason:nil];
}

- (void)processor:(OWProcessor *)aProcessor hasErrorName:(NSString *)name reason:(NSString *)reason;
{
    NSMutableDictionary *userInfo;

    userInfo = [[NSMutableDictionary alloc] initWithCapacity:3];
    [userInfo setObject:self forKey:OWPipelineHasErrorNotificationPipelineKey];
    if (aProcessor)
	[userInfo setObject:aProcessor forKey:OWPipelineHasErrorNotificationProcessorKey];
    [userInfo setObject:name forKey:OWPipelineHasErrorNotificationErrorNameKey];
    [userInfo setObject:reason forKey:OWPipelineHasErrorNotificationErrorReasonKey];
    [pipelineErrorNotificationCenter postNotificationName:OWPipelineHasErrorNotificationName object:self userInfo:userInfo];
    [userInfo release];

    flags.processingError = YES;
    if (name != errorNameString) {
        [errorNameString release];
        errorNameString = [name retain];
    }
    if (reason != errorReasonString) {
        [errorReasonString release];
        errorReasonString = [reason retain];
    }
    
    [self updateStatusOnTarget];
}


// OFWeakRetain protocol

OFWeakRetainConcreteImplementation_IMPLEMENTATION

- (void)invalidateWeakRetains;
{
    if (OWPipelineDebug)
        NSLog(@"%@: invalidateWeakRetains", [self shortDescription]);
    // Invalidate the weak retains from our processors
    [processorArrayConditionLock lock]; {
        [processorArray removeAllObjects];
        [unstartedProcessors removeAllObjects];
    } [processorArrayConditionLock unlockWithCondition:PipelineProcessorConditionNoProcessors];
}

// NSCopying protocol

- copyWithZone:(NSZone *)zone;
{
    OWPipeline *newPipeline;

    OMNI_POOL_START {
        newPipeline = [[isa allocWithZone:zone] initWithContent:lastAddress target:[self target] useCachedErrorContent:NO];
    } OMNI_POOL_END;
    return newPipeline;
}

@end


@implementation OWPipeline (SubclassesOnly)

- (void)deactivate;
{
    OMNI_POOL_START {
        id <OWTarget, OWOptionalTarget, NSObject> targetSnapshot;
    
        if (OWPipelineDebug)
            NSLog(@"%@: done", [self shortDescription]);
        
        if (lastAddress && state != PipelineAborting && ![self hadError]) {
            // On a successful fetch, post a notification that we've fetched this address.  Bookmarks watch for these notifications.
            NSDictionary *userInfo;
            id <OWContent> lastContent;
    
            lastContent = [self lastContent];
            userInfo = lastContent ? [[NSDictionary alloc] initWithObjectsAndKeys:lastContent, fetchedContentContentKey, nil] : nil;
            [fetchedContentNotificationCenter postNotificationName:[lastAddress cacheKey] object:self userInfo:userInfo];
            [userInfo release];
        }
    
        state = PipelineDead; // WJS 3/31/00 - moved this because when we short-circuited we don't get a final notification on the Browser and the title would stay at "Loading X..." and the stop button wouldn't highlight.
        targetSnapshot = (id)[self target];
        if ([targetSnapshot respondsToSelector:@selector(pipelineDidEnd:)])
            [targetSnapshot pipelineDidEnd:self];
    
        [pipelineCoordinator pipebuildingComplete:self];
        [self _releasePipelineCoordinator];
        [_lastContentLock lock];
        [_lastContent release];
        _lastContent = nil;
        [_lastContentLock unlock];
        [self treeActiveStatusMayHaveChanged]; // Will call -deactivateInTree
    } OMNI_POOL_END;
}

- (void)setLastAddress:(id <OWAddress>)newLastAddress;
{
    OFSimpleLock(&displayablesSimpleLock); {
        if (lastAddress != newLastAddress) {
            if ([newLastAddress isKindOfClass:[OWAddress class]])
                newLastAddress = [(OWAddress *)newLastAddress addressWithTarget:nil];
            if (lastAddress != newLastAddress) {
                [lastAddress release];
                lastAddress = [newLastAddress retain];
            }
        }
        [contentCacheForLastAddress release];
        contentCacheForLastAddress = [[OWContentCache contentCacheForAddress:lastAddress] retain];
        // Clean slate!
        [processedContentTypes release];
        processedContentTypes = nil;
        flags.disableContentCache = NO;
    
        if ([contentCacheForLastAddress contentIsError]) {
            if (flags.useCachedErrorContent)
                [self contentError];
            else
                [contentCacheForLastAddress flushCachedErrorContent];
        }
    } OFSimpleUnlock(&displayablesSimpleLock);
}

// This is called by the OWPipelineCoordinator

- (void)_buildPipe;
{
    id <OWContent> lastContent, targetContent;

    lastContent = [self lastContent];
    targetContent = [self _availableContentOfType:targetContentType];
    if (targetContent == nil && lastContent != nil && !flags.contentError) {
        // Keep building pipeline, we aren't there yet
        [self _createNextProcessor];
        return;
    }

    // Pipeline is built!
    if (targetContent && lastContent != targetContent) {
        [_lastContentLock lock];
        [_lastContent release];
        _lastContent = [targetContent retain];
        [_lastContentLock unlock];
        lastContentType = [targetContent contentType];
        [self setContentInfo:[targetContent contentInfo]];
    }
    [self _pipelineBuilt];
}


@end


@implementation OWPipeline (Private)

// Status monitors

+ (void)_updateStatusMonitors:(NSTimer *)timer;
{
    NSArray *activeChildrenCopy;
    int pipelineIndex, pipelineCount;

    activeChildrenCopy = [OWContentInfo allActiveTasks];

    pipelineCount = [activeChildrenCopy count];
    for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
        OWPipeline *pipeline;

        pipeline = [activeChildrenCopy objectAtIndex:pipelineIndex];
        if ([pipeline isKindOfClass:[OWPipeline class]])
            [pipeline updateStatusOnTarget];
    }
    
    [[NSNotificationCenter defaultCenter] postNotificationName:OWPipelineTreePeriodicUpdateNotificationName object:nil];
    
    activeTreeHasUndisplayedChanges = NO;
}


// Methods managing the targetPipelinesMapTable

+ (void)_addPipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
{
    NSMutableArray *pipelines;

    OBPRECONDITION(aTarget);
    OBPRECONDITION(aPipeline);
    OFSimpleLock(&targetPipelinesMapTableLock); {
        pipelines = NSMapGet(targetPipelinesMapTable, aTarget);
        if (!pipelines) {
            pipelines = [[NSMutableArray alloc] init];
            NSMapInsertKnownAbsent(targetPipelinesMapTable, aTarget, pipelines);
        }
        OBPRECONDITION([pipelines indexOfObjectIdenticalTo:aPipeline] == NSNotFound);
        [pipelines addObject:aPipeline];
    } OFSimpleUnlock(&targetPipelinesMapTableLock);
}

+ (void)_startPipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
{
    NSMutableArray *pipelines;
    unsigned int pipelineIndex;

    OBPRECONDITION(aTarget);
    OBPRECONDITION(aPipeline);
    OFSimpleLock(&targetPipelinesMapTableLock); {
        pipelines = NSMapGet(targetPipelinesMapTable, aTarget);
        if (!pipelines) {
            pipelines = [[NSMutableArray alloc] init];
            NSMapInsertKnownAbsent(targetPipelinesMapTable, aTarget, pipelines);
        }
        pipelineIndex = [pipelines indexOfObjectIdenticalTo:aPipeline];
        OBPRECONDITION(pipelineIndex != NSNotFound);
    
        // Move pipeline to the end of the target's pipelines array
        [aPipeline retain]; 
        [pipelines removeObjectAtIndex:pipelineIndex];
        [pipelines addObject:aPipeline];
        [aPipeline release];
    } OFSimpleUnlock(&targetPipelinesMapTableLock);
}

+ (void)_removePipeline:(OWPipeline *)aPipeline forTarget:(id <OWTarget>)aTarget;
{
    NSMutableArray *pipelines;

    OBPRECONDITION(aTarget);
    OBPRECONDITION(aPipeline);
    OFSimpleLock(&targetPipelinesMapTableLock); {
        pipelines = NSMapGet(targetPipelinesMapTable, aTarget);
        OBPRECONDITION(pipelines && [pipelines indexOfObjectIdenticalTo:aPipeline] != NSNotFound);
        [[aPipeline retain] autorelease];
        [pipelines removeObjectIdenticalTo:aPipeline];
        if ([pipelines count] == 0) {
            NSMapRemove(targetPipelinesMapTable, aTarget);
            [pipelines release];
        }
    } OFSimpleUnlock(&targetPipelinesMapTableLock);
}

+ (void)_target:(id <OWTarget>)aTarget acceptedContentFromPipeline:(OWPipeline *)acceptedPipeline;
{
    NSArray *pipelines;
    unsigned int pipelineIndex, pipelineCount;

    // Nullify the targets of all pipelines that were created BEFORE this one with the same target, because we don't want them returning content later and overwriting our content.  Pipelines created AFTER us are left alone, so if they ever return valid content they'll steal our target as their own.
    pipelines = [self pipelinesForTarget:aTarget];
    pipelineCount = [pipelines count];
    for (pipelineIndex = 0; pipelineIndex < pipelineCount; pipelineIndex++) {
        OWPipeline *aPipeline;

        aPipeline = [pipelines objectAtIndex:pipelineIndex];
        if (aPipeline == acceptedPipeline)
            return;
        [aPipeline invalidate];
    }
#ifdef DEBUG
    NSLog(@"PROGRAM ERROR: _target:acceptedContentFromPipeline: called with a pipeline that isn't registered for the target");
    OBASSERT(NO);
#endif
}

//

- (NSSet *)_cachedContentTypesMinusProcessed;
{
    NSSet *cachedContentTypes;

    cachedContentTypes = [contentCacheForLastAddress contentTypes];
    if (processedContentTypes) {
        NSMutableSet *contentTypes;

        contentTypes = [NSMutableSet setWithSet:cachedContentTypes];
        [contentTypes minusSet:processedContentTypes];
        return contentTypes;
    }

    return cachedContentTypes;
}

- (id <OWContent>)_availableContentOfType:(OWContentType *)searchType;
{
    if (lastContentType == searchType)
	return [self lastContent];
    if ([self hadError])
        return nil; // Don't return objects out of the cache when we've had an error
    return [contentCacheForLastAddress contentOfType:searchType];
}

- (BOOL)_readyContentType:(OWContentType *)aContentType;
{
    id <OWContent> readyContent;

    if (lastContentType == aContentType)
	return YES;
    readyContent = [contentCacheForLastAddress contentOfType:aContentType];
    if (!readyContent)
        return NO; // Sorry, that content is no longer available
    [_lastContentLock lock];
    [_lastContent release];
    _lastContent = [readyContent retain];
    [_lastContentLock unlock];
    lastContentType = [readyContent contentType];
    [self setContentInfo:[readyContent contentInfo]];
    return YES;
}

- (void)_processedContentType:(OWContentType *)aContentType;
{
    if (!aContentType)
	return;
    if (!processedContentTypes)
	processedContentTypes = [[NSMutableSet alloc] initWithCapacity:1];

    if (OWPipelineDebug)
        NSLog(@"%@: processedContentType %@", [self shortDescription], [aContentType shortDescription]);
    
    [processedContentTypes addObject:aContentType];
}

- (void)_startUnstartedProcessors;
{
    NSArray *unstartedProcessorsCopy;
    unsigned int unstartedProcessorsIndex, unstartedProcessorsCount;

    [processorArrayConditionLock lock]; {
        unstartedProcessorsCopy = [[NSArray alloc] initWithArray:unstartedProcessors];
        [unstartedProcessors removeAllObjects];
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];

    unstartedProcessorsCount = [unstartedProcessorsCopy count];
    for (unstartedProcessorsIndex = 0; unstartedProcessorsIndex < unstartedProcessorsCount; unstartedProcessorsIndex++) {
        OWProcessor *unstartedProcessor;

        unstartedProcessor = [unstartedProcessorsCopy objectAtIndex:unstartedProcessorsIndex];
        if (OWPipelineDebug)
            NSLog(@"%@: starting %@", [self shortDescription], [unstartedProcessor shortDescription]);
        [unstartedProcessor startProcessing];
    }

    [unstartedProcessorsCopy release];
}

- (void)_deactivateIfPipelineHasNoProcessors;
{
    if (state != PipelineDead && [self _hasNoProcessors]) {
	[self deactivate];
    }
}

- (void)_releasePipelineCoordinator;
{
    OWPipelineCoordinator *oldPipelineCoordinator;

    // Release the pipeline coordinator in a thread-safe manner
    oldPipelineCoordinator = pipelineCoordinator;
    pipelineCoordinator = nil;
    [oldPipelineCoordinator release];
}

- (void)_cleanupPipelineIfDead;
{
    // Note: This non-locked access to target should be fine because even if we're half-way through a write our equality test will still give a reasonable result
    if (target != nil || [self treeHasActiveChildren])
        return;
        
    [processorArrayConditionLock lockWhenCondition:PipelineProcessorConditionNoProcessors];
        
    if (state == PipelineInit) {
        state = PipelineDead;
        [_lastContentLock lock];
        if (_lastContent != nil) {
            [_lastContent release];
            _lastContent = nil;
        }
        [_lastContentLock unlock];
    }
    OBASSERT(_lastContent == nil);

    [processorArrayConditionLock unlock];

    [[self retain] autorelease]; // Ensure we stick around for a little while yet
    [self setParentContentInfo:nil];
    [self setContentInfo:nil];

    [[NSNotificationCenter defaultCenter] postNotificationName:OWPipelineWillDeallocateNotificationName object:self];
}

- (void)_pipelineBuilt;
{
    OMNI_POOL_START {
        OWTargetContentDisposition contentDisposition;
        id <OWTarget, OWOptionalTarget, NSObject> targetSnapshot;
        id <OWContent> lastContent;
    
        [pipelineCoordinator pipebuildingComplete:self];
        [self _releasePipelineCoordinator];
        state = PipelineRunning;
    
        contentDisposition = OWTargetContentDisposition_ContentRejectedCancelPipeline;
        targetSnapshot = (id)[self target];
        lastContent = [self lastContent];
        NS_DURING {
            if (flags.contentError) {
                if (OWPipelineDebug)
                    NSLog(@"%@: delivering error indication", [self shortDescription]);
                if ([targetSnapshot respondsToSelector:@selector(pipeline:hasErrorContent:)])
                    contentDisposition = [targetSnapshot pipeline:self hasErrorContent:lastContent];
            } else if (lastContentType == targetContentType) {
                if (OWPipelineDebug)
                    NSLog(@"%@: delivering content", [self shortDescription]);
                contentDisposition = [targetSnapshot pipeline:self hasContent:lastContent];
            } else {
                if (OWPipelineDebug)
                    NSLog(@"%@: delivering alternate content", [self shortDescription]);
                if ([targetSnapshot respondsToSelector:@selector(pipeline:hasAlternateContent:)])
                    contentDisposition = [targetSnapshot pipeline:self hasAlternateContent:lastContent];
            }
        } NS_HANDLER {
            if (OWPipelineDebug)
                NSLog(@"%@: exception from target: %@", [self shortDescription], localException);
            [self processor:nil hasErrorName:[localException displayName] reason:[localException reason]];
        } NS_ENDHANDLER;
    
        if (OWPipelineDebug)
            NSLog(@"%@: contentDisposition=%d", [self shortDescription], contentDisposition); 
    
        OBASSERT((id)targetSnapshot == (id)target || contentDisposition == OWTargetContentDisposition_ContentUpdatedOrTargetChanged);
        switch (contentDisposition) {
            case OWTargetContentDisposition_ContentAccepted:
                [isa _target:targetSnapshot acceptedContentFromPipeline:self];
                break;
            case OWTargetContentDisposition_ContentRejectedSavePipeline:
                break;
            case OWTargetContentDisposition_ContentRejectedCancelPipeline:
                [self invalidate];
                break;
            case OWTargetContentDisposition_ContentUpdatedOrTargetChanged:
                break;
        }
    
        [self updateStatusOnTarget];
        [self _deactivateIfPipelineHasNoProcessors];
    } OMNI_POOL_END;
}

- (BOOL)_hasNoProcessors;
{
    BOOL noMoreProcessors;

    [processorArrayConditionLock lock]; {
        noMoreProcessors = [processorArray count] == 0;
    } [processorArrayConditionLock unlockWithCondition:noMoreProcessors ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];
    return noMoreProcessors;
}

- (void)_createNextProcessor;
{
    OWContentTypeLink *link;
    Class processorClass;
    BOOL shouldStartProcessors;

    do {
        link = [OWContentType linkForTargetContentType:targetContentType fromContentType:lastContentType orContentTypes:[self _cachedContentTypesMinusProcessed]];
        if (!link) {
            [self _pipelineBuilt];
            return;
        }
    } while (![self _readyContentType:[link sourceContentType]]);

    if ([link targetContentType] == [OWContentType wildcardContentType])
        [self _processedContentType:lastContentType];

    if (OWPipelineDebug)
        NSLog(@"%@: creating %@ to convert %@ to %@", [self shortDescription], [link processorClassName], [[link sourceContentType] contentTypeString], [[link targetContentType] contentTypeString]);

    processorClass = [OFBundledClass classNamed:[link processorClassName]];

    [processorArrayConditionLock lock]; {
        shouldStartProcessors = [unstartedProcessors count] == 0;
    } [processorArrayConditionLock unlockWithCondition:(firstProcessor == nil) ? PipelineProcessorConditionNoProcessors : PipelineProcessorConditionSomeProcessors];

    [[[processorClass alloc] initWithPipeline:self] release];

    if (shouldStartProcessors)
	[self _startUnstartedProcessors];
}


// Target stuff

- (void)_notifyTargetOfTreeActivation;
{
    OMNI_POOL_START {
        [self _notifyTargetOfTreeActivation:[self target]];
    } OMNI_POOL_END;
}

- (void)_notifyTargetOfTreeDeactivation;
{
    OMNI_POOL_START {
        [self _notifyTargetOfTreeDeactivation:[self target]];
    } OMNI_POOL_END;
}

- (void)_notifyTargetOfTreeActivation:(id <OWTarget>)aTarget;
{
    NSNotification *activationNotification;

    [self _updateStatusOnTarget:aTarget];

    activationNotification = [NSNotification notificationWithName:OWPipelineTreeActivationNotificationName object:self];
    if ([(id <NSObject>)aTarget respondsToSelector:@selector(pipelineTreeDidActivate:)])
        [(id <OWOptionalTarget>)aTarget pipelineTreeDidActivate:activationNotification];
        
    [[NSNotificationCenter defaultCenter] postNotification:activationNotification];
}

- (void)_notifyTargetOfTreeDeactivation:(id <OWTarget>)aTarget;
{
    id <OWTarget, OWOptionalTarget, NSObject> optionalTarget = aTarget;
    NSNotification *deactivationNote;

    [self _updateStatusOnTarget:aTarget];

    deactivationNote = [NSNotification notificationWithName:OWPipelineTreeDeactivationNotificationName object:self];
    if ([optionalTarget respondsToSelector:@selector(pipelineTreeDidDeactivate:)])
        [optionalTarget pipelineTreeDidDeactivate:deactivationNote];

    [[NSNotificationCenter defaultCenter] postNotification:deactivationNote];
}

- (void)_updateStatusOnTarget:(id <OWTarget>)aTarget;
{
    id <OWTarget, OWOptionalTarget, NSObject> optionalTarget = aTarget;

    if ([optionalTarget respondsToSelector:@selector(updateStatusForPipeline:)]) {
        BOOL targetIsCurrentTarget;

        [targetLock lock]; {
            targetIsCurrentTarget = (id)optionalTarget == (id)target;
        } [targetLock unlock];

        if (targetIsCurrentTarget)
            [optionalTarget updateStatusForPipeline:self];
        else
            [optionalTarget updateStatusForPipeline:[isa lastActivePipelineForTarget:optionalTarget]];
    }
}

// NB The string (compositeTypeString) built by this method should be in the user's current language.
- (void)_rebuildCompositeTypeString;
{
    NSString *contentTypeString, *newCompositeTypeString;

    contentTypeString = [[self contentInfo] typeString];
    if (!contentTypeString) {
        id <OWTarget, OWOptionalTarget, NSObject> targetSnapshot;
    
        targetSnapshot = (id)[self target];
        if ([targetSnapshot respondsToSelector:@selector(expectedContentTypeString)])
            contentTypeString = [targetSnapshot expectedContentTypeString];
        if (!contentTypeString && [lastAddress isKindOfClass:[OWAddress class]])
            contentTypeString = [[(OWAddress *)lastAddress probableContentTypeBasedOnPath] readableString];
        if (!contentTypeString)
            contentTypeString = @"www/unknown";
    }

    if (targetTypeFormatString)
        newCompositeTypeString = [[NSString alloc] initWithFormat:targetTypeFormatString, contentTypeString];
    else
        newCompositeTypeString = [contentTypeString retain];

    OFSimpleLock(&displayablesSimpleLock); {
        [compositeTypeString release];
        compositeTypeString = newCompositeTypeString;
    } OFSimpleUnlock(&displayablesSimpleLock);
}

// Debugging

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

    debugDictionary = [super debugDictionary];

    if (target)
        [debugDictionary setObject:OBShortObjectDescription(target) forKey:@"target"];
    if (_lastContent)
        [debugDictionary setObject:[(NSObject *)_lastContent shortDescription] forKey:@"lastContent"];
    if (processorArray)
        [debugDictionary setObject:processorArray forKey:@"processorArray"];
    if (context)
        [debugDictionary setObject:context forKey:@"context"];

    return debugDictionary;
}

@end
