# Copyright 1999-2017. Parallels IP Holdings GmbH. All Rights Reserved.
import shutil
import os, cStringIO
from xml.dom import minidom
from xml.dom import Node
from encoder import EncoderFile
from sys import argv, exit
from xml.sax import make_parser
from xml.sax.xmlreader import AttributesImpl
from xml.sax.handler import ContentHandler
import pmm_config
import xml_transform
import pmmcli_session
import pmm_dump_transformer
import pmm_api_xml_protocols
import pmm_dump_access_service


class InfoXmlHandler(ContentHandler):
    def __init__(self, xmlTransformSpecificationWorker, xmlTransformPathProcessor):
        self.__xmlTransformSpecificationWorker = xmlTransformSpecificationWorker
        # The attribute self.__xml_transform_path_processor is an instance of xml_transform_path_processor,
        # unique to one xml transform processing session.
        # Tracks current context path, should be passed to XMLTransformWorker to process XPath matches
        self.__xmlTransformPathProcessor = xmlTransformPathProcessor
        self.__elementValue = u''
        self.__elementProcessingStack = []

    def startElement(self, name, attrs):
        self.__elementValue = u''
        self.__xmlTransformSpecificationWorker.addPathElement(name, attrs, self.__xmlTransformPathProcessor)

        parent_processing = None
        if len(self.__elementProcessingStack) > 0:
            parent_processing = self.__elementProcessingStack[len(self.__elementProcessingStack)-1][0]

        started = None
        # parent 'copy', 'filter-out' and 'skip' should override processing_type
        processing_type = parent_processing
        if processing_type in ['filter-out', 'skip']:
            pass
        elif processing_type == 'copy':
            self.__xmlTransformSpecificationWorker.startElement(name, attrs, self.__xmlTransformPathProcessor)
            started = 1
        else:
            matched, processing_type, attributes_processing_type = self.__xmlTransformSpecificationWorker.matchNode(
                name, attrs, self.__xmlTransformPathProcessor)
            if matched and processing_type != 'skip':
                if attributes_processing_type == 'skip':
                    attrs = AttributesImpl({})
                if processing_type == 'process-auxiliary':
                    attrs = AttributesImpl({'deployer-action': 'auxiliary'})
                elif processing_type == 'process-content':
                    attrs = AttributesImpl({'deployer-action': 'deploy-content'})
                self.__xmlTransformSpecificationWorker.startElement(name, attrs, self.__xmlTransformPathProcessor)
                started = 1
            else:
                # act like processing_type be 'filter-out' or 'skip'
                pass
        
        self.__elementProcessingStack.append((processing_type, started))

    def characters(self, ch):
        self.__elementValue = self.__elementValue + ch

    def endElement(self, name):
        self.__xmlTransformSpecificationWorker.removePathElement(name, self.__xmlTransformPathProcessor)
        
        started = self.__elementProcessingStack[len(self.__elementProcessingStack)-1][1]
        
        if started:
            self.__xmlTransformSpecificationWorker.endElement(name, self.__elementValue)
        
        self.__elementValue = u''
        self.__elementProcessingStack.pop()


class InfoXMLProcessor:
    def __init__(self, info_xml, infoXmlHandler):
        self.__info_xml = info_xml
        self.__infoXmlHandler = infoXmlHandler

    def process(self):
        parser = make_parser()   
        parser.setContentHandler(self.__infoXmlHandler)

        parser.parse(open(self.__info_xml))


#class to work with (XX)XMLTransformSpecification classes
class XMLTransformWorker:
    def __init__(self, xml_transform_specification, xml_writer):
        self.__xml_transform_specification = xml_transform_specification
        self.__xml_writer = xml_writer

    def get_xml_writer(self):
        return self.__xml_writer

    def get_xml_transform_specification(self):
        return self.__xml_transform_specification

    def addPathElement(self, name, attrs, xmlTransformPathProcessor):
        return xmlTransformPathProcessor.addPathElement(name, attrs)

    def removePathElement(self, name, xmlTransformPathProcessor):
        return xmlTransformPathProcessor.removePathElement(name)

    def matchNode(self, name, attrs, xmlTransformPathProcessor):
        return self.__xml_transform_specification.match(name, attrs, xmlTransformPathProcessor)

    def startElement(self, name, attrs, xmlTransformPathProcessor = None):
        self.__xml_writer.startElement(name, attrs)

    def endElement(self, name, value):
        self.__xml_writer.endElement(name, value)


class XMLTransformPathProcessor:
    def __init__(self):
        self.__exclude_from_path = ['Envelope', 'Data', 'migration-dump']
        # self.__path is a list of the tuples (element_name, attribute_dictionary)
        self.__path = []

    def addPathElement(self, name, attrs):
        if not name in self.__exclude_from_path:
            path_element = (name, attrs)
            self.__path.append(path_element)

    def removePathElement(self, name):
        if not name in self.__exclude_from_path:
            self.__path.pop()

    def getAttrRequiredPath(self, required_attribute_name, get_parent_path):
        guided_path = ''
        if get_parent_path:
            if len(self.__path) <= 1:
                return ''
            path_array = self.__path[:-1]
        else:
            path_array = self.__path
        for path_element in path_array:
            guided_path = guided_path + "/" + path_element[0]
            try:
                guid_value = path_element[1][required_attribute_name]
                guided_path = guided_path + "[@" + required_attribute_name + "='" + guid_value + "']"
            except KeyError:
                pass
        return guided_path

    # macth string 'path' with current context path
    def pathMatched(self, path):
        path_elements = path.split('/')
        # next allows both the path forms: "/root/element" and "root/element"
        if path_elements[0] == '':
            del path_elements[0]
        
        path_index = -1
        if (len(path_elements)+1) != len(self.__path):
            return False
        for element in path_elements:
            path_index = path_index + 1
            has_attr = False
            element_name = ''
            attr_value = ''
            attr_name = ''
            
            if element[-1] == ']' and element.find('[') > 0:
                attr_string_index = element.index('[')
                element_name = element[:attr_string_index]
                attr_string = element[attr_string_index+1:-1].strip()
                if attr_string[0] == "@":
                    aps = attr_string[-1]
                    aps_start_index = attr_string.find(aps)
                    equal_index = attr_string.find('=')
                    if (aps in ['"',"'"]) and (aps_start_index != (len(attr_string)-1)) and (equal_index < aps_start_index) and (equal_index > 2):
                        attr_value = attr_string[attr_string[:-1].rindex(aps)+1:-1]
                        attr_name = attr_string[1:equal_index].strip()
            else:
                element_name = element
            
            if self.__path[path_index][0] != element_name:
                return False
            if attr_name != '':
                attrs = self.__path[path_index][1]
                try:
                    attribute_value = attrs[attr_name]
                    if attr_value != '':
                        if attr_value != attribute_value:
                            return False
                except KeyError:
                    return False
        
        return True


# class to work with (XX)XMLTransformSpecification classes
#
# Notes:
# Current functional specification & pmm_api_xml_protocol.xsd schema do not support 'copy' processing type in 'dump-overview- transformation.
# Anyway, the 'dump-overview' xml transform processing type is not a 'copy' type by nature. It 'wraps' each processed and passed node into <objects>...<objects-to-select> elements.
# So, as there is no appropriate processing for 'copy' instruction, let it will be a synonym for 'filter-out' processing type.
#
class DumpOverviewXMLTransformWorker(XMLTransformWorker):
    def __init__(self, xml_transform_specification, xml_writer):
        XMLTransformWorker.__init__(self,xml_transform_specification,xml_writer)
        # The dump root element 'migration-dump' not exists usually in XMLTransform specification. So we should force its matching to work the same way as for other nodes
        # Include 'Envelope', 'Data' elements to allow get dump overview on signed dump
        self.__pass_throw_element = ['Envelope', 'Data', 'migration-dump']

    def matchNode(self, name, attrs, xmlTransformPathProcessor):
        if name in self.__pass_throw_element:
            return 1, 'process-recursive', 'copy'
        else:
            return XMLTransformWorker.matchNode(self, name, attrs, xmlTransformPathProcessor)

    def startElement(self, name, attrs, xmlTransformPathProcessor):
        if name in self.__pass_throw_element:
            if name == 'migration-dump':
                XMLTransformWorker.startElement(self, 'metainformation',{})
                for at_name in attrs.keys():
                    XMLTransformWorker.startElement(self,  'record',{'name':at_name})
                    XMLTransformWorker.endElement(self,  'record',attrs[at_name])
                XMLTransformWorker.endElement(self,  'metainformation','')
                XMLTransformWorker.startElement(self,  'objects-to-select',{})
        else:
            XMLTransformWorker.startElement(self, 'object', {})
            
            display_element_attrs = {'type':name,'name':''}
            if attrs.has_key('name'):
                display_element_attrs['name'] = attrs['name']
            XMLTransformWorker.startElement(self, 'display', display_element_attrs)
            XMLTransformWorker.endElement(self, 'display', '')
            
            select_element_attrs = {'nodename':name,'path':xmlTransformPathProcessor.getAttrRequiredPath('guid', True)}
            XMLTransformWorker.startElement(self, 'select', select_element_attrs)
            
            XMLTransformWorker.startElement(self, 'attributes', {})
            if attrs.has_key('owner-guid'):
                XMLTransformWorker.startElement(self, 'owner-guid', {})
                XMLTransformWorker.endElement(self, 'owner-guid', attrs['owner-guid'])
            if attrs.has_key('guid'):
                XMLTransformWorker.startElement(self, 'guid', {})
                XMLTransformWorker.endElement(self, 'guid', attrs['guid'])
            if attrs.has_key('id'):
                XMLTransformWorker.startElement(self, 'id', {})
                XMLTransformWorker.endElement(self, 'id', attrs['id'])
            if attrs.has_key('name'):
                XMLTransformWorker.startElement(self, 'name', {})
                XMLTransformWorker.endElement(self, 'name', attrs['name'])
           
            XMLTransformWorker.endElement(self, 'attributes', '')
            XMLTransformWorker.endElement(self, 'select', '')
            
            XMLTransformWorker.startElement(self, 'objects', {})

    def endElement(self, name, value):
        if name == 'migration-dump':
            XMLTransformWorker.endElement(self, 'objects-to-select','')
        elif not name in self.__pass_throw_element:
            XMLTransformWorker.endElement(self, 'objects', '')
            XMLTransformWorker.endElement(self, 'object', '')

#class to work with (XX)XMLTransformSpecification classes
class RestoreSpecificationXMLTransformWorker(XMLTransformWorker):
    def __init__(self, xml_transform_specification, xml_writer):
        XMLTransformWorker.__init__(self,xml_transform_specification,xml_writer)
        # The dump root element 'migration-dump' not exists usually in XMLTransform specification. So we should force its matching to work the same way as for other nodes
        self.__pass_throw_element = ['migration-dump']

    def matchNode(self, name, attrs, xmlTransformPathProcessor):
        if name in self.__pass_throw_element:
            return 1, 'process-recursive', 'copy'
        else:
            return XMLTransformWorker.get_xml_transform_specification(self).match(name, attrs, xmlTransformPathProcessor)

    def startElement(self, name, attrs, xmlTransformPathProcessor):
        XMLTransformWorker.startElement(self, name, attrs, xmlTransformPathProcessor)

    def endElement(self, name, value):
        XMLTransformWorker.endElement(self, name, value)


#implements XMLTransformSpecification static schema for 'dump-overview' type
class DumpOverviewXMLTransformSpecification:
    def __init__(self, dump_overview_xml_transform_specification_file = None):
        self.__dump_overview_xml_transform_specification_file = None
        self.__xmlt = None
        if not dump_overview_xml_transform_specification_file:
            dump_overview_xml_transform_specification_file = os.path.join(pmm_config.pmmcli_dir(),'dump_overview_xml_transform.xml')
        self.load(dump_overview_xml_transform_specification_file)
    
    def load(self,dump_overview_xml_transform_specification_file):
        self.__dump_overview_xml_transform_specification_file = dump_overview_xml_transform_specification_file
        self.__xmlt = xml_transform.Transformation.factory()
        needed_node = None
        for child_node in minidom.parse(os.path.join(pmm_config.pmmcli_dir(),dump_overview_xml_transform_specification_file)).childNodes:
            if child_node.nodeType == Node.ELEMENT_NODE:
                needed_node = child_node
                break

        if needed_node == None:
            raise Exception("File %s has wrong format" % os.path.join(pmm_config.pmmcli_dir(),dump_overview_xml_transform_specification_file))

        self.__xmlt.build(needed_node)

    def match(self, name, attrs, xmlTransformPathProcessor):
        # Dump Overview XmlTransform specification does not work with node attributes
        if self.__xmlt:
            for item in self.__xmlt.get_node():
                if item.get_name() == name:
                    return 1, item.get_children_processing_type(), item.get_attributes_processing_type()
        return 0, 'filter-out', ''


#implements XMLTransformSpecification static schema for 'dump-overview' type
class RestoreSpecificationXMLTransformSpecification:
    def __init__(self, restore_specification_xml_transform_specification_file = None):
        self.__restore_specification_xml_transform_specification_file = None
        self.__xmlt = None
        if not restore_specification_xml_transform_specification_file:
            restore_specification_xml_transform_specification_file = os.path.join(pmm_config.pmmcli_dir(),'restore_specification_xml_transform.xml')
        self.load(restore_specification_xml_transform_specification_file)
    
    def load(self,restore_specification_xml_transform_specification_file):
        self.__restore_specification_xml_transform_specification_file = restore_specification_xml_transform_specification_file
        self.__xmlt = xml_transform.Transformation.factory()
        self.__xmlt.build(minidom.parse(os.path.join(pmm_config.pmmcli_dir(),restore_specification_xml_transform_specification_file)).childNodes[0] )

    def match(self, name, attrs, xmlTransformPathProcessor):
        if self.__xmlt:
            for item in self.__xmlt.get_node():
                if item.get_name() == name:
                    attributes = item.get_attributes()
                    attributes_passed = 1
                    if attributes:
                        for attr in attributes.get_attribute():
                            attr_name = attr.get_name()
                            attr_value = attr.get_value()
                            try:
                                attribute_value = attrs[attr_name]
                                if attribute_value != attr_value:
                                    attributes_passed = 0
                            except KeyError:
                                attributes_passed = 0
                            
                            if attributes_passed == 0:
                                break
                    
                    if attributes_passed:
                        # now check if any <path> corresponds to 'path'
                        context_passed = 1
                        context = item.get_context()
                        if context:
                            context_passed = 0
                            for path_item in context.get_path():
                                if xmlTransformPathProcessor.pathMatched(path_item):
                                    context_passed = 1
                                    break
                        
                        if attributes_passed and context_passed:
                            return 1, item.get_children_processing_type(), item.get_attributes_processing_type()
        
        # matched node is not found or there are no nodes at all
        return 0, 'process-recursive', ''


class DumpOverviewCreator:
    def __init__(self, session_id):
        self.__session_id = session_id
        self.__dump_overview = None

    def getDumpFormat(self,info_xml):
        session = pmmcli_session.PmmcliSession(self.__session_id)
        dump_format = pmm_dump_transformer.DumpTransformer().getDumpFormat(info_xml, session.get_session_path())
        return dump_format

    def addDumpFormat(self,info_xml,dump_overview_object):
        dump_format_found = False
        metainformation_object = dump_overview_object.get_metainformation()
        if metainformation_object is not None:
            records = metainformation_object.get_record()
            if records is not None:
                dump_format_found = False
                for record_object in records:
                    if record_object.get_name() == 'dump-format':
                        dump_format_found = True
                        break
        if not dump_format_found:
            dump_format = self.getDumpFormat(info_xml)
            record_object = pmm_api_xml_protocols.Record( name = 'dump-format')
            record_object.setValueOf_(dump_format)
            if metainformation_object is None:
                metainformation_object = pmm_api_xml_protocols.Metainformation()
                dump_overview_object.set_metainformation( metainformation_object )
            metainformation_object.add_record(record_object)

    def create(self,info_xml,dump_overview_file):
        writer = pmm_dump_access_service.XMLContentSimpleWriter()
        xml_processor = InfoXMLProcessor( info_xml, InfoXmlHandler( DumpOverviewXMLTransformWorker(DumpOverviewXMLTransformSpecification(),writer),XMLTransformPathProcessor()) )
        xml_processor.process()
        
        self.__dump_overview = pmm_api_xml_protocols.DumpOverview.factory()
        self.__dump_overview.build(minidom.parseString('<dump-overview session-id="' + str(self.__session_id) + '">' + writer.get_content().encode('utf-8') + '</dump-overview>').childNodes[0] )
        
        self.addDumpFormat(info_xml, self.__dump_overview)
        
        f = open(dump_overview_file, 'wt')
        resp_str = cStringIO.StringIO()
        resp_encoded = EncoderFile(resp_str, "utf-8")
        resp_encoded.write('<?xml version="1.0" encoding="UTF-8"?>\n')
        self.__dump_overview.export(resp_encoded, 0, name_ = 'dump-overview')
        f.write(resp_str.getvalue())
        f.close()


class RestoreSpecificationCreator:
    def __init__(self, session_id):
        self.__session_id = session_id

    # the only way to create is create from info.xml and objects list to restore
    def create(self, owner_type, owner_guid, info_xml, restore_specification_file, dump_overview_file, selected_objects_file = None):
        # if selected_objects_file not specified - create restore_specification as copy of info_xml
        if not selected_objects_file:
            if info_xml != restore_specification_file:
                shutil.copy2(info_xml,restore_specification_file)
        else:
            writer = pmm_dump_access_service.XMLSimpleWriter()
            xml_processor = InfoXMLProcessor( info_xml, InfoXmlHandler( RestoreSpecificationXMLTransformWorker(RestoreSpecificationXMLTransformSpecification(selected_objects_file),writer),XMLTransformPathProcessor() ) )
            xml_processor.process()
            ps = open(restore_specification_file,'wt')
            ps.write(writer.get_content())
            ps.close()
        
        DumpOverviewCreator(self.__session_id).create(restore_specification_file,dump_overview_file)

    # assign restore_specification_file modified by ConflictResolver
    def update(self,restore_specification_file, dump_overview_file):
        DumpOverviewCreator(self.__session_id).create(restore_specification_file,dump_overview_file)


def usage():
    print "pmm_dump_overview. Plesk Migration Manager."
    print "(c) Parallels"
    print "" 
    print "Usage:"
    print "" 
    print "    pmm_dump_overview dump-overview [<info_xml>] [<output_file>]"
    print "        or" 
    print "    pmm_dump_overview restore-specification [<info_xml>] [<xml_transform>] [<output_file>]"
    print "" 
    print "" 
    print "        if 'dump-overview' command is specified, the result file will be dump overview of info-xml file"
    print "        if 'restore-specification'command is specified, the result file will be dump overview of info-xml file"
    print "" 
    print "        <info_xml> is an absolute path to input info.xml file"
    print "        <xml_transform> is an absolute path to input xml_transform xml file"
    print "" 
    print "" 


def main():
    print argv
    if len(argv) == 4 and argv[1] == 'dump-overview':
        info_xml_path = argv[2]
        dump_overview_path = argv[3]
        dump_overview_creator = DumpOverviewCreator('0')
        dump_overview_creator.create(info_xml_path,dump_overview_path)
    elif len(argv) == 5 and argv[1] == 'restore-specification':
        info_xml_path = argv[2]
        xml_transform_path = argv[3]
        restore_specification_path = argv[4]
        writer = pmm_dump_access_service.XMLSimpleWriter()
        rsxmlts = RestoreSpecificationXMLTransformSpecification(xml_transform_path)
        xml_processor = InfoXMLProcessor( info_xml_path, InfoXmlHandler( RestoreSpecificationXMLTransformWorker(rsxmlts,writer),XMLTransformPathProcessor() ) )
        xml_processor.process()
        ps = open(restore_specification_path,'wt')
        ps.write(writer.get_content())
        ps.close()
    else:
        usage()
        exit(1)

if __name__ == '__main__':
    main()

