root/gaphor/tags/gaphor-0.11.1/gaphor/storage.py

Revision 1559, 17.5 kB (checked in by arj..@yirdis.nl, 1 year ago)

fixed bug in storage module. Make constraints connect properly when loading a model.

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