# -*- coding: UTF-8 -*-


# Expression
# By: Frederic Peters <fpeters@entrouvert.com>
#     Emmanuel Raviart <eraviart@entrouvert.com>
#
# Copyright (C) 2004 Entr'ouvert, Frederic Peters & Emmanuel Raviart
#
# This program is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 2
# of the License, or (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.


"""Data Holders Module."""


import errno
import os
import time

import libxml2

import documents
import environs
import faults
import filesystems
import locations
import logs
import modules
import namespaces
import stations
import xpaths


holderClassesByMimeType = {}
mimeTypes = {
    ".css": "text/css",
    ".csv": "text/x-csv",
    ".dbxml": "application/x-dbxml",
    ".der": "application/x-x509-ca-cert",
    ".foaf": "application/rdf+xml",
    ".gif": "image/gif",
    ".htc": "text/x-component",
    ".htm": "text/html",
    ".html": "text/html",
    ".jpeg": "image/jpeg",
    ".jpg": "image/jpeg",
    ".js": "text/javascript",
    ".pdf": "application/pdf",
    ".pem": "application/x-x509-ca-cert",
    ".png": "image/png",
    ".py": "text/x-python-src",
    ".rst": "x-text/restructured",
    ".stc": "application/vnd.sun.xml.calc.template",
    ".std": "application/vnd.sun.xml.draw.template",
    ".sti": "application/vnd.sun.xml.impress.template",
    ".stw": "application/vnd.sun.xml.writer.template",
    ".sxc": "application/vnd.sun.xml.calc",
    ".sxd": "application/vnd.sun.xml.draw",
    ".sxg": "application/vnd.sun.xml.writer.global",
    ".sxi": "application/vnd.sun.xml.impress",
    ".sxm": "application/vnd.sun.xml.math",
    ".sxw": "application/vnd.sun.xml.writer",
    ".txt": "text/plain",
    ".xhtml": "application/xhtml+xml",
    ".xml": "text/xml",
    ".xsd": "text/xml",
    ".xsl": "text/xml",
    ".zip": "application/zip",
    }


class DataHolder(stations.AbstractStation):
    _configDoc = None
    _containedFileSystem = None
    _data = None
    _dataHolderInited = False
    _metadata = None
    _xslt = None
    defaultFileNameExtension = None
    mimeType = None
    mimeTypes = None
    pathFragment = None

    def __del__(self):
        if self._configDoc not in (None, "none"):
            self._configDoc.freeDoc()
            del self._configDoc

    def __init__(self, pathFragment = None, previous = None, parent = None,
                 uriPathFragment = None, uriScheme = None, uriAuthority = None,
                 isRemote = False, remoteUri = None, mimeType = None, isRootElder = False,
                 containedFileSystem = None):
        if self._dataHolderInited:
            return
        self._dataHolderInited = True

        super(DataHolder, self).__init__(
            previous = previous, parent = parent, uriPathFragment = uriPathFragment,
            uriScheme = uriScheme, uriAuthority = uriAuthority, isRemote = isRemote,
            remoteUri = remoteUri)
        if pathFragment:
            # When self is a root elder, pathFragment must be an absolute path.
            self.pathFragment = pathFragment
        if mimeType is not None:
            self.mimeType = mimeType
        if isRootElder:
            self.isRootElder = True
        if containedFileSystem is not None:
            self._containedFileSystem = containedFileSystem

    def __new__(cls, *arguments, **keywords):
        self = stations.AbstractStation.__new__(cls, *arguments, **keywords)
        self.__init__(*arguments, **keywords)
        if self.mimeType is None:
            fileSystem = self.fileSystem
            fileSystemPath = self.getFileSystemPath()
            if fileSystem.exists(fileSystemPath) and fileSystem.isdir(fileSystemPath):
                self.mimeType = "inode/directory"
        if self.mimeType is None:
            logs.debug('Missing MIME type for "%s" using StaticDataHolder.' % self)
            realClass = StaticDataHolder
        else:
            logs.debug("MIME: %s" % str(self.mimeType))
            realClass = self.getConfigPythonClass(
                "yep:pythonClass", default = None, ignoreGeneralValue = True)
            if realClass is None:
                if self.mimeType in holderClassesByMimeType:
                    realClass = holderClassesByMimeType[self.mimeType]
                else:
                    logs.debug('Unknown MIME type "%s" using StaticDataHolder.' % self.mimeType)
                    realClass = StaticDataHolder
        if realClass != self.__class__ and issubclass(realClass, self.__class__):
            self.__class__ = realClass
        return self

    def doHttpDelete(self):
        content = environs.getVar("submission").readFile()
        if content is not None:
            logs.debug(content)
        fileSystemPath = self.getFileSystemPath()
        try:
            self.fileSystem.remove(fileSystemPath)
        except OSError, error:
            if error.errno != errno.EPERM:
                raise
            return environs.getVar("httpRequestHandler").outputErrorAccessForbidden(
                self.getUriAbsolutePath())
        environs.getVar("httpRequestHandler").outputSuccessNoContent()

    def doHttpDelete1(self, multistatus):
        fileSystemPath = self.getFileSystemPath()
        try:
            self.fileSystem.remove(fileSystemPath)
        except OSError, error:
            if error.errno != errno.EPERM:
                raise
            multistatusNode = multistatus.node
            responseNode = multistatusNode.newTextChild(None, "response", None)
            # The href URI is quoted, because Nautilus requires that the returned href is the
            # same as the URL in the PROPFIND command. Otherwise it can not open "untitled
            # folder", because it expects "untitled%20folder".
            responseNode.newTextChild(None, "href", self.getAbsoluteUri(quote = True))
            responseNode.newTextChild(None, "status", "HTTP/1.1 403 Access Forbidden")
            return False
        return True

    def doHttpPut(self):
        # FIXME: To complete.
        # See RFC 2616 <http://www.ietf.org/rfc/rfc2616.txt>
        # and the full thread at
        # <http://www.mail-archive.com/www-talk@w3.org/msg00699.html>
        # to know how to handle HTTP PUT correctly.

        httpRequestHandler = environs.getVar("httpRequestHandler")
        # FIXME: We should check whether the save can be done before sending
        # a "continue" response.
        expect = httpRequestHandler.headers.get("Expect", "")
        if expect.lower() == "100-continue":
            httpRequestHandler.outputInformationContinue()
        submission = environs.getVar("submission")
        isCreated = self.saveDataFile(submission.file, submission.length)
        if isCreated:
            httpRequestHandler.outputSuccessCreated(self.getUriAbsolutePath())
        else:
            httpRequestHandler.outputSuccessNoContent()

    def getAbsolutePath(self):
        parent = self.getParent()
        if parent is None:
            absolutePath = "/"
        else:
            absolutePath = parent.getAbsolutePath()
        if self.pathFragment:
            absolutePath = os.path.normpath(os.path.join(absolutePath, self.pathFragment))
        return absolutePath

    def getAllConfigElementNodesAndOwnerCouples(self, xpath):
        relevantNodesAndOwnerCouples = super(
            DataHolder, self).getAllConfigElementNodesAndOwnerCouples(xpath)
        configDoc = self.getConfigDoc()
        elementNodes = xpaths.evaluateXpath("yep:element", contextNode = configDoc)
        for elementNode in elementNodes:
            nodes = xpaths.evaluateXpath(xpath, contextNode = elementNode)
            if not nodes:
                continue
            nameNodes = xpaths.evaluateXpath("@name", contextNode = elementNode)
            if not nameNodes:
                continue
            name = nameNodes[0].content
            namespaceUriNodes = xpaths.evaluateXpath("@namespace", contextNode = elementNode)
            if not namespaceUriNodes:
                continue
            namespaceUri = namespaceUriNodes[0].content
            key = "%s / %s" % (namespaceUri, name)
            relevantNodesAndOwnerCouples[key] = (nodes, self)
        return relevantNodesAndOwnerCouples

    def getConfigDataHolderNodesAndOwner(self, mimeType, xpath, ignoreGeneralValue = False):
        configDoc = self.getConfigDoc()
        dataHolderNodes = xpaths.evaluateXpath(
            "yep:inode[@mimeType = '%s']" %  mimeType.replace("'", "&apos;"),
            contextNode = configDoc)
        if dataHolderNodes:
            dataHolderNode = dataHolderNodes[0]
            nodes = xpaths.evaluateXpath(xpath, contextNode = dataHolderNode)
            if nodes:
                return nodes, self
        if not ignoreGeneralValue:
            nodes = xpaths.evaluateXpath(xpath, contextNode = configDoc)
            if nodes:
                return nodes, self
        return super(DataHolder, self).getConfigDataHolderNodesAndOwner(
            mimeType, xpath, ignoreGeneralValue = ignoreGeneralValue)

    def getConfigDoc(self):
        if self._configDoc is None:
            fileSystem = self.fileSystem
            fileSystemPath = self.getFileSystemPath()
            if fileSystem.exists(fileSystemPath) and fileSystem.isdir(fileSystemPath):
                fileSystemConfigPath = os.path.join(fileSystemPath, "_config.xml")
            else:
                fileSystemConfigPath = fileSystemPath + ".config.xml"
            absoluteConfigPath = fileSystem.getAbsolutePath(fileSystemConfigPath)
            try:
                configFile = fileSystem.open(fileSystemConfigPath, "rb")
            except IOError, error:
                if error.errno in (errno.ENOENT, errno.ENOTDIR):
                    # No such file or directory
                    configDoc = "none"
                elif error.errno == errno.EACCES:
                    # Permission denied
                    logs.info("Can't access configuration file %s: permission denied."
                              % fileSystemConfigPath)
                    configDoc = "none"
                else:
                    raise
            else:
                try:
                    try:
                        # FIXME: What to use for URL instead of None (second argument)?
                        if hasattr(configFile, "fileno"):
                            configDoc = libxml2.readFd(
                                configFile.fileno(), absoluteConfigPath, None,
                                libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
                        else:
                            # For zip files, ...
                            configDoc = libxml2.readDoc(
                                configFile.read(), absoluteConfigPath, None,
                                libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
                    except (libxml2.parserError, libxml2.treeError):
                        logs.info('[getConfigDog] Error parsing XML file at "%s".' %
                            fileSystemConfigPath)
                        raise
                finally:
                    configFile.close()
            self._configDoc = configDoc
        if self._configDoc == "none":
            return None
        return self._configDoc

    def getConfigElementNodesAndOwner(self, namespaceUri, name, xpath, ignoreGeneralValue = False):
        if namespaceUri is None:
            return [], self
        configDoc = self.getConfigDoc()
        elementNodes = xpaths.evaluateXpath(
            "yep:element[@namespace = '%s' and @name = '%s']" % (
                namespaceUri.replace("'", "&apos;"), name.replace("'", "&apos;")),
            contextNode = configDoc)
        if elementNodes:
            elementNode = elementNodes[0]
            nodes = xpaths.evaluateXpath(xpath, contextNode = elementNode)
            if nodes:
                return nodes, self
        if not ignoreGeneralValue:
            nodes = xpaths.evaluateXpath(xpath, contextNode = configDoc)
            if nodes:
                return nodes, self
        return super(DataHolder, self).getConfigElementNodesAndOwner(
            namespaceUri, name, xpath, ignoreGeneralValue = ignoreGeneralValue)

    def getConfigNodesAndOwner(self, xpath, ignoreGeneralValue = False):
        if not self.mimeType:
            self.mimeType = 'application/octet-stream'
            logs.debug('Invalid MIME type for %s' % self)
        return self.getConfigDataHolderNodesAndOwner(
            self.mimeType, xpath, ignoreGeneralValue = ignoreGeneralValue)

    def getConfigStationNodesAndOwner(self, xpath):
        nodes = xpaths.evaluateXpath(xpath, contextNode = self.getConfigDoc())
        if nodes:
            return nodes, self
        return super(DataHolder, self).getConfigStationNodesAndOwner(xpath)

    def getContainedFileSystem(self):
        if self._containedFileSystem is None:
            return self.fileSystem
        else:
            return self._containedFileSystem

    def getContainedFileSystemPath(self):
        if self._containedFileSystem is not None:
            return ""
        return super(DataHolder, self).getContainedFileSystemPath()

    def getData(self):
        if self._data is None:
            self._data = self.loadData()
        if self._data == "none":
            return None
        return self._data

    def getDataFile(self):
        return self.fileSystem.open(self.getFileSystemPath(), "rb")

    def getDirectoryAbsolutePath(self):
        parent = self.getParent()
        if parent is None:
            absolutePath = "/"
        else:
            absolutePath = parent.getAbsolutePath()
        if self.pathFragment:
            absolutePath = os.path.dirname(os.path.normpath(os.path.join(
                absolutePath, self.pathFragment)))
        return absolutePath

    def getDocument(self):
        return self

    def getFileSystem(self):
        parent = self.getParent()
        if parent is None:
            return self._containedFileSystem
        parentHolder = parent.getDataHolder()
        if parentHolder is None:
            return self._containedFileSystem
        return parentHolder.containedFileSystem

    def getFileSystemPath(self):
        parent = self.getParent()
        if parent is None:
            fileSystemPath = ""
        else:
            fileSystemPath = parent.getContainedFileSystemPath()
        if self.pathFragment:
            fileSystemPath = os.path.normpath(os.path.join(
                fileSystemPath, self.pathFragment))
        return fileSystemPath

    def getMetadata(self):
        if self._metadata is None:
            metadata = None
            metadataDoc = None
            fileSystem = self.fileSystem
            fileSystemPath = self.getFileSystemPath()
            if fileSystem.exists(fileSystemPath) and fileSystem.isdir(fileSystemPath):
                fileSystemMetadataPath = os.path.join(fileSystemPath, "_meta.xml")
                metadataHolderPathFragment = "_meta.xml"
                metadataHolderParent = None # Use previous (= self).
            else:
                fileSystemMetadataPath = fileSystemPath + ".meta.xml"
                metadataHolderPathFragment = self.pathFragment + ".meta.xml"
                metadataHolderParent = self.getParent()
            absoluteMetadataPath = fileSystem.getAbsolutePath(fileSystemMetadataPath)
            try:
                metadataFile = fileSystem.open(fileSystemMetadataPath, "rb")
            except IOError, error:
                if error.errno in (errno.ENOENT, errno.ENOTDIR):
                    # No such file or directory
                    pass
                elif error.errno == errno.EACCES:
                    # Permission denied
                    logs.info("Can't access metadata file %s: permission denied."
                              % fileSystemMetadataPath)
                    metadata = "none"
                else:
                    raise
            else:
                try:
                    try:
                        # FIXME: What to use for URL instead of None (second argument)?
                        if hasattr(metadataFile, "fileno"):
                            metadataDoc = libxml2.readFd(
                                metadataFile.fileno(), absoluteMetadataPath, None,
                                libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
                        else:
                            # For zip files, ...
                            metadataDoc = libxml2.readDoc(
                                metadataFile.read(), absoluteMetadataPath, None,
                                libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
                    except (libxml2.parserError, libxml2.treeError):
                        logs.info('[getMetadata] Error parsing XML file at "%s".' % 
                            fileSystemMetadataPath)
                finally:
                    metadataFile.close()
            if metadata is None:
                if metadataDoc is None:
                    metadataDoc = libxml2.newDoc("1.0")
                    metadataNode = metadataDoc.newTextChild(None, "metadata", None)
                    metadataNamespace = metadataNode.newNs(namespaces.yep.uri, None)
                    metadataNode.setNs(metadataNamespace)
                metadataHolder = XmlHolder(
                    metadataDoc, pathFragment = metadataHolderPathFragment, previous = self,
                    parent = metadataHolderParent, uriPathFragment = "metadata")
                metadata = metadataHolder.getRootElement()
            self._metadata = metadata
        if self._metadata == "none":
            return None
        return self._metadata

    def getModificationTime(self):
        fileSystemPath = self.getFileSystemPath()
        if fileSystemPath is None:
            return None
        fileSystem = self.fileSystem
        if fileSystem.exists(fileSystemPath):
            return fileSystem.getModificationTime(fileSystemPath)
        else:
            return None

    def getNewActionsByLabel(self):
        actionsByLabel = {}

        for namespaceName, name in modules.getHolderConstructorNamespaceAndElementNames():
            action = "new-%s-%s" % (namespaceName, name)
            label = _("New %s") % name
            actionsByLabel[label] = action

        import elements
        nodesAndOwnerCouples = self.getAllConfigElementNodesAndOwnerCouples(None)
        for nodes, owner in nodesAndOwnerCouples.values():
            node = nodes[0]
            pythonClassNodes = xpaths.evaluateXpath("yep:pythonClass", contextNode = node)
            if not pythonClassNodes:
                continue
            pythonClass = self.convertNodesToPythonClass(
                pythonClassNodes, owner, "yep:pythonClass", default = None)
            if pythonClass is None:
                continue
            if not issubclass(pythonClass, elements.RootElement):
                continue
            nameNodes = xpaths.evaluateXpath("@name", contextNode = node)
            if not nameNodes:
                continue
            name = nameNodes[0].content
            namespaceUriNodes = xpaths.evaluateXpath("@namespace", contextNode = node)
            if not namespaceUriNodes:
                continue
            namespaceUri = namespaceUriNodes[0].content
            namespaceName = namespaces.getName(namespaceUri)
            action = "new-%s-%s" % (namespaceName, name)
            label = _("New %s") % name
            actionsByLabel[label] = action

        for label, action in actionsByLabel.items():
            if not self.walk([action], "GET", "existsAndIsAuthorized"):
                del actionsByLabel[label]
        return actionsByLabel

    def getSimpleLabel(self):
        return _("Untitled Object #%s") % self.localId

    def getSiteXslt(self):
        if self._xslt is None:
            xslAbsolutePath = self.getConfigAbsolutePath("yep:siteXsltFilePath", default = None)
            if xslAbsolutePath is None:
                self._xslt == "none"
            else:
                xsltHolder = DataHolder(
                    pathFragment = xslAbsolutePath, previous = self, mimeType = "text/xml",
                    isRootElder = True,
                    containedFileSystem = filesystems.PartialFileSystem(xslAbsolutePath))
                import stylesheets
                xsltPrototype = xsltHolder.getRootElement()
                self._xslt = stylesheets.StylesheetContext(xsltPrototype, self, previous = self)
        if self._xslt == "none":
            return None
        return self._xslt

    def getWebDavContentLength(self):
        if self._data is None:
            try:
                dataFile = self.getDataFile()
            except IOError, error:
                if error.errno in (errno.ENOENT, errno.ENOTDIR):
                    # No such file or directory
                    raise faults.PathNotFound("")
                if error.errno == errno.EACCES:
                    # Permission denied
                    raise faults.PathForbidden("")
                raise
            if hasattr(dataFile, "fileno"):
                dataSize = os.fstat(dataFile.fileno())[6]
            else:
                # For StringIO and cStringIO classes.
                dataSize = len(dataFile.getvalue())
        else:
            dataSize = len(self._data)
        return dataSize

    def getWebDavContentType(self):
        return self.mimeType

    def getWebDavLastModified(self):
        modificationTime = self.getModificationTime()
        if modificationTime is None:
            return None
        else:
            return time.strftime("%a, %d %b %Y %H:%M:%S GMT", modificationTime)

    def loadData(self):
        dataFile = self.getDataFile()
        data = dataFile.read()
        dataFile.close()
        return data

    def metadata(self, *uriPathFragments):
        metadata = self.getMetadata()
        if metadata is None:
            raise faults.PathNotFound("metadata")
        return metadata.checkAccessAndWalk(
            uriPathFragments, environs.getVar("httpCommand"), environs.getVar("instruction"))

    def register(cls):
        if cls.defaultFileNameExtension and cls.mimeType \
               and cls.defaultFileNameExtension not in mimeTypes:
            logs.info("Registering extension: %s" % cls.defaultFileNameExtension)
            mimeTypes[cls.defaultFileNameExtension] = cls.mimeType
        if cls.mimeTypes:
            classMimeTypes = cls.mimeTypes[:]
        else:
            classMimeTypes = []
        if cls.mimeType and cls.mimeType not in classMimeTypes:
            classMimeTypes.append(cls.mimeType)
        for mimeType in classMimeTypes:
            if mimeType in holderClassesByMimeType:
                if not issubclass(cls, holderClassesByMimeType[mimeType]):
                    raise Exception("""\
Error while registering class %s: MIME type "%s" is already used for class %s.\
""" % (
                        cls, mimeType, holderClassesByMimeType[mimeType]))
            else:
                holderClassesByMimeType[mimeType] = cls
    register = classmethod(register)

    def saveDataFile(self, sourceDataFile, length = None):
        """Save data into file and return True if the file has been newly created."""
        path = self.getFileSystemPath()
        isNewlyCreated = not self.fileSystem.access(path, os.F_OK)
        if isNewlyCreated:
            directoryPath = os.path.dirname(path)
            if not self.fileSystem.access(directoryPath, os.F_OK):
                self.fileSystem.makedirs(directoryPath)
        dataFile = self.fileSystem.open(path, "wb")
        if length is None:
            while True:
                chunk = sourceDataFile.read(1048576) # 1 MB chunk
                if not chunk:
                    break
                dataFile.write(chunk)
        else:
            count = 0
            while count < length:
                chunkSize = min(1048576, length - count)
                chunk = sourceDataFile.read(chunkSize)
                if not chunk:
                    break
                dataFile.write(chunk)
                count += len(chunk)
        dataFile.close()
        if self._data is not None:
            del self._data
        return isNewlyCreated

    def setContainedFileSystem(self, containedFileSystem):
        self._containedFileSystem = containedFileSystem

    containedFileSystem = property(getContainedFileSystem, setContainedFileSystem)
    data = property(getData)
    fileSystem = property(getFileSystem)
    simpleLabel = property(getSimpleLabel)


class StaticDataHolder(DataHolder):
    styled = None # By defaut a static object has no HTML output.
    unstyled = None # By defaut a static object has no HTML output.

    def doHttpGet(self):
        return self.source()

    def getSimplestDownloadUrl(self):
        # By defaut, a data holder can be download.
        # A download is like asking for the source, but the download is forced
        # at the browser step.
        return self.getActionUri("download")

    def getSimplestSourceUrl(self):
        # By defaut, this is the source of a data holder that is sent.
        return self.getUri()

    def getSimplestStyledUrl(self):
        # By default, a data holder has no X(HT)ML representation.
        return None

    def outputHttpDownload(self):
        # Try to use a file instead of a string (so that it doesn't fail with
        # large files).
        try:
            data = self.getDataFile()
        except IOError, error:
            if error.errno == errno.ENOENT:
                # No such file or directory
                raise faults.PathNotFound("")
            if error.errno == errno.EACCES:
                # Permission denied
                raise faults.PathForbidden("")
            raise
        if data is None:
            data = self.data

        headers = None
        if self.uriPathFragment:
            headers = {
                "Content-Disposition": "attachment; filename=\"%s\"" % self.uriPathFragment
                }

        environs.getVar("httpRequestHandler").outputData(
            data, mimeType = self.mimeType, modificationTime = self.getModificationTime(),
            headers = headers)


    def outputHttpSource(self):
        # Try to use a file instead of a string (so that it doesn't fail with
        # large files).
        try:
            data = self.getDataFile()
        except IOError, error:
            if error.errno == errno.ENOENT:
                # No such file or directory
                raise faults.PathNotFound("")
            if error.errno == errno.EACCES:
                # Permission denied
                raise faults.PathForbidden("")
            raise
        if data is None:
            data = self.data

        charset = self.getConfigString("yep:charset", default = None)

        environs.getVar("httpRequestHandler").outputData(
            data, mimeType = self.mimeType, modificationTime = self.getModificationTime(),
            charset = charset)


class ImageHolder(StaticDataHolder):
    mimeTypes = ("image/gif", "image/jpeg", "image/png")

    def generateXml(self, layout):
        import html
        layout.append(html.html(html.body(html.img(src = self.getUri()))))
        return True

    def generateXmlDocument(self):
        htmlDocument = documents.newTemporaryHtmlDocument()
        self.generateXml(htmlDocument)
        return htmlDocument

    def getSimplestStyledUrl(self):
        return self.getActionUri("styled")

    def styled(self):
        # Skip StaticDataHolder (un)definition of styled.
        super(StaticDataHolder, self).styled()

    def unstyled(self):
        # Skip StaticDataHolder (un)definition of styled.
        super(StaticDataHolder, self).unstyled()


class PythonHolder(StaticDataHolder):
    defaultFileNameExtension = ".py"
    mimeType = "text/x-python-src"

    def walk(self, uriPathFragments, command = None, instruction = None):
        if not self.getInRawMode():
            modeNames = self.getConfigString("yep:defaultMode", default = "view").lower()
            for modeName in modeNames.split():
                if modeName == "execute":
                    return self.fileSystem.execute(self.getFileSystemPath(), globals(), locals())
                elif modeName == "view":
                    break
                else:
                    logs.info('Ignoring unknown mode "%s" for PythonHolder at "%s".' % (
                        modeName, self.getAbsolutePath()))
        return super(PythonHolder, self).walk(uriPathFragments, command, instruction)


class TextPlainHolder(StaticDataHolder):
    mimeType = "text/plain"

    def generatePlainText(self):
        return self.data.decode("UTF-8")

    def generateXml(self, layout):
        import html
        layout.append(html.html(html.body(html.pre(self.data))))
        return True

    def generateXmlDocument(self):
        htmlDocument = documents.newTemporaryHtmlDocument()
        self.generateXml(htmlDocument)
        return htmlDocument

    def getSimplestStyledUrl(self):
        return self.getActionUri("styled")

    def styled(self):
        # Skip StaticDataHolder (un)definition of styled.
        super(StaticDataHolder, self).styled()

    def unstyled(self):
        # Skip StaticDataHolder (un)definition of styled.
        super(StaticDataHolder, self).unstyled()


class XmlHolder(DataHolder, documents.AbstractDocument):
    _xmlHolderInited = False
    defaultFileNameExtension = ".xml"
    isTemporary = False
    mimeType = "text/xml"

    def __del__(self):
        DataHolder.__del__(self)
        documents.AbstractDocument.__del__(self)

    def __init__(self, node = None, *arguments, **keywords):
        if self._xmlHolderInited:
            return
        self._xmlHolderInited = True

        DataHolder.__init__(self, *arguments, **keywords)
        documents.AbstractDocument.__init__(self, node)
        self.changeClass()

    def changeClass(self):
        try:
            node = self.getRootNode()
        except libxml2.treeError:
            # we got here with "[EX] Parsing failed"; go back to StaticDataHolder and 
            # set mimetype to text/plain
            # Note: maybe we should instead return an error page?
            self.__class__ = StaticDataHolder
            self.mimeType = "text/plain"
            return
        if node is not None:
            try:
                namespaceUri = node.ns().content
            except libxml2.treeError:
                # The libxml2 Python binding raises this exception when xmlNs returns None.
                namespaceUri = None
            realClass = modules.getHolderClass(namespaceUri, node.name, default = self.__class__)
            if realClass != self.__class__ and issubclass(realClass, self.__class__):
                self.__class__ = realClass

    def deletePublicName(self):
        self.getRootNode().unsetProp("publicName")

    def destroy(self):
        parent = self.getParent()
        if parent is not None:
            parent.itemDestroyed(self)
        self.fileSystem.remove(self.getFileSystemPath())

    def doHttpGet(self):
        if not self.getInRawMode():
            rootElement = self.getRootElement()
            if rootElement is not None:
                try:
                    return rootElement.checkAccessAndWalk(
                        [], environs.getVar("httpCommand"), environs.getVar("instruction"))
                except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized), fault:
                    logs.exception("Ignoring fault %s in %s.doHttpGet()" % (fault, rootElement))
        return super(XmlHolder, self).doHttpGet()

    def doHttpPost(self):
        if not self.getInRawMode():
            rootElement = self.getRootElement()
            if rootElement is not None:
                try:
                    return rootElement.checkAccessAndWalk(
                        [], environs.getVar("httpCommand"), environs.getVar("instruction"))
                except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized), fault:
                    logs.exception("Ignoring fault %s in %s.doHttpPost()" % (fault, rootElement))
        return super(XmlHolder, self).doHttpPost()

    def generatePlainText(self):
        rootElement = self.getRootElement()
        if rootElement is not None:
            return rootElement.generateXml(layout)
        return super(TemporaryDocument, self).generatePlainText()

    def generateXml(self, layout):
        rootElement = self.getRootElement()
        if rootElement is not None:
            return rootElement.generateXml(layout)
        return super(XmlHolder, self).generateXml(layout)

    def generateXmlDocument(self):
        rootElement = self.getRootElement()
        if rootElement is not None:
            return rootElement.generateXmlDocument()
        return super(XmlHolder, self).generateXmlDocument()

    def getBind(self, context, modelContext):
        return None

    def getChildInstanceDataXpath(self, context, modelContext):
        return ""

    def getDataHolder(self):
        return self

    def getLocalId(self):
        return self.pathFragment

    def getModelId(self):
        return None

    def getNode(self):
        if self._node is None:
            if self.isRemote:
                raise Exception("FIXME, TODO: download data.")
            else:
                doc = self.parseDataFile()
            self._node = doc
            if self.isRootElder:
                self.publicName = self.getAbsolutePath()
            else:
                self.publicName = self.localId
            doc.xincludeProcess()

            # Now that we know what is the root node, we can change the class of the holder.
            # Note: The change of class can be done quite late after the instantiation of holder.
            self.changeClass()
        return self._node

    def getPublicName(self):
        return self.getRootNode().prop("publicName")

    def isAccessAuthorized(self, command):
        rootElement = self.getRootElement()
        if rootElement is not None:
            return rootElement.isAccessAuthorized(command)
        return super(XmlHolder, self).isAccessAuthorized(command)

    def isContentAccessAuthorized(self, contentName, command):
        rootElement = self.getRootElement()
        if rootElement is not None:
            return rootElement.isContentAccessAuthorized(contentName, command)
        return super(XmlHolder, self).isContentAccessAuthorized(contentName, command)

    def outputHttpSource(self):
        environs.getVar("httpRequestHandler").outputData(
            self.getData(), mimeType = self.mimeType,
            modificationTime = self.getModificationTime())

    def parseDataFile(self):
        dataFile = self.getDataFile()
        try:
            try:
                # The code below had several problems :
                #   - The parseXxx functions are deprecated (use readXxx
                #     instead).
                #   - htmlParseDoc doesn't set the namespaces.
                #   - parseDoc doesn't work with XHTML files containing
                #     entities like &eacute;, etc.
                # if self.mimeType == 'text/html':
                #     # FIXME: None instead of "UTF-8" doesn't work for
                #     # unicode files.
                #     self.node = libxml2.htmlParseDoc(data, "UTF-8")
                # else:
                #     self.node = libxml2.parseDoc(data)

                # FIXME: For performance reasons, we should use readFd
                # instead of readDoc, whithout reading data first.
                # FIXME: I'm not sure what to use as URL argument.
                # Option XML_PARSE_DTDLOAD is needed for XHTML documents
                # containing entities liek &eacute;, etc. Option
                # XML_PARSE_DTDATTR also works, but I don't know which
                # option is best.
                # Option XML_PARSE_NONET ensures no network access will be
                # done (but local XML catalog is still used). Setting this
                # option ensures that no "invisible" net access is done
                # and a warning is logged when one is required.
                if hasattr(dataFile, "fileno"):
                    doc = libxml2.readFd(
                        dataFile.fileno(), self.getAbsolutePath(), None,
                        libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
                else:
                    # For zip files, ...
                    doc = libxml2.readDoc(
                        dataFile.read(), self.getAbsolutePath(), None,
                        libxml2.XML_PARSE_DTDLOAD | libxml2.XML_PARSE_NONET)
            except (libxml2.parserError, libxml2.treeError), e:
                logs.info('[parseDataFile] Error parsing XML file at "%s".' % self.getAbsolutePath())
                # this is an error parsing but libxml2 may raise treeError;
                # <grin>; let's modify the message
                e.msg = "[EX] Parsing failed"
                raise
        finally:
            dataFile.close()
        return doc

    def save(self):
        publicName = self.publicName
        localId = self.localId
        if publicName and self.defaultFileNameExtension and "." not in publicName:
            publicName += self.defaultFileNameExtension
        if publicName and publicName != localId:
            isRenamed = True
            oldLocalId = localId
            oldFileSystemPath = self.getFileSystemPath()
            localId = publicName
            self.pathFragment = localId
            if self.uriPathFragment == oldLocalId:
                self.uriPathFragment = localId
        else:
            isRenamed = False
        del self.publicName
        data = self.serialize()
        self.publicName = publicName
        fileSystem = self.fileSystem
        fileSystemPath = self.getFileSystemPath()
        fileSystemDirectoryPath = os.path.dirname(fileSystemPath)
        if not self.fileSystem.access(fileSystemDirectoryPath, os.F_OK):
            self.fileSystem.makedirs(fileSystemDirectoryPath)
        if isRenamed:
            try:
                fileSystem.rename(oldFileSystemPath, fileSystemPath)
            except OSError:
                logs.debug('oldFileSystemPath = "%s", fileSystemPath = "%s"' % (
                    oldFileSystemPath, fileSystemPath))
                raise
        dataFile = fileSystem.open(fileSystemPath, "wb")
        dataFile.write(data)
        dataFile.close()
        if isRenamed:
            parent = self.getParent()
            if parent is not None:
                parent.itemRenamed(self, oldLocalId)

    def setNode(self, node):
        super(XmlHolder, self).setNode(node)
        self.changeClass()

    def setPermanentLocalId(self):
        publicName = self.publicName
        if publicName:
            localId = publicName
        else:
            localId = self.getParent().newItemLocalId()
        if self.defaultFileNameExtension and "." not in localId:
            localId += self.defaultFileNameExtension
        if self.uriPathFragment == self.pathFragment:
            self.uriPathFragment = localId
        self.pathFragment = localId
        if self.isTemporary:
            del self.isTemporary

    def setPublicName(self, publicName):
        self.getRootNode().setProp("publicName", publicName)

    def walk(self, uriPathFragments, command = None, instruction = None):
        try:
            return super(XmlHolder, self).walk(uriPathFragments, command, instruction)
        except (faults.PathForbidden, faults.PathNotFound, faults.PathUnauthorized), fault:
            rootElement = self.getRootElement()
            if rootElement is None:
                raise
            return rootElement.checkAccessAndWalk(uriPathFragments, command, instruction)

    localId = property(getLocalId)
    modelId = property(getModelId)
    node = property(getNode, setNode, documents.AbstractDocument.deleteNode)
    publicName = property(getPublicName, setPublicName, deletePublicName)


def newXmlHolder(namespaceUri, elementName, directoryHolder, pathFragment = None,
                 uriPathFragment = None, fileNameExtension = ".xml", temporary = False):
    assert isinstance(directoryHolder, DataHolder)
    if pathFragment is None:
        if temporary:
            namespaceName = namespaces.getName(namespaceUri)
            itemLocalId = "new-%s-%s" % (namespaceName, elementName)
        else:
            itemLocalId = directoryHolder.newItemLocalId()
            if fileNameExtension:
                itemLocalId = itemLocalId + fileNameExtension
            if uriPathFragment is None:
                uriPathFragment = itemLocalId
        pathFragment = itemLocalId
    itemDoc = libxml2.newDoc("1.0")
    itemRootNode = itemDoc.newTextChild(None, elementName, None)
    if namespaceUri == namespaces.yep.uri:
        namespacePrefix = None
    else:
        namespacePrefix = namespaces.getName(namespaceUri)
    itemNamespace = itemRootNode.newNs(namespaceUri, namespacePrefix)
    itemRootNode.setNs(itemNamespace)
    itemHolder = XmlHolder(itemDoc, pathFragment = pathFragment, previous = directoryHolder,
                           uriPathFragment = uriPathFragment)
    itemHolder.isTemporary = temporary
    return itemHolder


ImageHolder.register()
PythonHolder.register()
TextPlainHolder.register()
XmlHolder.register()
