root/gaphor/tags/gaphor-0.12.0/utils/command/gen_uml.py

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

separated parser and writer code for associations.

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