'''

Read in a configuration file and output one or more configuration files, each

of which contain a single Motion, Axis, Filter and Motor object. Each object is

renamed as "Object 0". For example, the file for Axis 2 would include Motion 2

(renamed "Motion 0"), Axis 2 (renamed "Axis 0"), Filter 2 (renamed "Filter 0")

and Motor 2 (renamed "Motor 0"). All non-portable configuration elements, such

as firmware addresses and object mappings, are stripped from the output.

'''

 

import sys

 

# The following is necessary in order to handle UNICODE characters properly.

if not hasattr(sys, 'setdefaultencoding'):

   reload(sys)

 

if hasattr(sys, 'setdefaultencoding'):

   sys.setdefaultencoding("utf-8")

   del sys.setdefaultencoding

 

import types

import sets

import gc

 

from xml.sax import saxutils, make_parser, SAXException, SAXParseException

from xml.dom import minidom

from xml.parsers.expat import ExpatError

 

_usage = '''%s [inputFile]

  inputFile     Optional meiConfig configuration file. If omitted, input is

                taken from stdin.

 

Transform the input file, into multiple configuration files. One configuration

file will be created for each Motion/Axis/Filter/Motor set of objects. Each

object is renamed as "Object 0". For example, the file for Axis 2 would

include Motion 2, Axis 2, Filter 2 and Motor 2.

'''

 

################################################

## classes

 

class Splitter(saxutils.XMLGenerator):

   '''This is a general purpose class that will create a DOM tree from an input

XML file. The dom tree will contain only the elements (and sub-elements)

listed in the elemPathList creation argument.

 

The DOM elements created for the items listed in elemPathList are not

attached to their parents, making it easier to create multiple files

containing only one of each type of object.

'''

   def __init__(self

       , elemPathList

       , includeElemPathList = []

       , excludeElemPathList = []

       , encoding = "iso-8859-1"

       , warningWriter = sys.stderr

   ):

       self.warningWriter = warningWriter

       

       saxutils.XMLGenerator.__init__(self, NullWriter(), encoding)

      

       self.elemPathList = elemPathList

       self.elemPathSet = sets.Set(elemPathList)

       self.includeElemPathSet = sets.Set(includeElemPathList)

       self.excludeElemPathSet = sets.Set(excludeElemPathList)

       self.elemPathSetAll = self.includeElemPathSet | self.elemPathSet

       self.nodeMap = {}

      

       self.elementStack = []

       self.nodeTypeStack = []

       self.idStack = []

       self.domElemStack = []

       self.dom = None

  

   def characters(self, content):

       pass

  

   def documentWrite(self, writer):

       self.dom.writexml(writer, indent = '\t', addindent = '\t', newl = '\n')

  

   def elementAppend(self, domElemParent, domElem, idPath, elemPath):

       # Don't attach the nodes that are going to be multiplexed out

       if elemPath not in self.elemPathSet:

           domElemParent.appendChild(domElem)

 

   def elementCreate(self, name, attrs, idPath, elemPath):

       domElem = self.dom.createElement(name)

       for (attr, value) in attrs.items():

           domElem.setAttribute(attr, value)

      

       return domElem

  

   def endElement(self, name):

       elementName = self.elementStack.pop(-1)

       nodeType = self.nodeTypeStack.pop(-1)

       id = self.idStack.pop(-1)

       domElem = self.domElemStack.pop(-1)

  

   def endElementNS(self, name, qname):

       pass

  

   def ignorableWhitespace(self, content):

       pass

  

   def processingInstruction(self, target, data):

       pass

 

   def startDocument(self):

       dom = minidom.Document()

       self.domElemStack.append(dom)

       self.dom = dom

  

   def startElement(self, name, attrs):

       self.elementStack.append(name)

       nodeType = attrs.get(attrNameNodeType, None)

       if nodeType is None:

           raise InvalidXMLFile(

               'Invalid file: no nodeType for %s' % (name, )

           )

      

       nodeTypeStack = self.nodeTypeStack

       id = name

       if attrNameID in attrs:

           id = attrs[attrNameID]

       parentNodeType = None

       if len(nodeTypeStack):

           parentNodeType = nodeTypeStack[-1]

       if parentNodeType == nodeTypePROPERTY_LIST:

           n = attrs[attrNameListElem]

           id += ' ' + n

       self.idStack.append(id)

       idPath = '.'.join(self.idStack)

      

       nodeTypeStack.append(nodeType)

       elemPath = '.'.join(self.elementStack)

      

       domElem = None

       domElemStack = self.domElemStack

       domElemParent = domElemStack[-1]

      

       elemInPathSet = elemPath in self.elemPathSetAll

 

if not elemInPathSet:

           found = False

           for p in self.elemPathList:

               if p.find(elemPath) == 0 or elemPath.find(p) == 0:

                   found = True

                   break

          

           if not found:

               name = None

      

       if elemPath in self.excludeElemPathSet:

           name = None

      

       if name is not None and domElemParent is not None:

           domElem = self.elementCreate(name, attrs, idPath, elemPath)

           self.elementAppend(domElemParent, domElem, idPath, elemPath)

          

           if elemInPathSet:

               self.nodeMap.setdefault(idPath, []).append(

                   (domElemParent, domElem)

               )

      

       domElemStack.append(domElem)

  

   def startElementNS(self, name, qname, attrs):

       pass

  

   def unlink(self):

       if self.dom is not None:

           # Attach all nodes so that they can all be destroyed

           for elemDataList in self.nodeMap.values():

               for elemData in elemDataList:

                   domElemParent, domElem = elemData

                   domElemParent.appendChild(domElem)

          

           self.dom.unlink()

           

           self.dom = None

           self.nodeMap = {}

 

class NullWriter(object):

   def write(self, text):

       pass

 

################################################

## Exceptions

_Exception = Exception

 

class Exception(_Exception):

   ''' This is a base class for all exceptions generated in this module. '''

   valueTypes = types.StringTypes

   def __init__(self, value):

       assert type(value) in self.valueTypes

       self.value = value

       _Exception.__init__(self, value)

 

def __str__(self):

       return self.value

 

class FileIoError(Exception):

   pass

 

class InvalidXMLFile(Exception):

   pass

 

class ParserError(Exception):

   pass

 

################################################

## Miscellaneous Constants

FILENAME_PREFIX = ',mafm'

MAX_OBJ_COUNT = 64

 

# Set MEMORY_CHECK to True in order to check for memory leaks

MEMORY_CHECK = False

 

# Names for the various node types in a .XMC file.

nodeTypePROPERTY = 'property'

nodeTypePROPERTY_LIST = 'propertyList'

nodeTypeOBJECT = 'object'

nodeTypeOBJECT_LIST = 'objectList'

 

# Names for various node attributes in a .XMC file.

attrNameListElem = '_n'

attrNameListLength = 'length'

attrNameValue = 'value'

attrNameValueList = 'valueList'

attrNameNodeType = 'nodeType'

attrNameObjectType = 'objectType'

attrNameHex = 'hex'

attrNameID = 'id'

 

################################################

## Functions

 

def main(argv):

   # Create an instance of a Splitter class and use it to create a DOM tree

   # of the input file. Only the elements necessary to create the paths listed

   # in elemPathList will attached to the DOM tree.

   if MEMORY_CHECK:

       # Check for memory leaks

       gc.enable()

       gc.set_debug(gc.DEBUG_LEAK)

  

   # Argument checking

   lenArgv = len(argv)

   if lenArgv > 2:

       sys.stderr.write(_usage)

       sys.exit(-1)

  

   if lenArgv <= 1:

 

input = sys.stdin

   else:

       input = argv[1]

  

   # elemPathList:

   # Paths to element types that are to be included in the output files

   elemPathList = [

       'MeiXmlConfig.Control.AxisList.Axis',

       'MeiXmlConfig.Control.FilterList.Filter',

       'MeiXmlConfig.Control.MotorList.Motor',

       'MeiXmlConfig.Control.MotionList.Motion',

   ]

  

   # idPathList:

   # Paths used to access nodes stored in Splitter.nodeMap

   idPathList = [

       'MeiXmlConfig.Control 0.AxisList.Axis %d',

       'MeiXmlConfig.Control 0.FilterList.Filter %d',

       'MeiXmlConfig.Control 0.MotorList.Motor %d',

       'MeiXmlConfig.Control 0.MotionList.Motion %d',

   ]

  

   # includeElemPathList:

   # Paths to elements that are to be included in all the output files.

   includeElemPathList = [

       'MeiXmlConfig.MPI_VERSION_INTERFACE',

       'MeiXmlConfig.BUILD_NO',

   ]

   # excludeElemPathList:

   # Paths to elements that are to be excluded from all the output files.

   excludeElemPathList = [

       # exclude object mapping and firmware address elements

       # Axis:

       'MeiXmlConfig.Control.AxisList.Axis.MPIAxisConfig.feedbackPtr',

       'MeiXmlConfig.Control.AxisList.Axis.MPIAxisConfig.gear.master.data.custom.address',

       'MeiXmlConfig.Control.AxisList.Axis.MPIAxisConfig.master.address',

       # Filter:

       'MeiXmlConfig.Control.FilterList.Filter.MPIFilterConfig.velPtr',

       # Motor:

       'MeiXmlConfig.Control.MotorList.Motor.MPIMotorConfig.commutation.input.input.ptr',

       'MeiXmlConfig.Control.MotorList.Motor.MPIMotorConfig.statusOutput.outPtr',

   ]

   try:

       splitter = Parse(input

           , contentHandlerClass = Splitter

           , elemPathList = elemPathList

           , includeElemPathList = includeElemPathList

           , excludeElemPathList = excludeElemPathList

       )

 

   except Exception, exc:

       sys.stderr.write('Transformation failed: %s' % (str(exc), ) + '\n')

   else:

       errMsg = None

       objCount = 0

       # Calculate objCount, the highest index for the objects to be split out

       # into separate files.

       # Also look for multiple objects with the same ID

       for objNdx in range(MAX_OBJ_COUNT):

           if errMsg is not None:

               break

           for idPathFmt in idPathList:

               idPath = idPathFmt % objNdx

               objList = splitter.nodeMap.get(idPath, [])

               lenObjList = len(objList)

               if lenObjList == 0:

                   # Stop as soon as there is no object for the objNdx

                   errMsg = ''

               elif lenObjList == 1:

                   objCount = objNdx

               else:

                   errMsg = 'Multiple objects with ID %s' % idPath

                   break

       if errMsg == '' or errMsg is None:

           for objNdx in range(objCount):

               # Create the output file

               source = '%s_%d.xmc' % (FILENAME_PREFIX, objNdx)

               try:

                   writer = open(source, 'w')

               except IOError, exc:

                   errMsg = '%s: %s' % (exc.strerror, source)

                   sys.stderr.write(errMsg + '\n')

                   continue

              

               # Link the nodes for all objects associated with objNdx up with

               # their parent nodes.

               for idPathFmt in idPathList:

                   idPath = idPathFmt % objNdx

                   domElemParent, domElem  = splitter.nodeMap[idPath][0]

                   domElemParent.appendChild(domElem)

                  

                   # Change the id of the element to be 0

                   idNameFmt = idPathFmt.split('.')[-1]

                   idNew = idNameFmt % 0

                   domElem.setAttribute(attrNameID, idNew)

              

               # Save the new configuration file

               print 'Saving %s' % source

               splitter.documentWrite(writer)

               writer.close()

              

               # Undo the linking performed above

               for idPathFmt in idPathList:

                   idPath = idPathFmt % objNdx

                   domElemParent, domElem  = splitter.nodeMap[idPath][0]

                   domElemParent.removeChild(domElem)

       else:

           sys.stderr.write(errMsg + '\n')

      

       # Free up memory attached to splitter

       splitter.unlink()

  

   if MEMORY_CHECK:

       # Check for memory leaks

       memLeakDump(140)

 

def memLeakDump(lenLine = 80):

   '''Use the gc module to check for memory leaks.'''

   assert lenLine > 4

   if not MEMORY_CHECK:

       return

  

   gc.collect()

   if len(gc.garbage):

       print 'GARBAGE'

       for x in gc.garbage:

           s = str(x)

           if len(s) > lenLine: s = s[:lenLine - 3] + '...'

           print '%s: %s' % (`type(x)`, s)

 

def Parse(input

   , contentHandlerClass = Splitter

   , *args, **kwargs

):

   '''Parse the input using the given contentHandlerClass. Exceptions

generated by the parser are consolidated into exceptions defined in this

module so that they can be handled by a single try-except statement.'''

   try:

       parser = make_parser()

   except SAXException, exc:

       raise ParserError(`exc`)

  

   contentHandler = contentHandlerClass(*args, **kwargs)

   try:

       try:

           parser.setContentHandler(contentHandler)

           parser.parse(input)

       except SAXParseException, exc:

           contentHandler.unlink()

           raise InvalidXMLFile(

               'Parser error at line %d: %s' % (

                   exc.getLineNumber(),

                   exc.getMessage(),

               )

           )

       except (SAXException, ExpatError), exc:

           contentHandler.unlink()

           raise InvalidXMLFile(`exc`)

       except IOError, exc:

           raise FileIoError('%s: %s' % (exc.strerror, input))

   finally:

       # There is a bug in the saxutils.XMLGenerator class that causes a

       # memory leak if there is an exception while parsing. The following

       # statement breaks the cycle created by expat handlers pointing to

       # parser methods.

       parser._parser = None

  

   return contentHandler

 

def _test(testFile):

   '''Parse testFile.'''

   import os

   op = os.path

  

   main([

       sys.argv[0],

       testFile

   ])

 

if __name__ == '__main__':

   # To test, set "test" to True and set testFile to the path to a test .XMC

   # file.

   test = False

   testFile = r"testDir/meiConfig.xmc"

   if test:

       _test(testFile)

   else:

       main(sys.argv)