"""
$RCSfile: ProcessorRegistry.py,v $

ZopeXMLMethods provides filters to apply to Zope objects for XML/XSLT
processing.  XSLTMethod associates XSLT transformers with XML
documents.  XSLTMethod automatically transforms an XML document via
XSLT, where the XML document is obtained from another Zope object (the
'source' object) via acquisition.  XPathMethod automatically evaluates
an XPath method on an XML document obtained from the Zope object to
which it is applied.

ProcessorRegistry automatically detects which of the supported
processor(s) are available in the environment, and makes it possible
to dynamically choose/switch processors at runtime

Author: <a href="mailto:cstrong@arielpartners.com">Craeg Strong</a>
Release: 1.0

$Id: ProcessorRegistry.py,v 1.3 2005/09/06 02:08:59 arielpartners Exp $
"""

__cvstag__  = '$Name:  $'[6:-2]
__date__    = '$Date: 2005/09/06 02:08:59 $'[6:-2]
__version__ = '$Revision: 1.3 $'[10:-2]

from zope.interface import implements
from interfaces import IProcessorRegistry, IXPathProcessor, \
     IXSLTProcessor, IDTDValidator

#
# One possibility to avoid this hardcoding is to look in
# the current directory for any Python module ending in
# "Processor," but that is more complex and relies on
# consistency of naming convention.  :-(
# 
CandidateProcessors = ( 'FourSuite11Processor', \
                        'FourSuiteProcessor', \
                        'LibXsltProcessor', \
                        'PyanaProcessor', \
                        'SabPythProcessor', )


################################################################
# ProcessorRegistry class
################################################################

class ProcessorRegistry:
    """

    ProcessorRegistry automatically detects which of the supported
    processor(s) are available in the environment, and makes it
    possible to dynamically choose/switch processors at runtime.

    I designed this to do lazy loading to enable ZSyncing from one
    Zope instance to another where the two Zope instances *do not*
    share the exact same set of XSLT libraries.  For a small
    performance price, we get lots more flexibility.  Besides, if you
    want performance, you should be using the CacheManager...

    CKS 10/31/2002

    @@ Rework in Python2.2 to function as an iterator

    Adapted from ProcessorChooser and redesigned to resemble
    GeneratorRegistry, CKS 6/10/2003

    """

    implements(IProcessorRegistry)

    def __init__(self):
        self._registry  = {} # name          -> object
        self._preferred = {} # interfaceName -> (name, exclusive)

    def reload(self):
        "Load the processors: they register themselves."
        for className in CandidateProcessors:
            try:
                klass = __import__(className, globals(),
                                   locals(), [className])
            except:
                pass

    def prefer(self, processorType, processorName, exclusive=0):
        """
        For a given processor type, indicate a preference for one
        processor over another.  Set the 'exclusive' flag to indicate
        that no other choices should be offered, even if available.
        To change your preference, simply call this method again.

        This is used to indicate global preferences.  Each XSLTMethod
        or XPathMethod instance stores their own choice.  Today, this
        method is used only in testing.
        """
        # @@ try storing the type directly here...
        #self._preferred[processorType.__name__] = (processorName,exclusive)
        self._preferred[processorType] = (processorName,exclusive)

    #def __reduce__(self):
    #    "Helper method for pickling"
    #    return self.__name__

    def __getstate__(self):
        """Only save preferences, do not pickle processor instances or we
        wont be able to ZSync to a differently-configured Zope"""
        return self._preferred()

    def __setstate__(self, state):
        "called with what we saved in getstate"
        self._preferred.update(state)

    def __getinitargs__(self):
        "helper method for pickling"
        return ()

    def register(self, proc):
        "register the given processor"
        self._registry[proc.name] = proc
            
    def names(self, processorType):
        """
        
        Return list of names of the currently available processors of
        the given type.

        If an exclusive preference has been expressed, *and* the
        preferred processor is available, return only that.

        Call reload() first to ensure we pick up new processor
        libraries if we just got ZSynced to another Zope instance.

        """
        return [ob.name for ob in self.items(processorType)]

    def items(self, processorType):
        """

        Return list of currently available processors of the given type.

        If an exclusive preference has been expressed, *and* the
        preferred processor is available, return only that.

        Call reload() first to ensure we pick up new processor
        libraries if we just got ZSynced to another Zope instance.

        """
        self.reload()

        # do we have an exclusive preference?
        (pref, exclusive) = self._preferred.get(processorType, (None, 0))

        candidates = [ob for ob in self._registry.values() if processorType.isImplementedBy(ob)]

        if candidates:
            if exclusive == 1:
                preferred = [ob for ob in candidates if ob.name == pref]
                if len(preferred) > 0:
                    return preferred
            else:
                return candidates
        else:
            return []

    def item(self, processorType, processorName):
        """
        
        Obtain the object encapsulating the named processor, if it is
        available.

        Otherwise, return the defaultItem()
        
        """
        self.reload()

        ob = self._registry.get(processorName, None)
        
        if ob is None:
            # otherwise try the default
            print "Sorry, processor", processorName, "not available."
            ob = self.defaultItem(processorType)

        return ob

    def defaultItem(self, processorType):
        """
        Obtain the object encapsulating the preferred processor of the
        given type, if it is available.

        Otherwise try all other processors in random order.

        Finally, give up if we don't have a single processor
        available.

        Call reload() first to ensure we pick up new processor
        libraries if we just got ZSynced to another Zope instance.

        """
        return self._registry[self.defaultName(processorType)]

    def defaultName(self, processorType):
        """
        Obtain the name of the preferred processor of the
        given type, if it is available.

        Otherwise try all other processors in random order.

        Finally, give up if we don't have a single processor
        available.

        Call reload() first to ensure we pick up new processor
        libraries if we just got ZSynced to another Zope instance.
        
        """
        self.reload()

        if len(self._registry) == 0:
            # not self._registry is equivalent, but more confusing
            message = "Sorry, no instances of an %s processor are available." % \
                      (processorType.__name__)
            raise Exception(message)
        else:
            # do we have a preference?        
            (pref, exclusive) = self._preferred.get(processorType,
                                                    (None, 0))
            if pref and self._registry.has_key(pref):
                return pref
            else:
                # return first one in random order
                return self._registry.keys()[0]

ProcessorRegistry = ProcessorRegistry()
    
# EOF ProcessorRegistry.py
