"""
$RCSfile: CacheManager.py,v $

ZopeXMLMethods provides filters to apply to Zope objects for XML/XSLT
processing.  XSLTMethod associates XSLT transformers with XML
documents.  ZopeXMLMethods includes XML Method Cache Manager that is
specialized to notice changes to the XML source files and to store
cached contents in files in the filesystem, rather than the Zope
object database.

Author: Craeg Strong <cstrong@arielpartners.com>
Modified by Philipp von Weitershausen <philikon@philikon.de>

$Id: CacheManager.py,v 1.2 2003/04/29 09:22:07 philikon Exp $
"""

import os, re, sys, urlparse

# base classes
from OFS.SimpleItem import SimpleItem
from Products.ZCatalog.CatalogAwareness import CatalogAware
from OFS.PropertyManager import PropertyManager
from zope.interface import implements


# peer classes
from interfaces import ICacheManager
import XSLTMethod # the module

# Zope builtins
import OFS
import Globals
import Acquisition
from Globals import MessageDialog, HTML
from Acquisition import aq_chain, aq_parent
from AccessControl import ClassSecurityInfo
from Products.PageTemplates.PageTemplateFile import PageTemplateFile

# Permission strings
# By defining these constants, no new permissions will be created when
# misspelling them in the declareProtected() call
PERM_EDIT   = "Edit"
PERM_MANAGE = "Manage XML Method Cache"

################################################################
# Defaults
################################################################

cachePrefix = 'ZopeXMLCache'

def defaultCachePrefix():
    spoolDirectory = "/tmp"
    if sys.platform == 'win32':
        spoolDirectory = r"c:\tmp" 
    return os.path.join(spoolDirectory, cachePrefix)

################################################################
# Module Scoped Convenience Methods
################################################################

def addInstance(folder, id, title='', description=''):
    """
    This is a convenience factory method for creating an instance of
    CacheManager.  It returns the object created, and may therefore be
    more convenient than the addCacheManager() method it calls.  It is
    used by the unit testing programs.
    """
    folder.manage_addProduct['ZopeXMLMethods'].addXMLMethodCacheManager(id,title,description)
    return folder[id]

################################################################
# Container Methods
################################################################

manage_addXMLMethodCacheManagerForm = PageTemplateFile('www/create_cachemgr.pt',
                                                       globals())

def manage_addXMLMethodCacheManager(self,
                                    id, title='', description='',
                                    REQUEST=None, RESPONSE=None):
    """
    Add an XML Method Cache Manager to a folder.  Called from the
    create_cachemgr.dtml GUI form in the Zope Management interface.
    It calls addXMLMethodCacheManager to actually do the work.
    """
    try:
        self.addXMLMethodCacheManager(id, title, description)
        RESPONSE.redirect(self.absolute_url() + "/manage_main")

    except Exception, e:
        message = str(e)
        mesage.replace('\n','<br/>')
        return MessageDialog(title   = 'Error',
                             message = message,
                             action  = 'manage_main')

def addXMLMethodCacheManager(self, id, title='', description=''):
    """
    Add an XML Method Cache Manager to a folder.  You should call
    this method directly if you are creating an instance of
    CacheManager programmatically.
    """
    if not id:
        raise Exception('Required fields must not be blank')

    self._setObject(id, CacheManager(id, title, description))

################################################################
# CacheManager class
################################################################

class CacheManager(CatalogAware,    # ZCatalog support
                   PropertyManager, # Property support
                   SimpleItem):
    """
    CacheManager caches the results of performing transformations with
    filters.  It does not inherit from OFS/CacheManager because it
    has so little in common with it.  @@ revisit this later CKS 3/2/2003
    """

    meta_type = 'XML Method Cache Manager'

    implements(ICacheManager)

    _properties = (
        {'id':'title',           'type':'string',    'mode': 'w'},
        {'id':'description',     'type':'string',    'mode': 'w'},
        {'id':'cachePrefix',     'type':'string',    'mode': 'w'},        
       )

    manage_options = (
        { 'label':'Cache', 'action':'manage_cacheForm', 'help':('ZopeXMLMethods', 'cache.stx') },
       ) + OFS.PropertyManager.PropertyManager.manage_options \
         + OFS.SimpleItem.SimpleItem.manage_options

    _security = ClassSecurityInfo()

    _security.declareProtected(PERM_MANAGE, 'manage_cacheForm')
    manage_cacheForm = PageTemplateFile('www/edit_cachemgr.pt', globals())

    def __init__(self, id, title, description):
        """
        Initialize a new instance of CacheManager
        """
        self.id             = id
        self.title          = title
        self.description    = description
        self.cachePrefix    = defaultCachePrefix()
        self.cacheFiles     = {}

    ################################################################
    # Methods called from ZMI pages
    ################################################################

    _security.declareProtected(PERM_EDIT, 'manage_setCachingOn')
    def manage_setCachingOn(self, REQUEST):
        """
        ZMI method: turn caching on for all XML Method objects.
        """
        message = self.batchSetCachingOn(REQUEST)
        message.replace('\n','<br/>')        
        return self.manage_cacheForm(batchOperationOutput = message)

    _security.declareProtected(PERM_EDIT, 'manage_setCachingOff')
    def manage_setCachingOff(self, REQUEST):
        """
        ZMI method: turn caching off for all XML Method objects.
        """
        message = self.batchSetCachingOff(REQUEST)
        message.replace('\n','<br/>')
        return self.manage_cacheForm(batchOperationOutput = message)

    _security.declareProtected(PERM_EDIT, 'manage_clearCache')
    def manage_clearCache(self):
        """
        ZMI method: clear cache
        """
        message = self.clearCache()
        message.replace('\n','<br/>')
        return self.manage_cacheForm(batchOperationOutput = message)

    _security.declareProtected(PERM_EDIT, 'manage_listCacheFiles')
    def manage_listCacheFiles(self):
        """
        ZMI method: list cache files
        """
        message = "Cache Files:\n\n"
        list = self.listCacheFiles()

        if list:
            for file in list:
                message = message + file + '\n'
        else:
            message = 'No files in cache'
                
        message.replace('\n','<br/>')
        return self.manage_cacheForm(batchOperationOutput = message)

    _security.declareProtected(PERM_EDIT, 'manage_editProperties')
    def manage_editProperties(self, REQUEST):
        """
        Cover for PropertyManager.manage_editProperties() method.
        First validate cachePrefix, then pass it on to the inherited
        method for further processing
        """
        for prop in self._propertyMap():
            name = prop['id']
            if name == 'cachePrefix':
                value = REQUEST.get(name, '')
                if not self.isValidCachePrefix(value):
                    return MessageDialog(title   = 'ERROR',
                                         message = value + ' is not a valid Cache Prefix',
                                         action  = 'manage_propertiesForm')
        return CacheManager.inheritedAttribute("manage_editProperties")(self, REQUEST)

    ################################################################
    # Methods implementing the ICacheManager interface
    ################################################################
    
    _security.declareProtected(PERM_EDIT, 'batchSetCachingOn')
    def batchSetCachingOn(self, REQUEST):
        """
        Turns caching on for all instances of all types of XML Methods
        within the scope of this cache manager.
        """
        parent = self.getParentFolder()
        message = "Turning Caching On for all XML filter instances under " + \
                  parent.getId() + \
                  " Folder:\n"
        message = message + \
                  self.batchExecute(parent, 'setCachingOn', REQUEST)
        return message

    _security.declareProtected(PERM_EDIT, 'batchSetCachingOff')
    def batchSetCachingOff(self, REQUEST):
        """
        Turns caching off for all instances of all types of XML
        Methods within the scope of this cache manager.
        """
        parent = self.getParentFolder()
        message = "Turning Caching Off for all XML filter instances under " + \
                  parent.getId() + \
                  " Folder:\n"
        message = message + \
                  self.batchExecute(parent, 'setCachingOff', REQUEST)
        return message

    _security.declareProtected(PERM_EDIT, 'cacheFileTimeStamp')
    def cacheFileTimeStamp(self, url):
        """
        Return the last modified time of the cache file for the passed in
        filter client object, or 0 if the cache file does not exist
        """
        fileName  = self.cacheFileName(url)
        cachetime = 0

        if os.path.exists(fileName):
            return os.path.getmtime(fileName)
        else:
            return 0

    _security.declareProtected(PERM_EDIT, 'valueFromCache')
    def valueFromCache(self, url):
        """
        Retrieve the output from the cache for the passed in
        filter client object, or None if the cache file does not
        exist.
        """
        fileName = self.cacheFileName(url)
        cache = open(fileName, 'rb')
        result = cache.read()
        cache.close()
        return result

    _security.declareProtected(PERM_EDIT, 'saveToCache')
    def saveToCache(self, url, contents):
        """
        Save the contents to the cache for the passed in filter
        client object.  It will overwrite the previous contents of the
        cache file, or create a new cache file if it does not yet
        exist.
        """
        fileName = self.cacheFileName(url)
        cache = open(fileName, 'wb')
        cache.write(contents)
        cache.close()
        self.cacheFiles[url] = fileName
        self._p_changed = 1

    _security.declareProtected(PERM_EDIT, 'clearCache')
    def clearCache(self):
        """
        Clear the cache.  This involves removing every cache file from
        the file system.
        """
        message = ""
        for fileName in self.cacheFiles.values():
            try:
                os.remove(fileName)
                message = message + fileName + " successfully removed\n"
            except Exception, value:
                message = message + "%s not removed, error: %s\n" % (fileName, value)
        self.cacheFiles.clear()
        return message or "No files in cache"

    _security.declareProtected(PERM_EDIT, 'listCacheFiles')
    def listCacheFiles(self):
        """
        Return the list of files in the cache
        """
        return self.cacheFiles.values()

    ################################################################
    # Utilities
    ################################################################

    def isValidCachePrefix(self, value):
        """
        Return 1 if this represents a valid value for a cache prefix
        """
        dirname = os.path.dirname(value)
        # We would like to check write permission in the directory
        # also, but this is difficult to do portably... TBD
        return os.path.exists(dirname)
    
    def cacheFileName(self, url):
        """
        Algorithmically determine the leaf name of the file in which
        to store the transformed contents of the passed in XML filter
        client object.
        """
        (addressingScheme, networkLocation, path, params, query, fragment) = \
          urlparse.urlparse(url)
        return os.path.normpath(self.cachePrefix + re.sub('[/\\\]','_',path))

    def getParentFolder(self):
        """
        Returns the folder containing this instance of CacheManager
        """
        return aq_parent(self)

    def batchExecute(self, context, methodName, REQUEST):
        """
        Recursively execute passed-in method on all objects of type
        XSLTMethod within the passed-in context (ie Zope folder).
        Returns a list of the object IDs of the affected objects.
        """
        metaType = 'XSLT Method'  # @@ expand to more types later CKS 3/23/2003
        objects = context.ZopeFind(context, obj_metatypes=[metaType],
                                   search_sub=1)
        func = XSLTMethod.XSLTMethod.__dict__[methodName]
        message = "\n\n"

        for doc in objects:
            try:
                message = message + "\nFound XSLTMethod " + \
                          doc[0]
                func(doc[1], REQUEST)
                message = message + "...success"
            except Exception, value:
                mesage = message + "...error: %s" % (value)
                
        return message

    # innocuous methods should be declared public
    _security.declarePublic('getSelf')
    def getSelf(self):
        "Return this object. For use in DTML scripts"
        return self.aq_chain[0]

    ################################################################
    # 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(CacheManager)
