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

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