root/gaphor/tags/release-0.10.2/gaphor/storage.py

Revision 1169, 17.3 kB (checked in by arjanmol, 2 years ago)

fixed post-load bug

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 # vim: sw=4:et
2 """Load and save Gaphor models to Gaphors own XML format.
3
4 Three functions are exported:
5 load(filename)
6     load a model from a file
7 save(filename)
8     store the current model in a file
9 verify(filename)
10     check the validity of the file (this does not tell us
11     we have a valid model, just a valid file).
12 """
13
14 from __future__ import generators
15
16 from cStringIO import StringIO
17 from xml.sax.saxutils import escape
18 import types
19 import sys
20 import os.path
21 import gc
22
23 import gaphas
24
25 from gaphor import UML
26 from gaphor import parser
27 from gaphor import diagram
28 from gaphor.diagram import items
29 from gaphor import resource
30 from gaphor.i18n import _
31 #from gaphor.misc.xmlwriter import XMLWriter
32
33 __all__ = [ 'load', 'save' ]
34
35 FILE_FORMAT_VERSION = '3.0'
36
37 def save(writer=None, factory=None, status_queue=None):
38     for status in save_generator(writer, factory):
39         if status_queue:
40             status_queue(status)
41
42 def save_generator(writer=None, factory=None):
43     """Save the current model using @writer, which is a
44     gaphor.misc.xmlwriter.XMLWriter instance (or at least a SAX serializer
45     with CDATA support).
46     """
47     # Make bool work for Python 2.2
48     bool_ = type(bool(0))
49
50     def save_reference(name, value):
51         """Save a value as a reference to another element in the model.
52         This applies to both UML as well as canvas items.
53         """
54         # Save a reference to the object:
55         if value.id: #, 'Referenced element %s has no id' % value
56             writer.startElement(name, {})
57             writer.startElement('ref', { 'refid': value.id })
58             writer.endElement('ref')
59             writer.endElement(name)
60
61     def save_collection(name, value):
62         """Save a list of references.
63         """
64         if len(value) > 0:
65             writer.startElement(name, {})
66             writer.startElement('reflist', {})
67             for v in value:
68                 #save_reference(name, v)
69                 if v.id: #, 'Referenced element %s has no id' % v
70                     writer.startElement('ref', { 'refid': v.id })
71                     writer.endElement('ref')
72             writer.endElement('reflist')
73             writer.endElement(name)
74
75     def save_value(name, value):
76         """Save a value (attribute).
77         If the value is a string, it is saves as a CDATA block.
78         """
79         if value is not None:
80             writer.startElement(name, {})
81             writer.startElement('val', {})
82             if isinstance(value, types.StringTypes):
83                 writer.startCDATA()
84                 writer.characters(value)
85                 writer.endCDATA()
86             elif isinstance(value, bool_):
87                 # Write booleans as 0/1.
88                 writer.characters(str(int(value)))
89             else:
90                 writer.characters(str(value))
91             writer.endElement('val')
92             writer.endElement(name)
93
94     def save_element(name, value):
95         """Save attributes and references from items in the gaphor.UML module.
96         A value may be a primitive (string, int), a gaphor.UML.collection
97         (which contains a list of references to other UML elements) or a
98         gaphas.Canvas (which contains canvas items).
99         """
100         #log.debug('saving element: %s|%s %s' % (name, value, type(value)))
101         if isinstance (value, (UML.Element, gaphas.Item)):
102             save_reference(name, value)
103         elif isinstance(value, UML.collection):
104             save_collection(name, value)
105         elif isinstance(value, gaphas.Canvas):
106             writer.startElement('canvas', {})
107             value.save(save_canvasitem)
108             writer.endElement('canvas')
109         else:
110             save_value(name, value)
111
112     def save_canvasitem(name, value, reference=False):
113         """Save attributes and references in a gaphor.diagram.* object.
114         The extra attribute reference can be used to force UML
115         """
116         #log.debug('saving canvasitem: %s|%s %s' % (name, value, type(value)))
117         if reference:
118             save_reference(name, value)
119         elif isinstance(value, UML.collection):
120             save_collection(name, value)
121         elif isinstance(value, gaphas.Item):
122             writer.startElement('item', { 'id': value.id,
123                                           'type': value.__class__.__name__ })
124             value.save(save_canvasitem)
125             writer.endElement('item')
126         elif isinstance(value, UML.Element):
127             save_reference(name, value)
128         else:
129             save_value(name, value)
130
131     if not factory:
132         factory = resource(UML.ElementFactory)
133
134     writer.startDocument()
135     writer.startElement('gaphor', { 'version': FILE_FORMAT_VERSION,
136                                     'gaphor-version': resource('Version') })
137
138     size = factory.size()
139     n = 0
140     for e in factory.values():
141         clazz = e.__class__.__name__
142         assert e.id
143         writer.startElement(clazz, { 'id': str(e.id) })
144         e.save(save_element)
145         writer.endElement(clazz)
146         n += 1
147         if n % 25 == 0:
148             yield (n * 100) / size
149
150     writer.endElement('gaphor')
151     writer.endDocument()
152
153
154 def load_elements(elements, factory, status_queue=None):
155     for status in load_elements_generator(elements, factory):
156         if status_queue:
157             status_queue(status)
158
159 def load_elements_generator(elements, factory, gaphor_version=None):
160     """Load a file and create a model if possible.
161     Exceptions: IOError, ValueError.
162     """
163     # TODO: restructure loading code, first load model, then add canvas items
164     log.debug(_('Loading %d elements...') % len(elements))
165
166     # The elements are iterated three times:
167     size = len(elements) * 3
168     def update_status_queue(_n=[0]):
169         n = _n[0] = _n[0] + 1
170         if n % 10 == 0:
171             return (n * 100) / size
172
173     #log.info('0%')
174
175     # Fix version inconsistencies
176     version_0_6_2(elements, factory, gaphor_version)
177     version_0_7_2(elements, factory, gaphor_version)
178     version_0_9_0(elements, factory, gaphor_version)
179
180     #log.debug("Still have %d elements" % len(elements))
181
182     # First create elements and canvas items in the factory
183     # The elements are stored as attribute 'element' on the parser objects:
184     for id, elem in elements.items():
185         yield update_status_queue()
186         if isinstance(elem, parser.element):
187             cls = getattr(UML, elem.type)
188             #log.debug('Creating UML element for %s (%s)' % (elem, elem.id))
189             elem.element = factory.create_as(cls, id)
190             if elem.canvas:
191                 elem.element.canvas.block_updates = True
192         elif isinstance(elem, parser.canvasitem):
193             cls = getattr(items, elem.type)
194             #log.debug('Creating canvas item for %s (%s)' % (elem, elem.id))
195             elem.element = diagram.create_as(cls, id)
196         else:
197             raise ValueError, 'Item with id "%s" and type %s can not be instantiated' % (id, type(elem))
198
199     #log.info('0% ... 33%')
200
201     #log.debug("Still have %d elements" % len(elements))
202
203     # load attributes and create references:
204     for id, elem in elements.items():
205         yield update_status_queue()
206         # Ensure that all elements have their element instance ready...
207         assert hasattr(elem, 'element')
208
209         # establish parent/child relations on canvas items:
210         if isinstance(elem, parser.element) and elem.canvas:
211             for item in elem.canvas.canvasitems:
212                 assert item in elements.values(), 'Item %s (%s) is a canvas item, but it is not in the parsed objects table' % (item, item.id)
213                 elem.element.canvas.add(item.element)
214
215         # Also create nested canvas items:
216         if isinstance(elem, parser.canvasitem):
217             for item in elem.canvasitems:
218                 assert item in elements.values(), 'Item %s (%s) is a canvas item, but it is not in the parsed objects table' % (item, item.id)
219                 elem.element.canvas.add(item.element, parent=elem.element)
220
221         # load attributes and references:
222         for name, value in elem.values.items():
223             try:
224                 elem.element.load(name, value)
225             except:
226                 log.error('Loading value %s (%s) for element %s failed.' % (name, value, elem.element))
227                 raise
228
229         for name, refids in elem.references.items():
230             if type(refids) == type([]):
231                 for refid in refids:
232                     try:
233                         ref = elements[refid]
234                     except:
235                         raise ValueError, 'Invalid ID for reference (%s) for element %s.%s' % (refid, elem.type, name)
236                     else:
237                         try:
238                             elem.element.load(name, ref.element)
239                         except:
240                             log.error('Loading %s.%s with value %s failed' % (type(elem.element).__name__, name, ref.element.id))
241                             raise
242             else:
243                 try:
244                     ref = elements[refids]
245                 except:
246                     raise ValueError, 'Invalid ID for reference (%s)' % refids
247                 else:
248                     try:
249                         elem.element.load(name, ref.element)
250                     except:
251                         log.error('Loading %s.%s with value %s failed' % (type(elem.element).__name__, name, ref.element.id))
252                         raise
253
254     # Fix version inconsistencies
255     version_0_5_2(elements, factory, gaphor_version)
256     version_0_7_1(elements, factory, gaphor_version)
257
258     # Before version 0.7.2 there was only decision node (no merge nodes).
259     # This node could have many incoming and outgoing flows (edges).
260     # According to UML specification decision node has no more than one
261     # incoming node.
262     #
263     # Now, we have implemented merge node, which can have many incoming
264     # flows. We also support combining of decision and merge nodes as
265     # described in UML specification.
266     #
267     # Data model, loaded from file, is updated automatically, so there is
268     # no need for special function.
269
270     # do a postload:
271     for id, elem in elements.items():
272         yield update_status_queue()
273         elem.element.postload()
274
275     # Unlock canvas's for updates
276     for id, elem in elements.items():
277         if isinstance(elem, parser.element) and elem.canvas:
278             elem.element.canvas.block_updates = False
279
280     factory.notify_model()
281
282
283 def load(filename, factory=None, status_queue=None):
284     """Load a file and create a model if possible.
285     Optionally, a status queue function can be given, to which the
286     progress is written (as status_queue(progress)).
287     Exceptions: GaphorError.
288     """
289     for status in load_generator(filename, factory):
290         if status_queue:
291             status_queue(status)
292
293 def load_generator(filename, factory=None):
294     """Load a file and create a model if possible.
295     This function is a generator. It will yield values from 0 to 100 (%)
296     to indicate its progression.
297
298     Exceptions: GaphorError.
299     """
300     log.info('Loading file %s' % os.path.basename(filename))
301     try:
302         # Use the incremental parser and yield the percentage of the file.
303         loader = parser.GaphorLoader()
304         for percentage in parser.parse_generator(filename, loader):
305             pass
306             if percentage:
307                 yield percentage / 2
308             else:
309                 yield percentage
310         elements = loader.elements
311         gaphor_version = loader.gaphor_version
312         #elements = parser.parse(filename)
313         #yield 100
314     except Exception, e:
315         log.error('File could no be parsed', e)
316         raise
317
318     try:
319         if not factory:
320             factory = resource(UML.ElementFactory)
321         factory.flush()
322         gc.collect()
323         log.info("Read %d elements from file" % len(elements))
324         for percentage in load_elements_generator(elements, factory, gaphor_version):
325             pass
326             if percentage:
327                 yield percentage / 2 + 50
328             else:
329                 yield percentage
330         gc.collect()
331         yield 100
332     except Exception, e:
333         log.info('file %s could not be loaded' % filename, e)
334         raise
335
336
337 def version_0_9_0(elements, factory, gaphor_version):
338     """
339     Before 0.9.0, we used DiaCanvas2 as diagram widget in the GUI. As of 0.9.0
340     Gaphas was introduced. Some properties of <item /> elements have changed,
341     renamed or been removed at all.
342
343     This function is called before the actual elements are constructed.
344     """
345     if tuple(map(int, gaphor_version.split('.'))) < (0, 9, 0):
346         for elem in elements.values():
347             try:
348                 if type(elem) is parser.canvasitem:
349                     # Rename affine to matrix
350                     if elem.values.get('affine'):
351                         elem.values['matrix'] = elem.values['affine']
352                         del elem.values['affine']
353                     # No more 'color' attribute:
354                     if elem.values.get('color'):
355                         del elem.values['color']
356
357             except Exception, e:
358                 log.error('Error while updating taggedValues', e)
359
360 def version_0_7_2(elements, factory, gaphor_version):
361     """
362     Before 0.7.2, only Property and Parameter elements had taggedValues.
363     Since 0.7.2 all NamedElements are able to have taggedValues. However,
364     the multiplicity of taggedValue has changed from 0..1 to *, so all elements
365     should be converted to a list.
366     """
367     from gaphor.misc.uniqueid import generate_id
368
369     if tuple(map(int, gaphor_version.split('.'))) < (0, 7, 2):
370         for elem in elements.values():
371             try:
372                 if type(elem) is parser.element \
373                    and elem.type in ('Property', 'Parameter') \
374                    and elem.taggedValue:
375                     tvlist = []
376                     tv = elements[elem.taggedValue]
377                     if tv.value:
378                         for t in map(str.strip, str(tv.value).split(',')):
379                             #log.debug("Tagged value: %s" % t)
380                             newtv = parser.element(generate_id(),
381                                                    'LiteralSpecification')
382                             newtv.values['value'] = t
383                             elements[newtv.id] = newtv
384                             tvlist.append(newtv.id)
385                         elem.references['taggedValue'] = tvlist
386             except Exception, e:
387                 log.error('Error while updating taggedValues', e)
388
389
390 def version_0_7_1(elements, factory, gaphor_version):
391     """
392     Before version 0.7.1, there were two states for association
393     navigability (in terms of UML 2.0): unknown and navigable.
394     In case of unknown navigability Property.owningAssociation was set.
395
396     Now, we have three states: unknown, non-navigable and navigable.
397     In case of unknown navigability the Property.owningAssociation
398     should not be set.
399     """
400     def fix(end1, end2):
401         if isinstance(end2.type, UML.Interface):
402             type = end1.interface_
403         else: # isinstance(end2.type, UML.Class):
404             type = end1.class_
405
406         # if the end of association is not navigable (in terms of UML 1.x)
407         # then set navigability to unknown (in terms of UML 2.0)
408         if not (type and end1 in type.ownedAttribute):
409             del end1.owningAssociation
410
411     if tuple(map(int, gaphor_version.split('.'))) < (0, 7, 1):
412         log.info('Fix navigability of Associations (file version: %s)' % gaphor_version)
413         for elem in elements.values():
414             try:
415                 if elem.type == 'Association':
416                     asc = elem.element
417                     end1 = asc.memberEnd[0]
418                     end2 = asc.memberEnd[1]
419                     if end1 and end2:
420                         fix(end1, end2)
421                         fix(end2, end1)
422             except Exception, e:
423                 log.error('Error while updating Association', e)
424
425
426 def version_0_6_2(elements, factory, gaphor_version):
427     """
428     Before 0.6.2 an Interface could be represented by a ClassItem and
429     a InterfaceItem. Now only InterfaceItems are used.
430     """
431     if tuple(map(int, gaphor_version.split('.'))) < (0, 6, 2):
432         for elem in elements.values():
433             try:
434                 if type(elem) is parser.element and elem.type == 'Interface':
435                     for p_id in elem.presentation:
436                         p = elements[p_id]
437                         if p.type == 'ClassItem':
438                             p.type = 'InterfaceItem'
439                             p.values['drawing-style'] = '0'
440                         elif p.type == 'InterfaceItem':
441                             p.values['drawing-style'] = '2'
442             except Exception, e:
443                 log.error('Error while updating InterfaceItems', e)
444
445
446 def version_0_5_2(elements, factory, gaphor_version):
447     """
448     Before version 0.5.2, the wrong memberEnd of the association was
449     holding the aggregation information.
450     """
451     if tuple(map(int, gaphor_version.split('.'))) < (0, 5, 2):
452         log.info('Fix composition on Associations (file version: %s)' % gaphor_version)
453         for elem in elements.values():
454             try:
455                 if elem.type == 'Association':
456                     a = elem.element
457                     agg1 = a.memberEnd[0].aggregation
458                     agg2 = a.memberEnd[1].aggregation
459                     a.memberEnd[0].aggregation = agg2
460                     a.memberEnd[1].aggregation = agg1
461             except Exception, e:
462                 log.error('Error while updating Association', e)
463
Note: See TracBrowser for help on using the browser.