"""
$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.55 2003/03/30 03:45:47 cstrong Exp $
"""

__cvstag__  = '$Name:  $'[6:-2]
__date__    = '$Date: 2003/03/30 03:45:47 $'[6:-2]
__version__ = '$Revision: 1.55 $'[10:-2]

# python
import mimetypes

# base classes
from OFS.SimpleItem import SimpleItem
from Products.ZCatalog.CatalogAwareness import CatalogAware
from OFS.PropertyManager import PropertyManager

# peer classes/modules
from IXSLTMethod import IXSLTMethod
from ProcessorChooser import ProcessorChooser
from GeneratorRegistry import GeneratorRegistry

# Zope builtins
import OFS
import Globals
from Globals import MessageDialog
from Acquisition import aq_base, aq_parent
from AccessControl import ClassSecurityInfo
from ZPublisher.mapply import mapply
from Products.PageTemplates.PageTemplateFile import PageTemplateFile

################################################################
# Permissions
################################################################
# By defining these constants, no new permissions will be created when
# misspelling them in the declareProtected() call
PERM_VIEW    = "View"
PERM_EDIT    = "Edit"
PERM_FTP     = "FTP access"
PERM_CONTENT = "Access contents information"
PERM_MANAGE  = "Manage XML Methods"

################################################################
# Utilties
################################################################

def getPublishedResult(name, obj, REQUEST):
    """
    Get the result of an object as if it were published
    """
    #
    # index_html is generally used for human-readable HTML, whereas
    # __call__ would normally be used to obtain access to the
    # underlying XML source.  However, some Zope products do not adhere
    # to this convention, so we call index_html if __call__ is not
    # supported.
    #
    if hasattr(aq_base(obj), "__call__") and obj.__call__ is not None:
        return obj(obj, REQUEST)
    elif hasattr(obj, "index_html") and obj.index_html is not None:
        return mapply(obj.index_html, (), REQUEST)
    elif callable(obj):
        return apply(obj)
    else:
        message = "Error, unable to obtain source from object %s" % (name)
        raise Exception(message)

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()


################################################################
# Main class
################################################################

class XSLTMethod(CatalogAware,    # ZCatalog support
                 PropertyManager, # Property support
                 SimpleItem):
    """
    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 = (
        {'id':'title',              'type':'string',    'mode': 'w' },
        {'id':'description',        'type':'text',      'mode': 'w' },
        {'id':'content_type',       'type':'string',    'mode': 'w' },
        {'id':'selected_processor', 'type':'selection', 'mode': 'w',
         'select_variable':'availableProcessors' },
        {'id':'debugLevel',         'type':'int',       'mode': 'w' },
        {'id':'caching',            'type':'selection', 'mode': 'w',
         'select_variable':"onOff" },
       )
    
    manage_options = (
        {'label': 'Edit', 'action': 'manage_editForm', 'help': ('ZopeXMLMethods','edit.stx') },) + \
        OFS.PropertyManager.PropertyManager.manage_options + \
        OFS.SimpleItem.SimpleItem.manage_options

    def __init__(self, id, title, description, selected_processor,
                 xslTransformerId, content_type="text/html", behave_like="",
                 caching='on', debugLevel=0):
        # string attributes
        self.id                 = id
        self.title              = title
        self.description        = description
        self.selected_processor = selected_processor
        self.xslTransformerId   = xslTransformerId
        self.content_type       = content_type
        self.behave_like        = behave_like
        self.caching            = caching
        self.debugLevel         = debugLevel

        # processor chooser
        self._processorChooser  = ProcessorChooser(preferred = selected_processor)
        self.selected_processor = self._processorChooser.defaultProcessor()
        if self.selected_processor is None:
            raise Exception('No supported XSLT processors available')

    ################################################################
    # 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)

    _security.declareProtected(PERM_EDIT, 'manage_editProperties')
    def manage_editProperties(self, REQUEST):
        """
        Cover for PropertyManager.manage_editProperties() method.  set
        debugLevel of underlying XSLTProcessor to our debugLevel, then
        pass it on to the inherited method for further processing
        """
        for prop in self._propertyMap():
            name = prop['id']
            if name == 'debugLevel':
                value = REQUEST.get(name, '')
                self.setDebugLevel(value)
        return XSLTMethod.inheritedAttribute("manage_editProperties")(self, REQUEST)

    ################################################################
    # 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
        """
        proc_chooser = ProcessorChooser()
        return proc_chooser.processors()

    _security.declarePublic('processor')
    def processor(self):
        """
        Obtain the object encapsulating the selected XSLT processor.
        """
        return self._processorChooser.processorObject(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, 'getXmlSourceObject')
    def getXmlSourceObject(self):
        """
        Retrieve the source object by using acquisition on the ID
        """
        # Our immediate parent might be a folderish object.  Keep
        # going up until we get to the first non folderish object

        ob = aq_parent(self)
        while ob.isPrincipiaFolderish:
            ob = aq_parent(ob)
        if self.isDebugging():
            print "Requesting contents of", ob.getId()
        return ob

    _security.declareProtected(PERM_VIEW, 'transform')
    def transform(self, REQUEST):
        """
        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()
        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, REQUEST)

    _security.declareProtected(PERM_VIEW, 'testTransform')
    def testTransform(self, REQUEST):
        """
        This version is used purely for testing.  It is the same as
        transform() but it throws exceptions.  This is used for
        negative tests.  See tests/TestXSLTMethod()
        """
        processor      = self.processor()
        xslObject      = self.getXslTransformer()
        xslContents    = getPublishedResult("XSL transformer", xslObject, REQUEST)
        xslURL         = xslObject.absolute_url()
        xmlObject      = self.getXmlSourceObject()
        xmlContents    = getPublishedResult("XML source", xmlObject, REQUEST)
        xmlURL         = xmlObject.absolute_url()
        topLevelParams = processor.getXSLParameters(self)

        processor.setDebugLevel(self.debugLevel)        

        return processor.transformGuts(xmlContents,
                                       xmlURL,
                                       xslContents,
                                       xslURL,
                                       self,
                                       topLevelParams,
                                       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'

    # innocuous methods should be declared public
    _security.declarePublic('setDebugLevel')
    def setDebugLevel(self, value):
        """
        Set debug level for ourselves and our underlying processor
        """
        self.debugLevel = value

    # innocuous methods should be declared public
    _security.declarePublic('isDebugging')
    def isDebugging(self):
        """
        Return true if and only if debugging is on.
        """
        return self.debugLevel > 0

    ################################################################
    # 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('getSelf')
    def getSelf(self):
        """
        Return this object. For use in DTML scripts
        """
        return self.aq_chain[0]

    _security.declarePublic('onOff')
    def onOff(self):
        return ["on", "off"]

    _security.declarePublic('behaveLikeList')
    def behaveLikeList(self):
        """
        Return list of standard zope objects this XSLTMethod can behave like
        """
        return GeneratorRegistry.supportedMetaTypes()

    ################################################################
    # Standard Zope stuff
    ################################################################

    # next line is not strictly necessary, Access contents info is Anonymous by default
    # as opposed to all other protections that get Manager Role by default
    _security.setPermissionDefault(PERM_CONTENT, ('Anonymous')) 

    _security.declareProtected(PERM_VIEW, 'index_html')
    # next line is not strictly necessary, Access contents info is Anonymous by default
    # as opposed to all other protections that get Manager Role by default
    _security.setPermissionDefault(PERM_VIEW, ('Anonymous')) 
    def index_html(self, REQUEST = None, RESPONSE = None):
        """
        Default view of rendered version of XML Document.  This is
        called when some one types the transformation path
        (e.g. "aSource/aXSLTMethod") directly into the browser, rather
        than via DTML or a page template.  We *must* use this method
        to pass the REQUEST parameter on, otherwise we wouldn't get it
        because _render_with_namespace_ is *not* called in this case.
        """
        return self(self, REQUEST, RESPONSE)
    
    _security.declareProtected(PERM_VIEW, '__call__')
    def __call__(self, client=None, REQUEST=None, RESPONSE=None):
        """
        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:
            rawResult = self.transform(REQUEST)
            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)

    #
    # isDocTemp tells Zope that we are like a Document Template, which
    # means that __call__ will get called with a REQUEST parameter
    # (which is what we want, since we grab the URL from REQUEST)
    #
    # CKS 3/22/2003 isDocTemp is no longer needed as of Zope 2.6.1,
    # but it might be needed by older releases, so might as well
    # keep it around...
    #
    isDocTemp = 1

    _security.declareProtected(PERM_VIEW, '__render_with_namespace__')
    def __render_with_namespace__(self, namespace):
        """
        Render with namespace namespace will be given to us by the ZPT
        that calls us, for example if a ZPT were to include something
        like the below:

        <div tal:replace="here/aSource/aXSLTMethod">replaceme</div>
        """
        REQUEST  = namespace["REQUEST"]
        RESPONSE = namespace["RESPONSE"]
        return self.__call__(REQUEST=REQUEST, RESPONSE=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.
        """
        # Nothing to repair at the moment


# register security information
Globals.InitializeClass(XSLTMethod)
