1 | """
|
---|
2 | XML serializer.
|
---|
3 | """
|
---|
4 |
|
---|
5 | from django.conf import settings
|
---|
6 | from django.core.serializers import base
|
---|
7 | from django.db import models
|
---|
8 | from django.utils.xmlutils import SimplerXMLGenerator
|
---|
9 | from xml.dom import pulldom
|
---|
10 |
|
---|
11 | class Serializer(base.Serializer):
|
---|
12 | """
|
---|
13 | Serializes a QuerySet to XML.
|
---|
14 | """
|
---|
15 |
|
---|
16 | def start_serialization(self):
|
---|
17 | """
|
---|
18 | Start serialization -- open the XML document and the root element.
|
---|
19 | """
|
---|
20 | self.xml = SimplerXMLGenerator(self.stream, self.options.get("encoding", settings.DEFAULT_CHARSET))
|
---|
21 | self.xml.startDocument()
|
---|
22 | self.xml.startElement("django-objects", {"version" : "1.0"})
|
---|
23 |
|
---|
24 | def end_serialization(self):
|
---|
25 | """
|
---|
26 | End serialization -- end the document.
|
---|
27 | """
|
---|
28 | self.xml.endElement("django-objects")
|
---|
29 | self.xml.endDocument()
|
---|
30 |
|
---|
31 | def start_object(self, obj):
|
---|
32 | """
|
---|
33 | Called as each object is handled.
|
---|
34 | """
|
---|
35 | if not hasattr(obj, "_meta"):
|
---|
36 | raise base.SerializationError("Non-model object (%s) encountered during serialization" % type(obj))
|
---|
37 |
|
---|
38 | self.xml.startElement("object", {
|
---|
39 | "pk" : str(obj._get_pk_val()),
|
---|
40 | "model" : str(obj._meta),
|
---|
41 | })
|
---|
42 |
|
---|
43 | def end_object(self, obj):
|
---|
44 | """
|
---|
45 | Called after handling all fields for an object.
|
---|
46 | """
|
---|
47 | self.xml.endElement("object")
|
---|
48 |
|
---|
49 | def handle_field(self, obj, field):
|
---|
50 | """
|
---|
51 | Called to handle each field on an object (except for ForeignKeys and
|
---|
52 | ManyToManyFields)
|
---|
53 | """
|
---|
54 | self.xml.startElement("field", {
|
---|
55 | "name" : field.name,
|
---|
56 | "type" : field.get_internal_type()
|
---|
57 | })
|
---|
58 |
|
---|
59 | # Get a "string version" of the object's data (this is handled by the
|
---|
60 | # serializer base class). None is handled specially.
|
---|
61 | value = self.get_string_value(obj, field)
|
---|
62 | if value is not None:
|
---|
63 | self.xml.characters(str(value))
|
---|
64 | else:
|
---|
65 | self.xml.addQuickElement("None")
|
---|
66 |
|
---|
67 | self.xml.endElement("field")
|
---|
68 |
|
---|
69 | def handle_fk_field(self, obj, field):
|
---|
70 | """
|
---|
71 | Called to handle a ForeignKey (we need to treat them slightly
|
---|
72 | differently from regular fields).
|
---|
73 | """
|
---|
74 | self._start_relational_field(field)
|
---|
75 | related = getattr(obj, field.name)
|
---|
76 | if related is not None:
|
---|
77 | self.xml.characters(str(related._get_pk_val()))
|
---|
78 | else:
|
---|
79 | self.xml.addQuickElement("None")
|
---|
80 | self.xml.endElement("field")
|
---|
81 |
|
---|
82 | def handle_m2m_field(self, obj, field):
|
---|
83 | """
|
---|
84 | Called to handle a ManyToManyField. Related objects are only
|
---|
85 | serialized as references to the object's PK (i.e. the related *data*
|
---|
86 | is not dumped, just the relation).
|
---|
87 | """
|
---|
88 | self._start_relational_field(field)
|
---|
89 | for relobj in getattr(obj, field.name).iterator():
|
---|
90 | self.xml.addQuickElement("object", attrs={"pk" : str(relobj._get_pk_val())})
|
---|
91 | self.xml.endElement("field")
|
---|
92 |
|
---|
93 | def _start_relational_field(self, field):
|
---|
94 | """
|
---|
95 | Helper to output the <field> element for relational fields
|
---|
96 | """
|
---|
97 | self.xml.startElement("field", {
|
---|
98 | "name" : field.name,
|
---|
99 | "rel" : field.rel.__class__.__name__,
|
---|
100 | "to" : str(field.rel.to._meta),
|
---|
101 | })
|
---|
102 |
|
---|
103 | class Deserializer(base.Deserializer):
|
---|
104 | """
|
---|
105 | Deserialize XML.
|
---|
106 | """
|
---|
107 |
|
---|
108 | def __init__(self, stream_or_string, **options):
|
---|
109 | super(Deserializer, self).__init__(stream_or_string, **options)
|
---|
110 | self.encoding = self.options.get("encoding", settings.DEFAULT_CHARSET)
|
---|
111 | self.event_stream = pulldom.parse(self.stream)
|
---|
112 |
|
---|
113 | def next(self):
|
---|
114 | for event, node in self.event_stream:
|
---|
115 | if event == "START_ELEMENT" and node.nodeName == "object":
|
---|
116 | self.event_stream.expandNode(node)
|
---|
117 | return self._handle_object(node)
|
---|
118 | raise StopIteration
|
---|
119 |
|
---|
120 | def _handle_object(self, node):
|
---|
121 | """
|
---|
122 | Convert an <object> node to a DeserializedObject.
|
---|
123 | """
|
---|
124 | # Look up the model using the model loading mechanism. If this fails, bail.
|
---|
125 | Model = self._get_model_from_node(node, "model")
|
---|
126 |
|
---|
127 | # Start building a data dictionary from the object. If the node is
|
---|
128 | # missing the pk attribute, bail.
|
---|
129 | pk = node.getAttribute("pk")
|
---|
130 | if not pk:
|
---|
131 | raise base.DeserializationError("<object> node is missing the 'pk' attribute")
|
---|
132 | data = {Model._meta.pk.name : pk}
|
---|
133 |
|
---|
134 | # Also start building a dict of m2m data (this is saved as
|
---|
135 | # {m2m_accessor_attribute : [list_of_related_objects]})
|
---|
136 | m2m_data = {}
|
---|
137 |
|
---|
138 | # Deseralize each field.
|
---|
139 | for field_node in node.getElementsByTagName("field"):
|
---|
140 | # If the field is missing the name attribute, bail (are you
|
---|
141 | # sensing a pattern here?)
|
---|
142 | field_name = field_node.getAttribute("name")
|
---|
143 | if not field_name:
|
---|
144 | raise base.DeserializationError("<field> node is missing the 'name' attribute")
|
---|
145 |
|
---|
146 | # Get the field from the Model. This will raise a
|
---|
147 | # FieldDoesNotExist if, well, the field doesn't exist, which will
|
---|
148 | # be propagated correctly.
|
---|
149 | field = Model._meta.get_field(field_name)
|
---|
150 |
|
---|
151 | # As is usually the case, relation fields get the special treatment.
|
---|
152 | if field.rel and isinstance(field.rel, models.ManyToManyRel):
|
---|
153 | m2m_data[field.name] = self._handle_m2m_field_node(field_node)
|
---|
154 | elif field.rel and isinstance(field.rel, models.ManyToOneRel):
|
---|
155 | data[field.name] = self._handle_fk_field_node(field_node)
|
---|
156 | else:
|
---|
157 | value = field.to_python(getInnerText(field_node).strip().encode(self.encoding))
|
---|
158 | data[field.name] = value
|
---|
159 |
|
---|
160 | # Return a DeserializedObject so that the m2m data has a place to live.
|
---|
161 | return base.DeserializedObject(Model(**data), m2m_data)
|
---|
162 |
|
---|
163 | def _handle_fk_field_node(self, node):
|
---|
164 | """
|
---|
165 | Handle a <field> node for a ForeignKey
|
---|
166 | """
|
---|
167 | # Try to set the foreign key by looking up the foreign related object.
|
---|
168 | # If it doesn't exist, set the field to None (which might trigger
|
---|
169 | # validation error, but that's expected).
|
---|
170 | RelatedModel = self._get_model_from_node(node, "to")
|
---|
171 | return RelatedModel.objects.get(pk=getInnerText(node).strip().encode(self.encoding))
|
---|
172 |
|
---|
173 | def _handle_m2m_field_node(self, node):
|
---|
174 | """
|
---|
175 | Handle a <field> node for a ManyToManyField
|
---|
176 | """
|
---|
177 | # Load the related model
|
---|
178 | RelatedModel = self._get_model_from_node(node, "to")
|
---|
179 |
|
---|
180 | # Look up all the related objects. Using the in_bulk() lookup ensures
|
---|
181 | # that missing related objects don't cause an exception
|
---|
182 | related_ids = [c.getAttribute("pk").encode(self.encoding) for c in node.getElementsByTagName("object")]
|
---|
183 | return RelatedModel._default_manager.in_bulk(related_ids).values()
|
---|
184 |
|
---|
185 | def _get_model_from_node(self, node, attr):
|
---|
186 | """
|
---|
187 | Helper to look up a model from a <object model=...> or a <field
|
---|
188 | rel=... to=...> node.
|
---|
189 | """
|
---|
190 | model_identifier = node.getAttribute(attr)
|
---|
191 | if not model_identifier:
|
---|
192 | raise base.DeserializationError(
|
---|
193 | "<%s> node is missing the required '%s' attribute" \
|
---|
194 | % (node.nodeName, attr))
|
---|
195 | try:
|
---|
196 | Model = models.get_model(*model_identifier.split("."))
|
---|
197 | except TypeError:
|
---|
198 | Model = None
|
---|
199 | if Model is None:
|
---|
200 | raise base.DeserializationError(
|
---|
201 | "<%s> node has invalid model identifier: '%s'" % \
|
---|
202 | (node.nodeName, model_identifier))
|
---|
203 | return Model
|
---|
204 |
|
---|
205 |
|
---|
206 | def getInnerText(node):
|
---|
207 | """
|
---|
208 | Get all the inner text of a DOM node (recursively).
|
---|
209 | """
|
---|
210 | # inspired by http://mail.python.org/pipermail/xml-sig/2005-March/011022.html
|
---|
211 | inner_text = []
|
---|
212 | for child in node.childNodes:
|
---|
213 | if child.nodeType == child.TEXT_NODE or child.nodeType == child.CDATA_SECTION_NODE:
|
---|
214 | inner_text.append(child.data)
|
---|
215 | elif child.nodeType == child.ELEMENT_NODE:
|
---|
216 | inner_text.extend(getInnerText(child))
|
---|
217 | else:
|
---|
218 | pass
|
---|
219 | return "".join(inner_text)
|
---|