root/gaphor/tags/gaphor-0.7.0/utils/genUML2.py

Revision 401, 16.7 kB (checked in by arjanmol, 4 years ago)

*** empty log message ***

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
Line 
1 #!/usr/bin/env python
2 # vim:sw=4:et
3 #
4 # Create a UML 2.0 datamodel from the Gaphor 0.2.0 model file.
5 #
6 # To do this we do the following:
7 # 1. read the model file with the gaphor parser
8 # 2. Create a object herarcy by ordering elements based on generalizations
9
10 # Recreate the model using some very dynamic class, so we can set all
11 # attributes and traverse them to generate the data model.
12
13 from gaphor.parser import parse, base, element, canvas, canvasitem
14 import sys, string, operator
15 import override
16
17 header = """# This file is generated by genUML2.py. DO NOT EDIT!
18
19 from properties import association, attribute, enumeration, derivedunion, redefine
20 """
21
22 # Make getitem behave more politely
23 base.__real_getitem__ = base.__getitem__
24
25 def base__getitem__(self, key):
26     try:
27         return self.__real_getitem__(key)
28     except KeyError:
29         return None
30
31 base.__getitem__ = base__getitem__
32
33 # redefine 'bool' for Python version < 2.3
34 if map(int, sys.version[:3].split('.')) < [2, 3]:
35     header = header + "bool = int\n"
36
37 def msg(s):
38     sys.stderr.write('  ')
39     sys.stderr.write(s)
40     sys.stderr.write('\n')
41     sys.stderr.flush()
42
43 class Writer:
44
45     def __init__(self, filename, overridesfile=None):
46         self.overrides = overridesfile and override.Overrides(overridesfile) or None
47         if filename:
48             self.out = open(filename, 'w')
49         else:
50             self.out = sys.stdout
51
52     def write(self, data):
53         self.out.write(data)
54
55     def close(self):
56         self.out.close()
57
58     def write_classdef(self, clazz):
59         """Write a class definition (class xx(x): pass).
60         First the parent classes are examined. After that its own definition
61         is written. It is ensured that class definitions are only written
62         once.
63         """
64         if not clazz.written:
65             s = ''
66             for g in clazz.generalization:
67                 self.write_classdef(g)
68                 if s: s += ', '
69                 s = s + g['name']
70             if not s: s = 'object'
71             if not self.overrides.write_override(self, clazz['name']):
72                 self.write('class %s(%s): pass\n' % (clazz['name'], s))
73         clazz.written = True
74
75     def write_property(self, full_name, value):
76         """Write a property to the file. If the property is overridden, use the
77         overridden value. full_name should be like Class.attribute. value is
78         free format text.
79         """
80         if not self.overrides.write_override(self, full_name):
81             self.write('%s = %s\n' % (full_name, value))
82
83     def write_attribute(self, a, enumerations={}):
84         """Write a definition for attribute a. Enumerations may be a dict
85         of enumerations, indexed by ID. These are used to identify enums.
86         """
87         params = { }
88         type = a.typeValue and a.typeValue.get('value')
89         if type.lower() == 'boolean':
90             # FixMe: Should this be a boolean or an integer?
91             # Integer is save and compattable with python2.2.
92             type = 'int'
93         elif type.lower() in ('integer', 'unlimitednatural'):
94             type = 'int'
95         elif type.lower() == 'string':
96             # Change to basestr for Python 2.3
97             type = 'str'
98             #type = '(str, unicode)'
99
100         default = a.defaultValue and a.defaultValue.value
101         # Make sure types are represented the Python way:
102         if default and default.lower() in ('true', 'false'):
103             default = default.title() # True or False...
104         if default is not None:
105             params['default'] = str(default)
106
107         lower = a.lowerValue and a.lowerValue.value
108         if lower and lower != '0':
109             params['lower'] = lower
110         upper = a.upperValue and a.upperValue.value
111         if upper and upper != '1':
112             params['upper'] = upper
113
114         #kind, derived, a.name, type, default, lower, upper = parse_attribute(a)
115
116         full_name = "%s.%s" % (a.class_name, a.name)
117         if self.overrides.has_override(full_name):
118             self.overrides.write_override(self, full_name)
119         elif eval(a.isDerived or '0'):
120             msg('ignoring derived attribute %s.%s: no definition' % (a.class_name, a.name))
121         elif type.endswith('Kind'):
122             e = filter(lambda e: e['name'] == type, enumerations.values())[0]
123             self.write_property("%s.%s" % (a.class_name, a.name),
124                                 "enumeration('%s', %s, '%s')" % (a.name, e.enumerates, default or e.enumerates[0]))
125         else:
126             if params:
127                 attribute = "attribute('%s', %s, %s)" % (a.name, type, ', '.join(map('='.join, params.items())))
128             else:
129                 attribute = "attribute('%s', %s)" % (a.name, type)
130             self.write_property("%s.%s" % (a.class_name, a.name), attribute)
131
132     def write_operation(self, o):
133         full_name = "%s.%s" % (o.class_name, o.name)
134         if self.overrides.has_override(full_name):
135             self.overrides.write_override(self, full_name)
136         else:
137             msg("No override for operation %s" % full_name)
138
139     def write_association(self, head, tail):
140         """Write an association for head. False is returned if the association
141         is derived.
142         The head association end is enriched with the following attributes:
143             derived - association is a derived union or not
144             name - name of the association end (name of head is found on tail)
145             class_name - name of the class this association belongs to
146             opposite_class_name - name of the class at the other end of the assoc.
147             lower - lower multiplicity
148             upper - upper multiplicity
149             subsets - derived unions that use the association
150             redefines - redefines existing associations
151         Returns True if the association is written. False is returned if
152         the association should be handled by write_derivedunion() or
153         write_redefine().
154         """
155         navigable = head.get('class_')
156         if not navigable:
157             # from this side, the association is not navigable
158             return True
159         try:
160             #derived, name = parse_association_name(head['name'])
161             name = head.name
162             derived = int(head.isDerived or 0)
163         except KeyError:
164             msg('ERROR! no name, but navigable: %s (%s.%s)' %
165                 (head.id, head.class_name, head.name))
166             return True
167
168         #lower, upper, subsets, redefines = parse_association_multiplicity(head.lowerValue)
169         #print head.id, head.lowerValue
170         upper = head.upperValue and head.upperValue.value or '*'
171         lower = head.lowerValue and head.lowerValue.value or upper
172         if lower == '*':
173             lower = 0
174         subsets, redefines = parse_association_tags(head.taggedValue and head.taggedValue.value or '')
175
176         # Add the values found. These are used later to generate derived unions.
177         head.derived = derived
178         #head.name = name
179         head.class_name = head.class_['name']
180         head.opposite_class_name = head.type['name']
181         head.lower = lower
182         head.upper = upper
183         head.subsets = subsets
184         head.redefines = redefines
185
186         # Derived unions and redefines are handled separately
187         if derived or redefines:
188             return False
189
190         a = "association('%s', %s" % (name, head.opposite_class_name)
191         if lower not in ('0', 0):
192             a += ', lower=%s' % lower
193         if upper != '*':
194             a += ', upper=%s' % upper
195         if head.get('aggregation') == 'composite':
196             a += ', composite=True'
197
198         # Add the opposite property if the head itself is navigable:
199         navigable = tail.get('class_')
200         if navigable:
201             try:
202                 #o_derived, o_name = parse_association_name(tail['name'])
203                 o_name = tail.name
204                 o_derived = int(tail.isDerived or 0)
205             except KeyError:
206                 msg('ERROR! no name, but navigable: %s (%s.%s)' %
207                     (tail.id, tail.class_name, tail.name))
208             else:
209                 assert not (derived and not o_derived), 'One end is derived, the other end not ???'
210                 a += ", opposite='%s'" % o_name
211
212         self.write_property("%s.%s" % (head.class_name, name), a + ')')
213         return True
214
215     def write_derivedunion(self, d):
216         """Write a derived union. If there are no subsets a warning
217         is issued. The derivedunion is still created though.
218         Derived unions may be created for associations that were returned
219         False by write_association()."""
220         subs = ''
221         for u in d.union:
222             if u.derived and not u.written:
223                 self.write_derivedunion(u)
224             if subs: subs += ', '
225             subs += '%s.%s' % (u.class_name, u.name)
226         if subs:
227             self.write_property("%s.%s" % (d.class_name, d.name),
228                                 "derivedunion('%s', %s, %s, %s)" % (d.name, d.lower, d.upper == '*' and "'*'" or d.upper, subs))
229         else:
230             if not self.overrides.has_override('%s.%s' % (d.class_name, d.name)):
231                 msg('no subsets for derived union: %s.%s[%s..%s]' % (d.class_name, d.name, d.lower, d.upper))
232             self.write_property("%s.%s" % (d.class_name, d.name),
233                                 "derivedunion('%s', %s, %s)" % (d.name, d.lower, d.upper == '*' and "'*'" or d.upper))
234         d.written = True
235
236     def write_redefine(self, r):
237         """Redefines may be created for associations that were returned
238         False by write_association()."""
239         self.write_property("%s.%s" % (r.class_name, r.name),
240                             "redefine('%s', %s, %s)" % (r.name, r.opposite_class_name, r.redefines))
241
242
243 def parse_association_name(name):
244     # First remove spaces
245     name = name.replace(' ','')
246     derived = False
247     # Check if this is a derived union
248     while name and not name[0].isalpha():
249         if name[0] == '/':
250             derived = True
251         name = name[1:]
252     return derived, name
253
254 def parse_association_multiplicity(mult):
255     subsets = []
256     redefines = None
257     tag = None
258     if '{' in mult:
259         # we have tagged values
260         mult, tag = map(string.strip, mult.split('{'))
261         if tag[-1] == '}':
262             tag = tag[:-1]
263     else:
264         mult = mult.strip()
265    
266     mult = mult.split('.')
267     lower = mult[0]
268     upper = mult[-1]
269     if lower == '*':
270         lower = 0
271     #if upper == '*':
272     #    upper = "'*'"
273
274     if tag and tag.find('subsets') != -1:
275         # find the text after 'subsets':
276         subsets = tag[tag.find('subsets') + len('subsets'):]
277         # remove all whitespaces and stuff
278         subsets = subsets.replace(' ', '').replace('\n', '').replace('\r', '')
279         subsets = subsets.split(',')
280     if tag and tag.find('redefines') != -1:
281         # find the text after 'redefines':
282         redefines = tag[tag.find('redefines') + len('redefines'):]
283         # remove all whitespaces and stuff
284         redefines = redefines.replace(' ', '').replace('\n', '').replace('\r', '')
285         l = redefines.split(',')
286         assert len(l) == 1
287         redefines = l[0]
288
289     return lower, upper, subsets, redefines
290
291 def parse_association_tags(tag):
292     subsets = []
293     redefines = None
294
295     if tag and tag.find('subsets') != -1:
296         # find the text after 'subsets':
297         subsets = tag[tag.find('subsets') + len('subsets'):]
298         # remove all whitespaces and stuff
299         subsets = subsets.replace(' ', '').replace('\n', '').replace('\r', '')
300         subsets = subsets.split(',')
301     if tag and tag.find('redefines') != -1:
302         # find the text after 'redefines':
303         redefines = tag[tag.find('redefines') + len('redefines'):]
304         # remove all whitespaces and stuff
305         redefines = redefines.replace(' ', '').replace('\n', '').replace('\r', '')
306         l = redefines.split(',')
307         assert len(l) == 1
308         redefines = l[0]
309
310     return subsets, redefines
311
312 def generate(filename, outfile=None, overridesfile=None):
313     # parse the file
314     all_elements = parse(filename)
315
316     def unref(val, attr):
317         """Resolve references.
318         """
319         try:
320             refs = val.references[attr]
321         except KeyError:
322             val.references[attr] = None
323             return
324
325         if type(refs) is type([]):
326             unrefs = []
327             for r in refs:
328                 unrefs.append(all_elements[r])
329             val.references[attr] = unrefs
330         else:
331             val.references[attr] = all_elements[refs]
332
333     writer = Writer(outfile, overridesfile)
334
335     # extract usable elements from all_elements. Some elements are given
336     # some extra attributes.
337     classes = { }
338     enumerations = { }
339     generalizations = { }
340     associations = { }
341     properties = { }
342     operations = { }
343     for key, val in all_elements.items():
344         # Find classes, *Kind (enumerations) are given special treatment
345         if isinstance(val, element):
346             if val.type == 'Class' and val.get('name'):
347                 if val['name'].endswith('Kind'):
348                     enumerations[key] = val
349                 else:
350                     classes[key] = val
351                     # Add extra properties for easy code generation:
352                     val.specialization = []
353                     val.generalization = []
354                     val.written = False
355             elif val.type == 'Generalization':
356                 generalizations[key] = val
357             elif val.type == 'Association':
358                 associations[key] = val
359             elif val.type == 'Property':
360                 properties[key] = val
361                 unref(val, 'typeValue')
362                 unref(val, 'defaultValue')
363                 unref(val, 'lowerValue')
364                 unref(val, 'upperValue')
365                 unref(val, 'taggedValue')
366             elif val.type == 'Operation':
367                 operations[key] = val
368
369     # find inheritance relationships
370     for g in generalizations.values():
371         #assert g.specific and g.general
372         specific = g['specific']
373         general = g['general']
374         classes[specific].generalization.append(classes[general])
375         classes[general].specialization.append(classes[specific])
376
377     # add values to enumerations:
378     for e in enumerations.values():
379         values = []
380         for key in e['ownedAttribute']:
381             values.append(str(properties[key]['name']))
382         e.enumerates = tuple(values)
383
384     # create file header
385     writer.write(header)
386
387     # create class definitions
388     for c in classes.values():
389         writer.write_classdef(c)
390
391     # create attributes and enumerations
392     for c in classes.values():
393         for p in c.get('ownedAttribute') or []:
394             a = properties.get(p)
395             # set class_name, since write_attribute depends on it
396             a.class_name = c['name']
397             if not a.get('association'):
398                 writer.write_attribute(a, enumerations)
399
400     # create associations, derivedunions are held back
401     derivedunions = { } # indexed by name in stead of id
402     redefines = [ ]
403     for a in associations.values():
404         ends = []
405         for end in a['memberEnd']:
406             end = properties[end]
407             end.type = classes[end['type']]
408             end.class_ = end.get('class_') and classes[end['class_']] or None
409             #assert end.type is not end.class_
410             #if not end.get('lowerValue'):
411                 #end.lowerValue = end.lowerValue.get('value') or ''
412             #else:
413                 #end.lowerValue = ''
414             ends.append(end)
415
416         for e1, e2 in ((ends[0], ends[1]), (ends[1], ends[0])):
417             if not writer.write_association(e1, e2):
418                 # At this point the association is parsed, but not written.
419                 # assure that derived unions do not get overwritten
420                 if e1.redefines:
421                     redefines.append(e1)
422                 else:
423                     assert not derivedunions.get(e1.name), "%s.%s is already in derived union set in class %s" % (e1.class_name, e1.name, derivedunions.get(e1.name).class_name)
424                     derivedunions[e1.name] = e1
425                     e1.union = [ ]
426                     e1.written = False
427
428
429     # create derived unions, first link the association ends to the d
430     for a in [v for v in properties.values() if v.subsets]:
431         for s in a.subsets