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