#! /usr/bin/env python
# -*- coding: utf-8 -*-
# ##########################################################################
#
# This program is part of Zenoss Core, an open source monitoring platform.
# Copyright (C) 2009, Zenoss Inc.
#
# This program is free software; you can redistribute it and/or modify it
# under the terms of the GNU General Public License version 2 as published by
# the Free Software Foundation.
#
# For complete information please visit: http://www.zenoss.com/oss/
#
# ##########################################################################

__doc__ = """ImportRM
Import RelationshipManager objects into a Zope database
This provides support methods used by the Python xml.sax library to
parse and construct an XML document.

Descriptions of the XML document format can be found in the
Developers Guide.
"""
import Globals
import sys
import os
import types
import transaction
import zope.component
from zope.event import notify
from DateTime import DateTime
from xml.sax import make_parser, saxutils, SAXParseException
from xml.sax.handler import ContentHandler

from Acquisition import aq_base
from zExceptions import NotFound
from OFS.PropertyManager import PropertyManager

from Products.ZenUtils.ZCmdBase import ZCmdBase
from Products.ZenUtils.Utils import importClass
from Products.ZenUtils.Utils import getObjByPath

from Products.ZenModel.interfaces import IZenDocProvider
from Products.ZenRelations.Exceptions import *
from Products.Zuul.catalog.events import IndexingEvent

_STRING_PROPERTY_TYPES = ( 'string', 'text', 'password' )



class ImportRM(ZCmdBase, ContentHandler):
    """
    Wrapper module to interface between Zope and the Python SAX XML library. 
The xml.sax.parse() calls different routines depending on what it finds. 

A simple example of a valid XML file can be found in the objects.xml file 
for a ZenPack. 

 <?xml version="1.0"?> 
 <objects> 
   <!-- ('', 'zport', 'dmd', 'Devices', 'rrdTemplates', 'HelloWorld') --> 
   <object id='/zport/dmd/Devices/rrdTemplates/HelloWorld' module='Products.ZenModel.RRDTemplate' class='RRDTemplate'> 
     <property type="text" id="description" mode="w" > This is the glorious description that shows up when we click on our RRD template </property> 
   <tomanycont id='datasources'> 
     <object id='hello' module='Products.ZenModel.BasicDataSource' class='BasicDataSource'> 
       <property select_variable="sourcetypes" type="selection" id="sourcetype" mode="w" > SNMP </property> 
       <property type="boolean" id="enabled" mode="w" > True </property> 
       <property type="string" id="eventClass" mode="w" > /Cmd/Fail </property> 
       <property type="int" id="severity" mode="w" > 3 </property> 
       <property type="int" id="cycletime" mode="w" > 300 </property> 
       <property type="boolean" id="usessh" mode="w" > False </property> 
     <tomanycont id='datapoints'> 
       <object id='hello' module='Products.ZenModel.RRDDataPoint' class='RRDDataPoint'> 
           <property select_variable="rrdtypes" type="selection" id="rrdtype" mode="w" > GAUGE </property> 
           <property type="boolean" id="isrow" mode="w" > True </property> 
       </object> 
     </tomanycont> 
  </object> 

 <!--    snip  --> 

 </objects> 
    """
    rootpath = ''
    skipobj = 0

    def __init__(self, noopts=0, app=None, keeproot=False):
        """
        Initializer
        
        @param noopts: don't use sys.argv for command-line options
        @type noopts: boolean
        @param app: app
        @type app: object
        @param keeproot: keeproot
        @type keeproot: boolean
        """
        ZCmdBase.__init__(self, noopts, app, keeproot)
        ContentHandler.__init__(self)

    def context(self):
        """
        Return the bottom object in the stack
        
        @return:
        @rtype: object
        """
        return self.objstack[-1]

    def cleanattrs(self, attrs):
        """
        Convert all attributes to string values
        
        @param attrs: (key,val) pairs
        @type attrs: list
        @return: same list, but with all values as strings
        @rtype: list
        """
        myattrs = {}
        for (key, val) in attrs.items():
            myattrs[key] = str(val)
        return myattrs

    def startElement(self, name, attrs):
        """
        Function called when the parser finds the starting element
        in the XML file.
        
        @param name: name of the element
        @type name: string
        @param attrs: list of (key, value) tuples
        @type attrs: list
        """
        ignoredElements = [ 'objects' ]
        attrs = self.cleanattrs(attrs)
        if self.skipobj > 0:
            self.skipobj += 1
            return

        self.log.debug('tag %s, context %s, line %s'  % (
             name, self.context().id, self._locator.getLineNumber() ))
        
        if name == 'object':
            
            obj = self.createObject(attrs)

            if obj is None:
                formattedAttrs = ''
                for key, value in attrs.items():
                    formattedAttrs += '  * %s: %s\n' % (key, value)
                raise Exception('Unable to create object using the following '
                        'attributes:\n%s' % formattedAttrs)

            if not self.options.noindex and hasattr(aq_base(obj),
                    'reIndex') and not self.rootpath:
                self.rootpath = obj.getPrimaryId()
                
            self.objstack.append(obj)

        elif name == 'tomanycont' or name == 'tomany':
            nextobj = self.context()._getOb(attrs['id'], None)
            if nextobj is None:
                self.skipobj = 1
                return
            else:
                self.objstack.append(nextobj)
        elif name == 'toone':
            relname = attrs.get('id')
            self.log.debug('toone %s, on object %s', relname,
                           self.context().id)
            rel = getattr(aq_base(self.context()), relname, None)
            if rel is None:
                return
            objid = attrs.get('objid')
            self.addLink(rel, objid)
        elif name == 'link':
            self.addLink(self.context(), attrs['objid'])
        elif name == 'property':
            self.curattrs = attrs
        elif name in ignoredElements:
            pass
        else: 
            self.log.warning( "Ignoring an unknown XML element type: %s" % name ) 

    def endElement(self, name):
        """
        Function called when the parser finds the starting element
        in the XML file.

        @param name: name of the ending element
        @type name: string
        """
        ignoredElements = [ 'toone', 'link' ]
        if self.skipobj > 0:
            self.skipobj -= 1
            return

        noIncrementalCommit = self.options.noCommit or self.options.chunk_size==0

        if name in ('object', 'tomany', 'tomanycont'):
            obj = self.objstack.pop()
            notify(IndexingEvent(obj))
            if hasattr(aq_base(obj), 'index_object'):
               obj.index_object()
            if self.rootpath == obj.getPrimaryId():
                self.log.info('Calling reIndex %s', obj.getPrimaryId())
                obj.reIndex()
                self.rootpath = ''
            if (not noIncrementalCommit and
                not self.objectnumber % self.options.chunk_size):
                self.log.debug("Committing a batch of %s objects" %
                               self.options.chunk_size)
                self.commit()

        elif name == 'objects': # ie end of the file
            self.log.info('End loading objects')
            self.log.info('Processing links')
            self.processLinks()
            if not self.options.noCommit:
                self.commit()
                self.log.info('Loaded %d objects into the ZODB database'
                           % self.objectnumber)
            else:
                self.log.info('Would have created %d objects in the ZODB database'
                           % self.objectnumber)

        elif name == 'property':
            self.setProperty(self.context(), self.curattrs,
                             self.charvalue)
            # We've closed off a tag, so now we need to re-initialize
            # the area that stores the contents of elements
            self.charvalue = ''

        elif name in ignoredElements:
            pass
        else: 
            self.log.warning( "Ignoring an unknown XML element type: %s" % name ) 

    def characters(self, chars):
        """
        Called by xml.sax.parse() with data found in an element 
        eg <object>my characters stuff</object> 

        Note that this can be called character by character. 
        
        @param chars: chars
        @type chars: string
        """
        self.charvalue += saxutils.unescape(chars)

    def createObject(self, attrs):
        """
        Create an object and set it into its container
        
        @param attrs: attrs
        @type attrs: string
        @return: newly created object
        @rtype: object
        """
        # Does the object exist already?
        id = attrs.get('id')
        obj = None
        try:
            if id.startswith('/'):
                obj = getObjByPath(self.app, id)
            else:
                obj = self.context()._getOb(id)
        except (KeyError, AttributeError, NotFound):
            pass

        if obj is None:
            klass = importClass(attrs.get('module'), attrs.get('class'))
            if id.find('/') > -1:
                (contextpath, id) = os.path.split(id)
                try:
                    pathobj = getObjByPath(self.context(), contextpath)
                except (KeyError, AttributeError, NotFound):
                    self.log.warn( "Unable to find context path %s (line %s ?) for %s" % (
                        contextpath, self._locator.getLineNumber(), id ))
                    if not self.options.noCommit:
                        self.log.warn( "Not committing any changes" )
                        self.options.noCommit = True
                    return None
                self.objstack.append(pathobj)
            self.log.debug('Building instance %s of class %s',id,klass.__name__)
            obj = klass(id)
            self.context()._setObject(obj.id, obj)
            obj = self.context()._getOb(obj.id)
            self.objectnumber += 1
            if self.objectnumber % 5000 == 0:
                transaction.savepoint()
            self.log.debug('Added object %s to database'
                            % obj.getPrimaryId())
        else:
            self.log.debug('Object %s already exists -- skipping' % id)
        return obj

    def setZendoc(self, obj, value):
        zendocObj = zope.component.queryAdapter(obj, IZenDocProvider)
        if zendocObj is not None:
            zendocObj.setZendoc( value )
        elif value:
            self.log.warn('zendoc property could not be set to' +
                          ' %s on object %s' % ( value, obj.id ) )

    def setProperty(self, obj, attrs, value):
        """
        Set the value of a property on an object.
        
        @param obj: obj
        @type obj: string
        @param attrs: attrs
        @type attrs: string
        @param value: value
        @type value: string
        @return:
        @rtype:
        """
        name = attrs.get('id')
        proptype = attrs.get('type')
        setter = attrs.get('setter', None)
        self.log.debug('Setting object %s attr %s type %s value %s'
                        % (obj.id, name, proptype, value))
        linenum = self._locator.getLineNumber()

        # Sanity check the value
        value = value.strip()
        try:
            value = str(value)
        except UnicodeEncodeError:
            self.log.warn('UnicodeEncodeError at line %s while attempting' % linenum + \
             ' str(%s) for object %s attribute %s -- ignoring' % (
                           obj.id, name, proptype, value))

        if name == 'zendoc':
            return self.setZendoc( obj, value )

        # Guess at how to interpret the value given the property type
        if proptype == 'selection':
            try:
                firstElement = getattr(obj, name)[0]
                if type(firstElement) in types.StringTypes:
                    proptype = 'string'
            except (TypeError, IndexError):
                self.log.warn("Error at line %s when trying to " % linenum + \
                    " use (%s) as the value for object %s attribute %s -- assuming a string"
                               % (obj.id, name, proptype, value))
                proptype = 'string'

        if proptype == 'date':
            try:
                value = float(value)
            except ValueError:
                pass
            value = DateTime(value)

        elif proptype not in _STRING_PROPERTY_TYPES:
            try:
                value = eval(value)
            except NameError:
                self.log.exception( 'Error trying to evaluate %s', value )
                raise
            except SyntaxError:
                self.log.debug("Non-fatal SyntaxError at line %s while eval'ing '%s'" % (
                     linenum, value) )

        # Actually use the value
        if not obj.hasProperty(name):
            obj._setProperty(name, value, type=proptype, setter=setter)
        else:
            obj._updateProperty(name, value)

    def addLink(self, rel, objid):
        """
        Build list of links to form after all objects have been created
        make sure that we don't add other side of a bidirectional relation
        
        @param rel: relationship object
        @type rel: relation object
        @param objid: objid
        @type objid: string
        """
        self.links.append((rel.getPrimaryId(), objid))

    def processLinks(self):
        """
        Walk through all the links that we saved and link them up
        """
        for (relid, objid) in self.links:
            try:
                self.log.debug('Linking relation %s to object %s',
                               relid, objid)
                rel = getObjByPath(self.app, relid)
                obj = getObjByPath(self.app, objid)
                if not rel.hasobject(obj):
                    rel.addRelation(obj)
            except:
                self.log.critical('Failed linking relation %s to object %s' % (
                                  relid, objid))

    def buildOptions(self):
        """
        Command-line options specific to this command
        """
        ZCmdBase.buildOptions(self)
        self.parser.add_option('-i', '--infile', dest='infile',
                               help='Input file for import. Default is stdin'
                               )
        self.parser.add_option('--noindex', dest='noindex',
                               action='store_true', default=False,
                               help='Do not try to index the freshly loaded objects.'
                               )
        self.parser.add_option('--chunksize', dest='chunk_size',
                               help='Number of objects to commit at a time.',
                               type='int',
                               default=100
                               )
        self.parser.add_option(
            '-n',
            '--noCommit',
            dest='noCommit',
            action='store_true',
            default=0,
            help='Do not store changes to the DMD (ie for debugging purposes)',
            )

    def loadObjectFromXML(self, xmlfile=''):
        """
        This method can be used to load data for the root of Zenoss (default
        behavior) or it can be used to operate on a specific point in the
        Zenoss hierarchy (ZODB).
        
        Upon loading the XML file to be processed, the content of the XML file
        is handled by the methods in this class when called by xml.sax.parse().

        Reads from a file if xmlfile is specified, otherwise reads 
        from the command-line option --infile.  If no files are found from 
        any of these places, read from standard input. 
        
        @param xmlfile: Name of XML file to load, or file-like object
        @type xmlfile: string or file-like object
        """
        self.objstack = [self.app]
        self.links = []
        self.objectnumber = 0
        self.charvalue = ''
        if xmlfile and type(xmlfile) in types.StringTypes:
            self.infile = open(xmlfile)
        elif hasattr(xmlfile, 'read'):
            self.infile = xmlfile
        elif self.options.infile:
            self.infile = open(self.options.infile)
        else:
            self.infile = sys.stdin
        parser = make_parser()
        parser.setContentHandler(self)
        try:
            parser.parse(self.infile)
        except SAXParseException, ex:
            self.log.error("XML parse error at line %d column %d: %s",
                   ex.getLineNumber(), ex.getColumnNumber(),
                   ex.getMessage())
        finally:
            self.infile.close()

    def loadDatabase(self):
        """
        The default behavior of loadObjectFromXML() will be to use the Zope
        app object, and thus operatate on the whole of Zenoss.
        """
        self.loadObjectFromXML()

    def commit(self):
        """
        Wrapper around the Zope database commit()
        """
        trans = transaction.get()
        trans.note('Import from file %s using %s'
                    % (self.options.infile, self.__class__.__name__))
        trans.commit()
        if hasattr(self, 'connection'):
            # It's safe to call syncdb()
            self.syncdb()

class SpoofedOptions(object):
    """
    SpoofedOptions
    """

    def __init__(self):
        self.infile = ''
        self.noCommit = True
        self.noindex = True


class NoLoginImportRM(ImportRM):
    """
    An ImportRM that does not call the __init__ method on ZCmdBase
    """

    def __init__(self, app):
        """
        Initializer 
        
        @param app: app
        @type app: string
        """
        self.app = app
        ContentHandler.__init__(self)
        import logging
        self.log = logging.getLogger('zen.ImportRM')
        self.options = SpoofedOptions()


if __name__ == '__main__':
    im = ImportRM()
    im.loadDatabase()
