gaphor/utils/genUML2.py
Arjan Molenaar a21bbff037 *** empty log message ***
git-svn-id: file:///Users/arjan/backup/gaphor/trunk/gaphor@356 a8418922-720d-0410-834f-a69b97ada669
2004-06-17 19:31:12 +00:00

455 lines
17 KiB
Python

#!/usr/bin/env python
# vim:sw=4:et
#
# Create a UML 2.0 datamodel from the Gaphor 0.2.0 model file.
#
# To do this we do the following:
# 1. read the model file with the gaphor parser
# 2. Create a object herarcy by ordering elements based on generalizations
# Recreate the model using some very dynamic class, so we can set all
# attributes and traverse them to generate the data model.
from gaphor.parser import parse, base, element, canvas, canvasitem
import sys, string, operator
import override
header = """# This file is generated by genUML2.py. DO NOT EDIT!
from properties import association, attribute, enumeration, derivedunion, redefine
"""
# Make getitem behave more politely
base.__real_getitem__ = base.__getitem__
def base__getitem__(self, key):
try:
return self.__real_getitem__(key)
except KeyError:
return None
base.__getitem__ = base__getitem__
# redefine 'bool' for Python version < 2.3
if map(int, sys.version[:3].split('.')) < [2, 3]:
header = header + "bool = int\n"
def msg(s):
sys.stderr.write(' ')
sys.stderr.write(s)
sys.stderr.write('\n')
sys.stderr.flush()
class Writer:
def __init__(self, filename, overridesfile=None):
self.overrides = overridesfile and override.Overrides(overridesfile) or None
if filename:
self.out = open(filename, 'w')
else:
self.out = sys.stdout
def write(self, data):
self.out.write(data)
def close(self):
self.out.close()
def write_classdef(self, clazz):
"""Write a class definition (class xx(x): pass).
First the parent classes are examined. After that its own definition
is written. It is ensured that class definitions are only written
once.
"""
if not clazz.written:
s = ''
for g in clazz.generalization:
self.write_classdef(g)
if s: s += ', '
s = s + g['name']
if not s: s = 'object'
if not self.overrides.write_override(self, clazz['name']):
self.write('class %s(%s): pass\n' % (clazz['name'], s))
clazz.written = True
def write_property(self, full_name, value):
"""Write a property to the file. If the property is overridden, use the
overridden value. full_name should be like Class.attribute. value is
free format text.
"""
if not self.overrides.write_override(self, full_name):
self.write('%s = %s\n' % (full_name, value))
def write_attribute(self, a, enumerations={}):
"""Write a definition for attribute a. Enumerations may be a dict
of enumerations, indexed by ID. These are used to identify enums.
"""
params = { }
type = a.typeValue and a.typeValue.get('value')
if type.lower() == 'boolean':
# FixMe: Should this be a boolean or an integer?
# Integer is save and compattable with python2.2.
type = 'int'
elif type.lower() in ('integer', 'unlimitednatural'):
type = 'int'
elif type.lower() == 'string':
# Change to basestr for Python 2.3
type = 'str'
#type = '(str, unicode)'
default = a.defaultValue and a.defaultValue.value
# Make sure types are represented the Python way:
if default and default.lower() in ('true', 'false'):
default = default.title() # True or False...
if default is not None:
params['default'] = str(default)
lower = a.lowerValue and a.lowerValue.value
if lower and lower != '0':
params['lower'] = lower
upper = a.upperValue and a.upperValue.value
if upper and upper != '1':
params['upper'] = upper
#kind, derived, a.name, type, default, lower, upper = parse_attribute(a)
full_name = "%s.%s" % (a.class_name, a.name)
if self.overrides.has_override(full_name):
self.overrides.write_override(self, full_name)
elif eval(a.isDerived or '0'):
msg('ignoring derived attribute %s.%s: no definition' % (a.class_name, a.name))
elif type.endswith('Kind'):
e = filter(lambda e: e['name'] == type, enumerations.values())[0]
self.write_property("%s.%s" % (a.class_name, a.name),
"enumeration('%s', %s, '%s')" % (a.name, e.enumerates, default or e.enumerates[0]))
else:
if params:
attribute = "attribute('%s', %s, %s)" % (a.name, type, ', '.join(map('='.join, params.items())))
else:
attribute = "attribute('%s', %s)" % (a.name, type)
self.write_property("%s.%s" % (a.class_name, a.name), attribute)
def write_operation(self, o):
full_name = "%s.%s" % (o.class_name, o.name)
if self.overrides.has_override(full_name):
self.overrides.write_override(self, full_name)
else:
msg("No override for operation %s" % full_name)
def write_association(self, head, tail):
"""Write an association for head. False is returned if the association
is derived.
The head association end is enriched with the following attributes:
derived - association is a derived union or not
name - name of the association end (name of head is found on tail)
class_name - name of the class this association belongs to
opposite_class_name - name of the class at the other end of the assoc.
lower - lower multiplicity
upper - upper multiplicity
subsets - derived unions that use the association
redefines - redefines existing associations
Returns True if the association is written. False is returned if
the association should be handled by write_derivedunion() or
write_redefine().
"""
navigable = head.get('class_')
if not navigable:
# from this side, the association is not navigable
return True
try:
#derived, name = parse_association_name(head['name'])
name = head.name
derived = int(head.isDerived or 0)
except KeyError:
msg('ERROR! no name, but navigable: %s (%s.%s)' %
(head.id, head.class_name, head.name))
return True
#lower, upper, subsets, redefines = parse_association_multiplicity(head.lowerValue)
#print head.id, head.lowerValue
upper = head.upperValue and head.upperValue.value or '*'
lower = head.lowerValue and head.lowerValue.value or upper
if lower == '*':
lower = 0
subsets, redefines = parse_association_tags(head.taggedValue and head.taggedValue.value or '')
# Add the values found. These are used later to generate derived unions.
head.derived = derived
#head.name = name
head.class_name = head.class_['name']
head.opposite_class_name = head.type['name']
head.lower = lower
head.upper = upper
head.subsets = subsets
head.redefines = redefines
# Derived unions and redefines are handled separately
if derived or redefines:
return False
a = "association('%s', %s" % (name, head.opposite_class_name)
if lower not in ('0', 0):
a += ', lower=%s' % lower
if upper != '*':
a += ', upper=%s' % upper
if tail.get('aggregation') == 'composite':
a += ', composite=True'
# Add the opposite property if the head itself is navigable:
navigable = tail.get('class_')
if navigable:
try:
#o_derived, o_name = parse_association_name(tail['name'])
o_name = tail.name
o_derived = int(tail.isDerived or 0)
except KeyError:
msg('ERROR! no name, but navigable: %s (%s.%s)' %
(tail.id, tail.class_name, tail.name))
else:
assert not (derived and not o_derived), 'One end is derived, the other end not ???'
a += ", opposite='%s'" % o_name
self.write_property("%s.%s" % (head.class_name, name), a + ')')
return True
def write_derivedunion(self, d):
"""Write a derived union. If there are no subsets a warning
is issued. The derivedunion is still created though.
Derived unions may be created for associations that were returned
False by write_association()."""
subs = ''
for u in d.union:
if u.derived and not u.written:
self.write_derivedunion(u)
if subs: subs += ', '
subs += '%s.%s' % (u.class_name, u.name)
if subs:
self.write_property("%s.%s" % (d.class_name, d.name),
"derivedunion('%s', %s, %s, %s)" % (d.name, d.lower, d.upper == '*' and "'*'" or d.upper, subs))
else:
if not self.overrides.has_override('%s.%s' % (d.class_name, d.name)):
msg('no subsets for derived union: %s.%s[%s..%s]' % (d.class_name, d.name, d.lower, d.upper))
self.write_property("%s.%s" % (d.class_name, d.name),
"derivedunion('%s', %s, %s)" % (d.name, d.lower, d.upper == '*' and "'*'" or d.upper))
d.written = True
def write_redefine(self, r):
"""Redefines may be created for associations that were returned
False by write_association()."""
self.write_property("%s.%s" % (r.class_name, r.name),
"redefine('%s', %s, %s)" % (r.name, r.opposite_class_name, r.redefines))
def parse_association_name(name):
# First remove spaces
name = name.replace(' ','')
derived = False
# Check if this is a derived union
while name and not name[0].isalpha():
if name[0] == '/':
derived = True
name = name[1:]
return derived, name
def parse_association_multiplicity(mult):
subsets = []
redefines = None
tag = None
if '{' in mult:
# we have tagged values
mult, tag = map(string.strip, mult.split('{'))
if tag[-1] == '}':
tag = tag[:-1]
else:
mult = mult.strip()
mult = mult.split('.')
lower = mult[0]
upper = mult[-1]
if lower == '*':
lower = 0
#if upper == '*':
# upper = "'*'"
if tag and tag.find('subsets') != -1:
# find the text after 'subsets':
subsets = tag[tag.find('subsets') + len('subsets'):]
# remove all whitespaces and stuff
subsets = subsets.replace(' ', '').replace('\n', '').replace('\r', '')
subsets = subsets.split(',')
if tag and tag.find('redefines') != -1:
# find the text after 'redefines':
redefines = tag[tag.find('redefines') + len('redefines'):]
# remove all whitespaces and stuff
redefines = redefines.replace(' ', '').replace('\n', '').replace('\r', '')
l = redefines.split(',')
assert len(l) == 1
redefines = l[0]
return lower, upper, subsets, redefines
def parse_association_tags(tag):
subsets = []
redefines = None
if tag and tag.find('subsets') != -1:
# find the text after 'subsets':
subsets = tag[tag.find('subsets') + len('subsets'):]
# remove all whitespaces and stuff
subsets = subsets.replace(' ', '').replace('\n', '').replace('\r', '')
subsets = subsets.split(',')
if tag and tag.find('redefines') != -1:
# find the text after 'redefines':
redefines = tag[tag.find('redefines') + len('redefines'):]
# remove all whitespaces and stuff
redefines = redefines.replace(' ', '').replace('\n', '').replace('\r', '')
l = redefines.split(',')
assert len(l) == 1
redefines = l[0]
return subsets, redefines
def generate(filename, outfile=None, overridesfile=None):
# parse the file
all_elements = parse(filename)
def unref(val, attr):
"""Resolve references.
"""
try:
refs = val.references[attr]
except KeyError:
val.references[attr] = None
return
if type(refs) is type([]):
unrefs = []
for r in refs:
unrefs.append(all_elements[r])
val.references[attr] = unrefs
else:
val.references[attr] = all_elements[refs]
writer = Writer(outfile, overridesfile)
# extract usable elements from all_elements. Some elements are given
# some extra attributes.
classes = { }
enumerations = { }
generalizations = { }
associations = { }
properties = { }
operations = { }
for key, val in all_elements.items():
# Find classes, *Kind (enumerations) are given special treatment
if isinstance(val, element):
if val.type == 'Class' and val.get('name'):
if val['name'].endswith('Kind'):
enumerations[key] = val
else:
classes[key] = val
# Add extra properties for easy code generation:
val.specialization = []
val.generalization = []
val.written = False
elif val.type == 'Generalization':
generalizations[key] = val
elif val.type == 'Association':
associations[key] = val
elif val.type == 'Property':
properties[key] = val
unref(val, 'typeValue')
unref(val, 'defaultValue')
unref(val, 'lowerValue')
unref(val, 'upperValue')
unref(val, 'taggedValue')
elif val.type == 'Operation':
operations[key] = val
# find inheritance relationships
for g in generalizations.values():
#assert g.specific and g.general
specific = g['specific']
general = g['general']
classes[specific].generalization.append(classes[general])
classes[general].specialization.append(classes[specific])
# add values to enumerations:
for e in enumerations.values():
values = []
for key in e['ownedAttribute']:
values.append(str(properties[key]['name']))
e.enumerates = tuple(values)
# create file header
writer.write(header)
# create class definitions
for c in classes.values():
writer.write_classdef(c)
# create attributes and enumerations
for c in classes.values():
for p in c.get('ownedAttribute') or []:
a = properties.get(p)
# set class_name, since write_attribute depends on it
a.class_name = c['name']
if not a.get('association'):
writer.write_attribute(a, enumerations)
# create associations, derivedunions are held back
derivedunions = { } # indexed by name in stead of id
redefines = [ ]
for a in associations.values():
ends = []
for end in a['memberEnd']:
end = properties[end]
end.type = classes[end['type']]
end.class_ = end.get('class_') and classes[end['class_']] or None
#assert end.type is not end.class_
#if not end.get('lowerValue'):
#end.lowerValue = end.lowerValue.get('value') or ''
#else:
#end.lowerValue = ''
ends.append(end)
for e1, e2 in ((ends[0], ends[1]), (ends[1], ends[0])):
if not writer.write_association(e1, e2):
# At this point the association is parsed, but not written.
# assure that derived unions do not get overwritten
if e1.redefines:
redefines.append(e1)
else:
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)
derivedunions[e1.name] = e1
e1.union = [ ]
e1.written = False
# create derived unions, first link the association ends to the d
for a in [v for v in properties.values() if v.subsets]:
for s in a.subsets or ():
try:
derivedunions[s].union.append(a)
except KeyError:
msg('not a derived union: %s.%s' % (a.class_name, s))
for d in derivedunions.values():
writer.write_derivedunion(d)
for r in redefines or ():
msg('redefining %s -> %s.%s' % (r.redefines, r.class_name, r.name))
writer.write_redefine(r)
# create operations
for c in classes.values():
for p in c.get('ownedOperation') or []:
o = operations.get(p)
o.class_name = c['name']
writer.write_operation(o)
writer.close()
if __name__ == '__main__':
generate('uml2.gaphor')