"""
$RCSfile: XSLTMethod.py,v $

ZopeXMLMethods provides methods 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.

Author: Craeg Strong <cstrong@arielpartners.com>
Modified by Philipp von Weitershausen <philikon@philikon.de>
Release: 1.0

$Id: XSLTMethod.py,v 1.8 2003/06/19 00:31:48 arielpartners Exp $
"""

__cvstag__  = '$Name:  $'[6:-2]
__date__    = '$Date: 2003/06/19 00:31:48 $'[6:-2]
__version__ = '$Revision: 1.8 $'[10:-2]

# peer classes/modules
from interfaces import IXSLTMethod
from processors.interfaces import IXSLTProcessor
from processors.ProcessorRegistry import ProcessorRegistry
from GeneratorRegistry import GeneratorRegistry

# our base class
from XMLMethod import XMLMethod, getPublishedResult, \
     PERM_VIEW, PERM_EDIT, PERM_FTP, PERM_CONTENT, PERM_MANAGE

# Zope builtins
import Globals
from Globals import MessageDialog
from Acquisition import aq_parent, aq_get
from AccessControl import ClassSecurityInfo
from Products.PageTemplates.PageTemplateFile import PageTemplateFile

################################################################
# Defaults
################################################################
parametersPropertyName        = 'XSLparameters'

################################################################
# Utilties
################################################################

def findCacheManager(self):
    """
    Find an instance of CacheManager to use (optionally, provided
    the caching property is set to 'on')

    CacheManager is purely optional.  If it is present, caching may be
    done.  If applicable, the XSLTMethod will use the CacheManager
    it finds via this method
        
    The current policy is as follows: use the first cache manager
    found by acquisition.  Alternatively, a more complex policy could
    be used such as
    a) looking in some predefined place, where the place could be
    specified in some property (a la Zope3 explicit acquisition), or
    b) finding all cache managers in the ZODB and presenting a list
    for selection by the user.
    For right now, we don't want to over-engineer the damn thing.  CKS
    8/4/2002.
    """
    metaType = 'XML Method Cache Manager'

    cm     = None
    folder = self
    root   = self.getPhysicalRoot()
    
    while cm is None:
        if folder.isPrincipiaFolderish:
            cacheManagers = folder.objectValues(metaType)
            if cacheManagers:
                cm = cacheManagers[0]
        if cm is None:
            if folder is root:
                return None
            else:
                folder = aq_parent(folder)
    return cm

    # Below gets confused by context aquisition.  Bad idea.
    #
    #cacheManagers = self.superValues(metaType)
    #print "got cache managers", cacheManagers
    #if cacheManagers:
    #    return cacheManagers[0]
    #else:
    #    return None



################################################################
# Module Scoped Convenience Methods
################################################################

def addInstance(folder,
                id, title='', description='', selected_processor='',
                xslTransformerId='', content_type="text/html",
                behave_like='', caching='on', debugLevel=0):
    """
    This is a convenience factory method for creating an instance of
    XSLTMethod.  It returns the object created, and may therefore be
    more convenient than the addXSLTMethod() method it calls.  It is
    used by the unit testing programs.
    """
    folder.manage_addProduct['ZopeXMLMethods'].addXSLTMethod(
        id, title, description, selected_processor, xslTransformerId,
        content_type, behave_like, caching, debugLevel)
    return folder[id]

################################################################
# Contructors
################################################################

manage_addXSLTMethodForm = PageTemplateFile('www/create.pt', globals())
def manage_addXSLTMethod(self, id, title='', description='',
                         selected_processor='', xslTransformerId='',
                         content_type="text/html", behave_like='',
                         caching='on', debugLevel=0,
                         REQUEST=None, RESPONSE=None):
    """
    Factory method to create an instance of XSLTMethod, called from
    a GUI in the Zope Management Interface.  It calls addXSLTMethod
    to actually do the work.
    """
    try:
        self.addXSLTMethod(id, title, description, selected_processor,
                           xslTransformerId, content_type, behave_like,
                           caching, debugLevel)
        message = 'Successfully created ' + id + ' XSLTMethod object.'

        if REQUEST is None:
            return

        # support Add and Edit
        try:
            url = self.DestinationURL()
        except:
            url = REQUEST['URL1']
        if REQUEST['submit'] == " Add and Edit ":
            url = "%s/%s" % (url, id)
            REQUEST.RESPONSE.redirect(url + '/manage_editForm')
        else:
            REQUEST.RESPONSE.redirect(url + "/manage_main")
        
    except Exception, e:
        message = str(e)
        message.replace('\n','<br/>')
        return MessageDialog(title   = 'Error',
                             message = message,
                             action  = 'manage_main')

def addXSLTMethod(self, id, title='', description='',
                  selected_processor='', xslTransformerId='',
                  content_type="text/html", behave_like='',
                  caching='on',debugLevel=0):
    """
    Factory method to actually create an instance of XSLTMethod and
    return it.

    You should call this method directly if you are creating an
    instance of XSLTMethod programatically.
    """

    if not id or not xslTransformerId:
        raise Exception('Required fields must not be blank')

    tran = self.restrictedTraverse(xslTransformerId, None)
    if tran is None:
        message = "Invalid transformer name. %s not found" % (xslTransformerId)
        raise Exception(message)

    self._setObject(id, XSLTMethod(id, title, description,
                                   selected_processor, xslTransformerId,
                                   content_type, behave_like,
                                   caching, debugLevel))
    self._getOb(id).reindex_object()

def availableProcessors(self):
        """
        Return names of currently available XSLT processor libraries
        """
        return ProcessorRegistry.names(IXSLTProcessor)


################################################################
# Main class
################################################################

class XSLTMethod(XMLMethod):
    """
    Automatically transforms an XML document via XSLT, where both the
    XML document and XSLT are obtained from other Zope objects via
    acquisition.
    """

    meta_type = 'XSLT Method'

    __implements__ = IXSLTMethod

    _security = ClassSecurityInfo()

    _properties = XMLMethod._properties + (
        {'id':'caching',            'type':'selection', 'mode': 'w',
         'select_variable':"onOff" },
        )
    
    manage_options = (
        {'label': 'Edit', 'action': 'manage_editForm', 'help': ('ZopeXMLMethods','edit.stx') },) + \
        XMLMethod.manage_options

    def __init__(self, id, title, description, selected_processor,
                 xslTransformerId, content_type="text/html", behave_like="",
                 caching='on', debugLevel=0):

        XMLMethod.__init__(self, id, title, description,
                           selected_processor, content_type,
                           behave_like, debugLevel)

        # string attributes
        self.xslTransformerId   = xslTransformerId
        self.caching            = caching

        # if selected_processor is blank, get the default
        if not self.selected_processor:
            self.selected_processor = ProcessorRegistry.defaultName(IXSLTProcessor)

    ################################################################
    # ZMI methods
    ################################################################

    _security.declareProtected(PERM_MANAGE, 'manage_editForm')
    manage_editForm = PageTemplateFile('www/edit.pt',globals())

    _security.declareProtected(PERM_EDIT, 'manage_edit')
    def manage_edit(self, xslTransformerId, behave_like,
                     REQUEST=None, RESPONSE=None):
        """
        Edit XSLTMethod settings
        """
        try:
            self.editTransform(xslTransformerId)
        except Exception, e:
            return MessageDialog(title   = 'Error',
                                 message = str(e),
                                 action  = 'manage_editForm')

        self.behave_like = behave_like

        message = 'Changes saved.'
        return self.manage_editForm(manage_tabs_message=message)

    ################################################################
    # Methods implementing the IXSLTMethod interface below
    ################################################################

    _security.declareProtected(PERM_VIEW,'isCacheFileUpToDate')
    def isCacheFileUpToDate(self, REQUEST):
        """
        Return 1 if and only if self should use the cached value
        rather than regenerating the transformed value, and the cache
        manager exists, and the cache file exists.  Note: this
        algorithm is rather simple and limited right now, but we
        intend to improve it over time.  Many items are not taken into
        account today: a) XML fragments that are included into the
        main document via the XSLT document() function b) XSLT
        transformer parameters c) XSLT transformers themselves (as
        well as all included and imported transformers).  FIXME cks
        11/26/2001
        """
        manager = self.findCacheManager()
        if manager is None:
            return 0
        
        srcObject   = self.getXmlSourceObject()
        xformObject = self.getXslTransformer()
        
        # ZODB mod time.  This accounts for properties of the
        # XSLTMethod Zope object itself, and its content, unless
        # its content is stored in an external file.
        # NOTE: this value is returned as number of seconds
        # since the epoch in UTC (see python time module)
        #
        # Unfortunately, the Python time module returns number of
        # seconds, truncated.  bobobase returns microseconds.
        # Therefore, we must truncate by subtracting 0.5 and rounding.
        sourceTime = round(srcObject.bobobase_modification_time().timeTime() - 0.5)
        xformTime  = round(xformObject.bobobase_modification_time().timeTime() - 0.5)
        cacheTime  = manager.cacheFileTimeStamp(REQUEST.get("URL"))

        return ((cacheTime >= sourceTime) and (cacheTime >= xformTime))

    _security.declareProtected(PERM_VIEW, 'findCacheManager')
    findCacheManager = findCacheManager

    _security.declarePublic('availableProcessors')
    def availableProcessors(self):
        """
        Return names of currently available XSLT processor libraries
        """
        return ProcessorRegistry.names(IXSLTProcessor)

    _security.declarePublic('processor')
    def processor(self):
        """
        Obtain the object encapsulating the selected XSLT processor.
        """
        return ProcessorRegistry.item(IXSLTProcessor, self.selected_processor)

    _security.declarePublic('xslTransformer')
    def getXslTransformer(self):
        """
        Obtain the Zope object holding the XSLT, or None if the name
        does not point to a valid object.
        """
        return self.restrictedTraverse(self.xslTransformerId, None)

    _security.declareProtected(PERM_VIEW, 'transform')
    def transform(self, REQUEST, *args, **kwargs):
        """
        Generate result using transformer and return it as a string
        """
        processor   = self.processor()
        xslObject   = self.getXslTransformer()
        xslContents = getPublishedResult("XSL transformer", xslObject, REQUEST)
        xslURL      = xslObject.absolute_url()
        kw = {}
        if kwargs:
            kw = kwargs.copy()
        xslParams   = self.getXSLParameters(**kw)
        xmlObject   = self.getXmlSourceObject()
        xmlContents = getPublishedResult("XML source", xmlObject, REQUEST)
        xmlURL      = xmlObject.absolute_url()
        
        processor.setDebugLevel(self.debugLevel)

        return processor.transform(xmlContents,
                                   xmlURL,
                                   xslContents,
                                   xslURL,
                                   self,
                                   xslParams,
                                   REQUEST)

    _security.declareProtected(PERM_EDIT, 'setCachingOn')
    def setCachingOn(self, REQUEST=None):
        """
        same as changing the property.  For use in scripts or by the cache Manager
        """
        self.caching = 'on'

    _security.declareProtected(PERM_EDIT, 'setCachingOff')
    def setCachingOff(self, REQUEST=None):
        """
        same as changing the property.  For use in scripts or by the cache Manager
        """
        self.caching = 'off'

    # innocuous methods should be declared public
    _security.declarePublic('isCachingOn')
    def isCachingOn(self):
        """
        Return true if caching is turned on
        """
        return self.caching == 'on'

    ################################################################
    # Methods called from ZMI
    ################################################################

    _security.declareProtected(PERM_EDIT, 'editTransform')
    def editTransform(self, xslTransformerId):
        """
        Change transformer to be used or ID of source object
        """
        # does the xslTransformerId point to a valid transformer?
        tran = self.restrictedTraverse(xslTransformerId, None)
        if tran is None:
            message = "Invalid xslTransformerId %s" % (xslTransformerId)
            raise Exception(message)
        
        self.xslTransformerId = xslTransformerId

    ################################################################
    # Utilities
    ################################################################

    _security.declarePublic('onOff')
    def onOff(self):
        return ["on", "off"]

    _security.declarePrivate('getXSLParameters')
    def getXSLParameters(self, *args, **kwargs):
        """

        Return XSL Transformer parameters as a dictionary of strings
        in the form 'name:value' as would be passed to an XSLT engine
        like Saxon, 4suite, etc. The values are obtained by looking
        for a property in the current context called 'XSLparameters',
        which should be a list of strings. Each name on the list is
        looked up in the current context. If its value is a scalar,
        then the pair 'name:value' is returned. If the value is an
        object, then the pair 'name:url' is returned where url is the
        absolute URL of the object.  The key (name) is actually a
        tuple of two strings, the first of which is an optional
        namespace (we don't use this today).

        """
        parms  = aq_get(self,parametersPropertyName,None)
        result = {}
        if parms is not None:
            for p in parms:
                value = aq_get(self,p,None)
                # I use callable() to determine if it is not a scalar.
                # If not, it must be a Zope object (I think) - WGM

                if callable(value):
                    val = value.absolute_url()
                else:
                    val = str(value)

                # Allow request variables to override parameters
                val = self.REQUEST.get(p, val)
                result[p] = val

                # Finally, kwargs override all others if provided
                for arg in kwargs.keys():
                    result[arg] = kwargs[arg]

        return result


    ################################################################
    # Standard Zope stuff
    ################################################################

    _security.declareProtected(PERM_VIEW, '__call__')
    def __call__(self, client=None, REQUEST=None, RESPONSE=None,
                 *args, **kwargs):
        """
        Render self by processing its content with the named XSLT stylesheet
        """
        rawResult = None
        # Check for caching
        if self.isCachingOn():
            if self.isCacheFileUpToDate(REQUEST):
                manager = self.findCacheManager()
                if manager is not None:
                    rawResult = manager.valueFromCache(REQUEST.get("URL"))

        if rawResult is None:
            kw = {}
            if kwargs:
                kw = kwargs.copy()
            rawResult = self.transform(REQUEST, **kw)
            manager = self.findCacheManager()
            if self.isCachingOn() and manager is not None:
                manager.saveToCache(REQUEST.get("URL"), rawResult)
            
        if self.behave_like == "":
            behave_like = self.getXmlSourceObject().meta_type
        else:
            behave_like = self.behave_like

        gen = GeneratorRegistry.getGenerator(behave_like)
        if gen is None:
            gen = GeneratorRegistry.getDefaultGenerator()

        # explicitly set the Content-Type here because calling the XML
        # source object or the XSL transformer object might have
        # changed it and the call of gen.getResult() below doesn't
        # guarantee that it will be changed back.
        if RESPONSE is not None:
            RESPONSE.setHeader("Content-Type", self.content_type)

        obj = gen.createObject(self.id, self.title, rawResult,
                               content_type=self.content_type)
        
        if client is None:
            client = self

        return gen.getResult(obj, client, REQUEST, RESPONSE)

    ################################################################
    # Support for Schema Migration
    ################################################################

    def repair(self):
        """
        Repair this object.  This method is used for schema migration,
        when the class definition changes and existing instances of
        previous versions must be updated.
        """
        if self.hasattr('_processorChooser'):
            del self._processorChooser

# register security information
Globals.InitializeClass(XSLTMethod)
