# Copyright (c) 2000 LOGILAB S.A. (Paris, FRANCE).
# http://www.logilab.fr/ -- mailto:contact@logilab.fr
#
# 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.

__revision__ = "$Id: Xmlrep.py,v 1.1 2001/12/21 13:44:03 syt Exp $"

from narval.lib import *
try:
    from Ft.Xml import cDomlette
except:
    from xml import cDomlette
from narval.dom import DomReader
import urllib, urlparse
import types, string

MOD_XML="""<?xml version='1.0' encoding='%s'?>
<module xmlns:al='%s' xmlns:xsl='%s'>

<al:action name='get_xslt_result' func='apply_xslt_f'>
 <al:description lang='en'>
apply an xsl transform to an xml element
 </al:description>
 <al:input id='repository'>
   <al:match>xml-repository[@type]/text()</al:match>
   <al:description lang='en'>
the repository which contain the element to transform
   </al:description>
 </al:input>
 <al:input  id='transform'>
   <al:match>xsl:transform</al:match>
   <al:description lang='en'>the xsl tranform</al:description>
 </al:input>
 <al:input optional='yes' list='yes' id='additional-node'>
   <al:description lang='en'>optional additional node</al:description>
 </al:input>
 <al:output list='yes' optional='yes' id='result'>
   <al:description lang='en'>result from the transforms</al:description>
 </al:output>
</al:action>

<al:action name='write_xslt_result' func='apply_xslt_f'>
 <al:description lang='en'>apply an xsl transform to an xml element and write the result to a file</al:description>
 <al:input id='repository'><al:match>xml-repository[@type]/text()</al:match></al:input>
   <al:description lang='en'>the repository which contain the element to transform</al:description>
 <al:input id='url'><al:match>url</al:match>
   <al:description lang='en'>url for the result file</al:description></al:input>
 <al:input id='transform'><al:match>xsl:transform</al:match> 
   <al:description lang='en'>the xsl tranform</al:description></al:input>
 <al:input optional='yes' list='yes' id='additional-node'>
   <al:description lang='en'>optional additional node</al:description></al:input>
</al:action>

<al:action name='get_node' func='apply_xpath_f'>
 <al:description lang='en'>
return a single node matching xpath in a xml repository
 </al:description>
 <al:input id='repository'>
   <al:match>xml-repository[@type]/text()</al:match>
   <al:description lang='en'>xml repository</al:description>
 </al:input>
 <al:input id='location'>
   <al:match>xpath-location/text()</al:match>
   <al:description lang='en'>xpath locating the node</al:description>
 </al:input>
 <al:output id='result' list='no' optional='yes'>
   <al:description lang='en'>the selected node</al:description>
 </al:output>
</al:action>

<al:action name='get_nodes' func='apply_xpath_f'>
<al:description lang='en'>return one ore more nodes matching xpath</al:description>
<al:input id='repository'><al:match>xml-repository[@type]/text()</al:match>
<al:description lang='en'>xml repository</al:description></al:input>
<al:input id='location'><al:match>xpath-location/text()</al:match>
<al:description lang='en'>xpath locating the nodes</al:description></al:input>
<al:output id='result' list='yes' optional='yes'>
<al:description lang='en'>the selected nodes</al:description></al:output>
</al:action>

<al:action name='xpath_expression' func='apply_xpath_f'>
<al:description lang='en'>return a node with text child representing the xpath
expression result</al:description>
<al:input id='repository'><al:match>xml-repository[@type]/text()</al:match>
<al:description lang='en'>xml repository</al:description></al:input>
<al:input id='location'><al:match>xpath-expression/text()</al:match>
<al:description lang='en'>xpath to evaluate</al:description></al:input>
<al:output id='result'><al:match>expression-result</al:match>
<al:description lang='en'>node with a text node containing the result</al:description>
</al:output>
</al:action>

<al:action name='delete_nodes' func='delete_node_f'>
<al:description lang='en'>delete all nodes matching xpath location</al:description>
<al:input id='repository'><al:match>xml-repository[@type]/text()</al:match>
<al:description lang='en'>xml repository to update</al:description></al:input>
<al:input id='location'><al:match>xpath-location/text()</al:match>
<al:description lang='en'>xpath designing the nodes to delete</al:description></al:input>
</al:action>

<al:action name='insert_node' func='insert_node_f'>
 <al:description lang='en'>insert a new node to the xpath location</al:description>
 <al:input id='repository'>
   <al:match>xml-repository[@type]/text()</al:match>
   <al:description lang='en'>xml repository to update</al:description>
 </al:input>
 <al:input id='location'>
   <al:match>xpath-location/text()</al:match>
   <al:description lang='en'>
xpath designing the place where the node will be insert
   </al:description></al:input>
 <al:input id='inserted-node'>
   <al:description lang='en'>node to insert</al:description>
 </al:input>
 <al:input optional='yes' id='pos'>
   <al:match>position/text()</al:match>
   <al:description lang='en'>
optional node with an integer I as text, saying to insert the node as child number I
   </al:description>
 </al:input>
</al:action>

<al:action name='replace_node' func='replace_node_f'>
 <al:description lang='en'>
replace a node designed with xpath by a given node
 </al:description>
 <al:input id='repository'>
   <al:match>xml-repository[@type]/text()</al:match>
   <al:description lang='en'>xml repository to update</al:description>
 </al:input>
 <al:input id='discriminant' list='yes'>
   <al:match>discriminant-attribute/text()</al:match>
   <al:description lang='en'>
optional node with an attribute name as text child. The node will replace a node
from the xml repository which as the same attribute value 
   </al:description>
 </al:input>
 <al:input id='inserted-node'>
   <al:description lang='en'>new node</al:description>
 </al:input>
</al:action>

<al:action name='replace_selected_node' func='replace_node_f'>
 <al:description lang='en'>
replace a node designed with xpath by a given node
 </al:description>
 <al:input id='repository'>
   <al:match>xml-repository[@type]/text()</al:match>
   <al:description lang='en'>xml repository to update</al:description>
 </al:input>
 <al:input id='location'>
   <al:match>xpath-location/text()</al:match>
   <al:description lang='en'>xpath designing the node to replace</al:description>
 </al:input>
 <al:input id='inserted-node'>
   <al:description lang='en'>new node</al:description>
 </al:input>
</al:action>

</module>""" % (ENCODING, AL_NS, XSL_NS) # find the %ss ;o) (too easy :(

import threading
class FileLock:
    def __init__(self):
        self.file_dict={}
        self.lock = threading.Lock()
        
    def acquire(self,file_name):
        file_name = expand_vars(file_name)
        self.lock.acquire()
        if not self.file_dict.has_key(file_name):
            self.file_dict[file_name]=threading.Lock()
        self.file_dict[file_name].acquire()
        self.lock.release()
        
    def release(self,file_name):
        file_name = expand_vars(file_name)
        if not self.file_dict.has_key(file_name):
            print "ERROR : no lock on file ",file_name
        else:
            self.file_dict[file_name].release()
            
xmlrep_lock = FileLock()

def apply_xslt_f(args, doc):
    out = {'result': []}
    stylesheet = args['transform']
    rep = args['repository']
    doc_uri = rep.firstChild.data
    doc_type = rep.getAttributeNS(NO_NS,"type")
    additional_nodes = args['additional-node']
    #additional_nodes = additional_nodes + select_many(args,"@*")
    if doc_type == 'plain-file' :
        rw_module = None
    else :
        rw_module = module(doc_type)
    result = _process_xslt(doc_uri, rw_module, additional_nodes, stylesheet, doc)
    if args.has_key("url"):
        output_uri = args["url"].firstChild.data
        write_tree_and_release(output_uri,rw_module,result,DomReader())
    else:
        for n in result.childNodes[:]:
            result.removeChild(n)
            out['result'].append(n)
    return out


def apply_xpath_f(args, doc):
    out = {'result':[]}
    rep = args['repository']
    doc_uri = rep.firstChild.data
    doc_type = rep.getAttributeNS(NO_NS,"type")
    if doc_type == 'plain-file' :
        rw_module = None
    else :
        rw_module = module(doc_type)
    xpath = args["location"].firstChild.data
    d,result,reader = _evaluate_xpath(doc_uri,rw_module,xpath)
    if d is None:
        out['error'] = error(doc, 'No such xml repository %s'%doc_uri)
    elif result is None:
        out['error'] = error(doc, 'No such node %s in xml repository %s'%xpath)
    elif type(result) == types.ListType:
        for elt in result:
            # FIXME: avoid copy
            out['result'].append(doc.importNode(elt,DEEP_COPY))
    else:
        node = doc.createElementNS(NO_NS, 'expression-result')
        out['result'].append(node)
        tnode = doc.createTextNode(str(result))
        node.appendChild(tnode)
    return out


def delete_node_f(args, doc):
    rep = args['repository']
    doc_uri = rep.firstChild.data
    doc_type = rep.getAttributeNS(NO_NS,"type")
    if doc_type == 'plain-file' :
        rw_module = None
    else :
        rw_module = module(doc_type)
    xpath = args["location"].firstChild.data
    d,result,reader = _evaluate_xpath(doc_uri, rw_module, xpath,
                                      xml_reader=cDomlette.RawExpatReader)

    for elt in result:
        elt.parentNode.removeChild(elt)
    write_tree_and_release(doc_uri,rw_module,d,reader)
    return {}


def insert_node_f(args, doc):
    rep = args['repository']
    doc_uri = rep.firstChild.data
    doc_type = rep.getAttributeNS(NO_NS,"type")
    if doc_type == 'plain-file' :
        rw_module = None
    else :
        rw_module = module(doc_type)
    inserted_node = args['inserted-node']
    xpath = args["location"].firstChild.data
    d,result,reader = _evaluate_xpath(doc_uri,rw_module,xpath)
    new_node = d.importNode(inserted_node,DEEP_COPY)
    new_node.removeAttributeNS(AL_NS, 'eid')
    new_node.removeAttributeNS(AL_NS, 'input-id')
    new_node.removeAttributeNS(AL_NS, 'timestamp')
    new_node.removeAttributeNS(AL_NS, 'from_plan')
    new_node.removeAttributeNS(AL_NS, 'from_step')
    for nd in select_many(new_node,'tags') :
        new_node.removeChild(nd)
    
    try:
        position = int(select_one(args,"position/text()").data)
        result[0].insertBefore(new_node,result[0].childNodes[position])
    except:
        result[0].appendChild(new_node)

    write_tree_and_release(doc_uri,rw_module,d,reader)
    return {}

def replace_node_f(args, doc):
    rep = args['repository']
    doc_uri = rep.firstChild.data
    doc_type = rep.getAttributeNS(NO_NS,"type")
    if doc_type == 'plain-file' :
        rw_module = None
    else :
        rw_module = module(doc_type)
    inserted_node = args['inserted-node']
    
    if args.has_key("location"):
        xpath = args["location"].firstChild.data
        key = None
    else:
        xpath = None
        print args
        key = [k.firstChild.data for k in args["discriminant"]]

    if key:
        xpath_build = []
        for k in key:
            key_value = select_one(inserted_node,'@%s'%k).value
            xpath_build.append('@%s="%s"'%(k, key_value))
        xpath = '//%s[%s]'%(inserted_node.nodeName,string.join(xpath_build,' and '))

    d,result,reader = _evaluate_xpath(doc_uri,rw_module,xpath)
    new_node = d.importNode(inserted_node,DEEP_COPY)
    new_node.removeAttributeNS(AL_NS, 'eid')
    new_node.removeAttributeNS(AL_NS, 'input-id')
    new_node.removeAttributeNS(AL_NS, 'timestamp')
    new_node.removeAttributeNS(AL_NS, 'from_plan')
    new_node.removeAttributeNS(AL_NS, 'from_step')
    for nd in select_many(new_node,'.//tags'):
        new_node.removeChild(nd)
    
    result[0].parentNode.replaceChild(new_node,result[0])
    write_tree_and_release(doc_uri,rw_module,d,reader)
    return {}



## Utility functions to be called by action stubs #############################
def module(type) :
    from re import sub
    mod = sub('/', '_', type)
    return load_extension_module('xmlrep',mod)


def _process_xslt(doc_uri, rw_module, additional_nodes, stylesheet, dom_output_document):
    xmlrep_lock.acquire(doc_uri)
    result = None
    try:
        try:
            try:
                from Ft.Xml.Xslt.Processor import Processor
                from Ft.Xml.Xslt import DomWriter
            except:
                import traceback
                traceback.print_exc()
                from xml.xslt.Processor import Processor
                from xml.xslt import DomWriter
            reader = DomReader()
            processor = Processor()
            xpath_lock.acquire()
            processor.appendStylesheetString(domtree_to_string(stylesheet))
            xpath_lock.release()
            writer = DomWriter.DomWriter(dom_output_document)
            
            if rw_module :
                doc = rw_module.read(normalize_url(doc_uri)[0])
            else :
                doc = reader.fromUri(normalize_url(doc_uri)[0])
                    
            arg_node = doc.createElementNS(AL_NS, 'al:arguments')
            arg_node.appendChild(doc.documentElement)
            doc.appendChild(arg_node)
            for n in additional_nodes:
                if n.nodeType == n.ATTRIBUTE_NODE :
                    arg_node.setAttributeNS(n.namespaceURI, n.nodeName, n.value)
                else :
                    arg_node.appendChild(doc.importNode(n,DEEP_COPY))

            xpath_lock.acquire()
            result = processor.runNode(doc,1,{},writer)
            xpath_lock.release()
        except :
            import traceback
            traceback.print_exc()
    finally:
        xmlrep_lock.release(doc_uri)
        return StripXml(result)


def _evaluate_xpath(doc_uri,rw_module,xpath,xml_reader=DomReader):
    doc,result,reader = None, None, None
    xmlrep_lock.acquire(doc_uri)
    try:
        if rw_module :
            reader = DomReader()
            doc = rw_module.read(normalize_url(doc_uri)[0])
        else :
            reader = xml_reader()
            doc = reader.fromUri(normalize_url(doc_uri)[0])
        try:
            result = select_many(doc, xpath)
        except Exception,e:
            xmlrep_lock.release(doc_uri)
            raise e
    finally:
        xmlrep_lock.release(doc_uri)
        return doc,result,reader # we need all this to free memory afterwards
    

def write_tree_and_release(url,rw_module,doc,reader):
    xmlrep_lock.acquire(url)
    try:
        url,url_tuple = normalize_url(url)
        if url_tuple[0] == 'file' :
            if url_tuple[2][0]=='/' and url_tuple[2][2] == ':':
                fname = url_tuple[2][1:]
            else:
                fname = url_tuple[2]
        else:
            fname = url
        try:
            if rw_module :
                rw_module.write(doc,fname)
            else :
                f = open(url_tuple[2],'w')
                #from xml.dom.ext import PrettyPrint
                PrettyPrint(doc,f)
                f.close
        except:
            import traceback
            traceback.print_exc()
            xmlrep_lock.release(url)
            raise Exception("Can't write file at URL %s" % url)
    finally:
        xmlrep_lock.release(url)

if __name__ == "__main__" :
    from xml.dom.ext.reader import Sax2
    doc=Sax2.FromXml(MOD_XML)
    print doc.documentElement
