git-svn-id: file:///Users/arjan/backup/gaphor/trunk/gaphor@356 a8418922-720d-0410-834f-a69b97ada669
455 lines
17 KiB
Python
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')
|