Improvements in the XML marshaller.

This commit is contained in:
Gaetan Delannay 2010-03-31 15:49:54 +02:00
parent 3f08cb989f
commit 9cff9df299
5 changed files with 85 additions and 45 deletions

View file

@ -340,16 +340,19 @@ class AbstractWrapper:
def export(self, at='string'): def export(self, at='string'):
'''Creates an "exportable", XML version of this object. If p_at is '''Creates an "exportable", XML version of this object. If p_at is
"string", this method returns the XML version. Else, (a) if not p_at, "string", this method returns the XML version, without the XML
the XML will be exported on disk, in the OS temp folder, with an prologue. Else, (a) if not p_at, the XML will be exported on disk,
ugly name; (b) else, it will be exported at path p_at.''' in the OS temp folder, with an ugly name; (b) else, it will be
exported at path p_at.'''
# Determine where to put the result # Determine where to put the result
toDisk = (at != 'string') toDisk = (at != 'string')
if toDisk and not at: if toDisk and not at:
at = getOsTempFolder() + '/' + self.o.UID() + '.xml' at = getOsTempFolder() + '/' + self.o.UID() + '.xml'
# Create the XML version of the object # Create the XML version of the object
xml = XmlMarshaller(cdata=True, dumpUnicode=True).marshall( marshaller = XmlMarshaller(cdata=True, dumpUnicode=True,
self.o, objectType='appy') dumpXmlPrologue=toDisk,
rootTag=self.klass.__name__)
xml = marshaller.marshall(self.o, objectType='appy')
# Produce the desired result # Produce the desired result
if toDisk: if toDisk:
f = file(at, 'w') f = file(at, 'w')

View file

@ -229,4 +229,21 @@ class Keywords:
op = ' %s ' % self.operator op = ' %s ' % self.operator
return op.join(self.keywords)+'*' return op.join(self.keywords)+'*'
return '' return ''
# ------------------------------------------------------------------------------
class FakeBrain:
'''This class behaves like a brain retrieved from a query to a ZCatalog. It
is used for representing a fake brain that was generated from a search in
a distant portal_catalog.'''
Creator = None
created = None
modified = None
review_state = None
def has_key(self, key): return hasattr(self, key)
def getPath(self): return self.path
def getURL(self, relative=0): return self.url
def _unrestrictedGetObject(self): return self
def pretty_title_or_id(self): return self.Title
def getObject(self, REQUEST=None): return self
def getRID(self): return self.url
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -18,6 +18,7 @@ mimeTypesExts = {
'image/jpeg' : 'jpg', 'image/jpeg' : 'jpg',
'image/gif' : 'gif' 'image/gif' : 'gif'
} }
xmlPrologue = '<?xml version="1.0" encoding="utf-8"?>'
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------
class UnmarshalledObject: class UnmarshalledObject:

View file

@ -94,4 +94,8 @@ def normalizeString(s, usage='fileName'):
res += char res += char
s = res s = res
return unicodedata.normalize('NFKD', s).encode("ascii","ignore") return unicodedata.normalize('NFKD', s).encode("ascii","ignore")
# ------------------------------------------------------------------------------
typeLetters = {'b': bool, 'i': int, 'j': long, 'f':float, 's':str, 'u':unicode,
'l': list, 'd': dict}
# ------------------------------------------------------------------------------ # ------------------------------------------------------------------------------

View file

@ -20,7 +20,7 @@
import xml.sax, difflib, types import xml.sax, difflib, types
from xml.sax.handler import ContentHandler, ErrorHandler from xml.sax.handler import ContentHandler, ErrorHandler
from xml.sax.xmlreader import InputSource from xml.sax.xmlreader import InputSource
from appy.shared import UnicodeBuffer from appy.shared import UnicodeBuffer, xmlPrologue
from appy.shared.errors import AppyError from appy.shared.errors import AppyError
# Error-related constants ------------------------------------------------------ # Error-related constants ------------------------------------------------------
@ -157,24 +157,30 @@ class XmlUnmarshaller(XmlParser):
If "object" is specified, it means that the tag contains sub-tags, each If "object" is specified, it means that the tag contains sub-tags, each
one corresponding to the value of an attribute for this object. one corresponding to the value of an attribute for this object.
if "tuple" is specified, it will be converted to a list.''' if "tuple" is specified, it will be converted to a list.'''
def __init__(self, klass=None, tagTypes={}, conversionFunctions={}): def __init__(self, classes={}, tagTypes={}, conversionFunctions={}):
XmlParser.__init__(self) XmlParser.__init__(self)
self.klass = klass # If a klass is given here, instead of creating # self.classes below is a dict whose keys are tag names and values are
# a root UnmarshalledObject instance, we will create an instance of this # Python classes. During the unmarshalling process, when an object is
# class (only if the root object is an object; this does not apply if # encountered, instead of creating an instance of UnmarshalledObject,
# it is a list or tuple; yes, technically the root tag can be a list or # we will create an instance of the class specified in self.classes.
# tuple even if it is silly because only one root tag can exist). But be # Root tag is named "xmlPythonData" by default by the XmlMarshaller.
# careful: we will not call the constructor of this class. We will # This will not work if the object in the specified tag is not a
# simply create an instance of UnmarshalledObject and dynamically change # UnmarshalledObject instance (ie it is a list or tuple or simple
# the class of the created instance to this class. # value). Note that we will not call the constructor of the specified
self.tagTypes = tagTypes # class. We will simply create an instance of UnmarshalledObject and
# dynamically change the class of the created instance to this class.
if not isinstance(classes, dict) and classes:
# The user may only need to define a class for the root tag
self.classes = {'xmlPythonData': classes}
else:
self.classes = classes
# We expect that the parsed XML file will follow some conventions # We expect that the parsed XML file will follow some conventions
# (ie, a tag that corresponds to a list has attribute type="list" or a # (ie, a tag that corresponds to a list has attribute type="list" or a
# tag that corresponds to an object has attribute type="object".). If # tag that corresponds to an object has attribute type="object".). If
# it is not the case of p_xmlContent, you can provide the missing type # it is not the case of p_xmlContent, you can provide the missing type
# information in p_tagTypes. Here is an example of p_tagTypes: # information in p_tagTypes. Here is an example of p_tagTypes:
# {"information": "list", "days": "list", "person": "object"}. # {"information": "list", "days": "list", "person": "object"}.
self.conversionFunctions = conversionFunctions self.tagTypes = tagTypes
# The parser assumes that data is represented in some standard way. If # The parser assumes that data is represented in some standard way. If
# it is not the case, you may provide, in this dict, custom functions # it is not the case, you may provide, in this dict, custom functions
# allowing to convert values of basic types (long, float, DateTime...). # allowing to convert values of basic types (long, float, DateTime...).
@ -187,6 +193,7 @@ class XmlUnmarshaller(XmlParser):
# and create a specific conversionFunction for it. This way, you can # and create a specific conversionFunction for it. This way, you can
# for example convert strings that have specific values (in this case, # for example convert strings that have specific values (in this case,
# knowing that the value is a 'string' is not sufficient). # knowing that the value is a 'string' is not sufficient).
self.conversionFunctions = conversionFunctions
def convertAttrs(self, attrs): def convertAttrs(self, attrs):
'''Converts XML attrs to a dict.''' '''Converts XML attrs to a dict.'''
@ -236,14 +243,15 @@ class XmlUnmarshaller(XmlParser):
def storeValue(self, name, value): def storeValue(self, name, value):
'''Stores the newly parsed p_value (contained in tag p_name) on the '''Stores the newly parsed p_value (contained in tag p_name) on the
current container in environment p_e.''' current container in environment self.env.'''
e = self.env e = self.env
# Change the class of the value if relevant
if (name in self.classes) and isinstance(value, UnmarshalledObject):
value.__class__ = self.classes[name]
# Where must I store this value? # Where must I store this value?
if not e.containerStack: if not e.containerStack:
# I store the object at the root of the web. # I store the object at the root of the web.
self.res = value self.res = value
if self.klass and isinstance(value, UnmarshalledObject):
self.res.__class__ = self.klass
else: else:
currentContainer = e.containerStack[-1] currentContainer = e.containerStack[-1]
if isinstance(currentContainer, list): if isinstance(currentContainer, list):
@ -252,7 +260,8 @@ class XmlUnmarshaller(XmlParser):
currentContainer.content += value currentContainer.content += value
else: else:
# Current container is an object # Current container is an object
if hasattr(currentContainer, name): if hasattr(currentContainer, name) and \
getattr(currentContainer, name):
# We have already encountered a sub-object with this name. # We have already encountered a sub-object with this name.
# Having several sub-objects with the same name, we will # Having several sub-objects with the same name, we will
# create a list. # create a list.
@ -326,19 +335,19 @@ class XmlMarshaller:
'''This class allows to produce a XML version of a Python object, which '''This class allows to produce a XML version of a Python object, which
respects some conventions as described in the doc of the corresponding respects some conventions as described in the doc of the corresponding
Unmarshaller (see above).''' Unmarshaller (see above).'''
xmlPrologue = '<?xml version="1.0" encoding="utf-8"?>'
xmlEntities = {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;', xmlEntities = {'<': '&lt;', '>': '&gt;', '&': '&amp;', '"': '&quot;',
"'": '&apos;'} "'": '&apos;'}
trueFalse = {True: 'True', False: 'False'} trueFalse = {True: 'True', False: 'False'}
sequenceTypes = (tuple, list) sequenceTypes = (tuple, list)
rootElementName = 'xmlPythonData'
fieldsToMarshall = 'all' fieldsToMarshall = 'all'
fieldsToExclude = [] fieldsToExclude = []
atFiles = ('image', 'file') # Types of archetypes fields that contain files. atFiles = ('image', 'file') # Types of archetypes fields that contain files.
def __init__(self, cdata=False, dumpUnicode=False, conversionFunctions={}): def __init__(self, cdata=False, dumpUnicode=False, conversionFunctions={},
'''If p_cdata is True, all string values will be dumped as XML CDATA.''' dumpXmlPrologue=True, rootTag='xmlPythonData'):
# If p_cdata is True, all string values will be dumped as XML CDATA.
self.cdata = cdata self.cdata = cdata
# If p_dumpUnicode is True, the result will be unicode.
self.dumpUnicode = dumpUnicode self.dumpUnicode = dumpUnicode
# The following dict stores specific conversion (=Python to XML) # The following dict stores specific conversion (=Python to XML)
# functions. A specific conversion function is useful when you are not # functions. A specific conversion function is useful when you are not
@ -350,6 +359,10 @@ class XmlMarshaller:
# being dumped, while the second one is the Python object or value to # being dumped, while the second one is the Python object or value to
# dump. # dump.
self.conversionFunctions = conversionFunctions self.conversionFunctions = conversionFunctions
# If dumpXmlPrologue is True, the XML prologue will be dumped.
self.dumpXmlPrologue = dumpXmlPrologue
# The name of the root tag
self.rootElementName = rootTag
def dumpString(self, res, s): def dumpString(self, res, s):
'''Dumps a string into the result.''' '''Dumps a string into the result.'''
@ -391,17 +404,17 @@ class XmlMarshaller:
res.write(v.encode('base64')) res.write(v.encode('base64'))
res.write('</part>') res.write('</part>')
def dumpValue(self, res, value, fieldType): def dumpValue(self, res, value, fieldType, isRef=False):
'''Dumps the XML version of p_value to p_res.''' '''Dumps the XML version of p_value to p_res.'''
# Use a custom function if one is defined for this type of value. # Use a custom function if one is defined for this type of value.
fType = value.__class__.__name__ className = value.__class__.__name__
if fType in self.conversionFunctions: if className in self.conversionFunctions:
self.conversionFunctions[fType](res, value) self.conversionFunctions[className](res, value)
return return
# Use a standard conversion else. # Use a standard conversion else.
if fieldType == 'file': if fieldType == 'file':
self.dumpFile(res, value) self.dumpFile(res, value)
elif fieldType == 'ref': elif isRef:
if value: if value:
if type(value) in self.sequenceTypes: if type(value) in self.sequenceTypes:
for elem in value: for elem in value:
@ -417,7 +430,7 @@ class XmlMarshaller:
self.dumpString(res, value) self.dumpString(res, value)
elif isinstance(value, bool): elif isinstance(value, bool):
res.write(self.trueFalse[value]) res.write(self.trueFalse[value])
elif self.isAnObject(value): elif fieldType == 'object':
if hasattr(value, 'absolute_url'): if hasattr(value, 'absolute_url'):
res.write(value.absolute_url()) res.write(value.absolute_url())
else: else:
@ -443,11 +456,12 @@ class XmlMarshaller:
elif isinstance(fieldValue, tuple): fType = 'tuple' elif isinstance(fieldValue, tuple): fType = 'tuple'
elif isinstance(fieldValue, list): fType = 'list' elif isinstance(fieldValue, list): fType = 'list'
elif fieldValue.__class__.__name__ == 'DateTime': fType = 'DateTime' elif fieldValue.__class__.__name__ == 'DateTime': fType = 'DateTime'
elif self.isAnObject(fieldValue): fType = 'object'
if fType: res.write(' type="%s"' % fType) if fType: res.write(' type="%s"' % fType)
# Dump other attributes if needed # Dump other attributes if needed
if type(fieldValue) in self.sequenceTypes: if type(fieldValue) in self.sequenceTypes:
res.write(' count="%d"' % len(fieldValue)) res.write(' count="%d"' % len(fieldValue))
if fieldType == 'file': if fType == 'file':
if hasattr(fieldValue, 'content_type'): if hasattr(fieldValue, 'content_type'):
res.write(' mimeType="%s"' % fieldValue.content_type) res.write(' mimeType="%s"' % fieldValue.content_type)
if hasattr(fieldValue, 'filename'): if hasattr(fieldValue, 'filename'):
@ -456,7 +470,7 @@ class XmlMarshaller:
res.write('"') res.write('"')
res.write('>') res.write('>')
# Dump the field value # Dump the field value
self.dumpValue(res, fieldValue, fieldType) self.dumpValue(res, fieldValue, fType, isRef=(fieldType=='ref'))
res.write('</'); res.write(fieldName); res.write('>') res.write('</'); res.write(fieldName); res.write('>')
def isAnObject(self, instance): def isAnObject(self, instance):
@ -487,8 +501,9 @@ class XmlMarshaller:
self.conversionFunctions.update(conversionFunctions) self.conversionFunctions.update(conversionFunctions)
# Create the buffer where the XML result will be dumped. # Create the buffer where the XML result will be dumped.
res = UnicodeBuffer() res = UnicodeBuffer()
# Dump the XML prologue # Dump the XML prologue if required
res.write(self.xmlPrologue) if self.dumpXmlPrologue:
res.write(xmlPrologue)
if self.isAnObject(instance): if self.isAnObject(instance):
# Determine object ID # Determine object ID
if objectType in ('archetype', 'appy'): if objectType in ('archetype', 'appy'):
@ -572,7 +587,7 @@ class XmlHandler(ContentHandler):
(like dates) from a file that need to be compared to another file.''' (like dates) from a file that need to be compared to another file.'''
def __init__(self, xmlTagsToIgnore, xmlAttrsToIgnore): def __init__(self, xmlTagsToIgnore, xmlAttrsToIgnore):
ContentHandler.__init__(self) ContentHandler.__init__(self)
self.res = u'<?xml version="1.0" encoding="UTF-8"?>' self.res = unicode(xmlPrologue)
self.namespaces = {} # ~{s_namespaceUri:s_namespaceName}~ self.namespaces = {} # ~{s_namespaceUri:s_namespaceName}~
self.indentLevel = -1 self.indentLevel = -1
self.tabWidth = 3 self.tabWidth = 3