# State-Chart Code Generation from UML Diagram # Copyright (c) 2009 Sebastian Setzer # Use at your own risk. There's no support, no warranty, nothing at all for this prototype version. # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # templates # header t_hdr = ''' --- --- start #ifndef $include_guard #define $include_guard class $classname { enum State { --- --- enum_value $name = $id, --- --- between_enum_and_triggers STATE_COUNT = $state_count } m_states; void setState(State state); void do(); public: $classname(); --- --- triggerdecl void $trigger(); --- --- end } #endif /* $include_guard */ ''' # compilation unit t_cpp = ''' --- --- start #include "$filename.h" $classname::$classname() : m_states(START) {} void $classname::setState(State state) { // exit old state switch(m_state) { --- --- case case $label: --- --- action $action; --- --- guarded_action if($guard) $action; --- --- case_break break; --- --- between_exit_and_enter } // enter new state m_state = state; switch(m_state) { --- --- setstate_end } } --- --- do_start void $classname::do() { switch(m_state) { --- --- do_end } } --- --- triggerdef_start void $classname::$trigger() { switch(m_state) { --- --- triggerdef_guard_start if($guard) { --- --- triggerdef_guarded_action $action; --- --- triggerdef_guard_end } --- --- error_multiple_unguarded_transitions Error: multiple unguarded transitions. --- --- triggerdef_action $action; --- --- triggerdef_end } } --- --- end // end of $classname ''' # substitute into a string, not into a file: t_sub = ''' --- --- triggerdef_setstate -n setState($state) ''' # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # # import sys, dia, re, os.path from string import Template import re sep_re = re.compile('---(.*)\n', re.M) class Templates: def __init__(self, template_string): parts = sep_re.split(template_string)[1:] self.templates = {} while parts: (sep_line, template), parts = parts[:2], parts[2:] argv=sep_line.strip('- ').split() key=argv[0] for arg in argv[1:]: # no last newline, as in echo -n if arg=='-n' and template.endswith('\n'): template=template[:-1] else: raise '%s: ununsed argument "%s"' % (key, arg) self.templates[key] = Template(template) def __getattr__(self, name): return self.templates[name].substitute class FileTemplates(Templates): def __init__(self, file, template_string): Templates.__init__(self, template_string) self.file=file def __getattr__(self, name): return lambda **kws: self.file.write(self.templates[name].substitute(kws)) def transition_label(o): l = o.properties["trigger"].value if o.properties["guard"].value: l += " [%s]" % o.properties["guard"].value if o.properties["action"].value: l += " / %s" % o.properties["action"].value return l def char2cpp(c): if not c.isalnum(): return '_' return c def make_t_cpp_id(s): s = ''.join(map(char2cpp, s)) if not s: s = '_' if s[0].isdigit(): s = '_' + s return s # For every edge there is a trigger, an guard and an action attribute. # For states, there are 3 predefined triggers entry, do, exit. # Unfortunately, these have only actions, no guards. # This function checks if the action matches a "[guard] action" - pattern. # If it does, it extracts the guard. def extract_guard(action): action = action.strip() if action.startswith('['): level = 0 for i,c in enumerate(action): if c=='[': level += 1 elif c==']': level -= 1 if level == 0: # found matching ']' return action[1:i].strip(), action[i+1:].strip() return None, action class Rectangle: def __init__(self, o): self.x1, self.y1, self.x2, self.y2 = tuple(o.properties["obj_bb"].value) def inside(state,cluster): s = Rectangle(state) c = Rectangle(cluster) return (s.x1 >= c.x1) and (s.x2 <= c.x2) and (s.y1 >= c.y1) and (s.y2 <= c.y2) # class for dia_dict of dia state objects def State_reset(): State.dia_dict={} State.start_ids = 0 State.final_ids = 0 class State: dia_dict={} start_ids = 0 final_ids = 0 def __init__(self, o): self.entry_guard, self.entry_action, self.do_guard, self.do_action, self.exit_guard, self.exit_action = 6*(None,) if o.type.name == "UML - State": self.name = o.properties["text"].value.text.strip() entry, do, exit = o.properties["entry_action"].value, o.properties["do_action"].value, o.properties["exit_action"].value if entry: self.entry_guard, self.entry_action = extract_guard(entry) if do: self.do_guard, self.do_action = extract_guard(do) if exit: self.exit_guard, self.exit_action = extract_guard(exit) elif o.type.name == "UML - State Term": if o.properties["is_final"].value: # final state self.name = "FINAL_%d" % State.final_ids State.final_ids += 1 else: # start state self.name = "START" if State.start_ids: name = "START_%d" % State.start_ids State.start_ids += 1 def dia_dict(o): return State.dia_dict[o] def output(filename, start_states, final_states, normal_states, clusters, edges, errors): filename, ext = os.path.splitext(filename) path, filename = os.path.split(filename) classname = filename filename = filename.lower() include_guard = '%s_H' % filename.upper() path = os.path.join(path, filename) f_hdr = open(path + ".h", "w") f_cpp = open(path + ".cpp", "w") hdr = FileTemplates(f_hdr, t_hdr) cpp = FileTemplates(f_cpp, t_cpp) sub = Templates(t_sub) triggers = set() transition_table = {} cluster_states = {} # Output is done in this order, which is probably creation order in dia for the 3 groups of states. # I think that's quite good for version control of the generated files: states won't get reordered when renamed. all_states = start_states + normal_states + final_states # create State objects for o in all_states: State.dia_dict[o] = State(o) # use order of all_states instead of unsorted State.dia_dict.values() all_gen_info = map(lambda o: dia_dict(o), all_states) # find states which are geometrically inside clusters for c in clusters: cluster_states[c] = filter(lambda s: inside(s,c), all_states) # TODO: # # Edges # ---------- # An edge with cluser-tgt is equivalent to an edge to the (unique) start state of the cluster # An edge with cluser-src is equivalent to lots of edges with state-src. # In Triggers of cluster-edges, "case" could be combined, so the body doesn't need to be duplicated. # But it would be even more general if we had an "equal body recognition" for switch-es, # So that could compress non-cluster-switches, too. # Like this: for each case: d[body].append(case.label) ... # For this, abstract the different switch-outputs below to a switch-output-function. # # Actions # ------------ # do-actions sind einfach: Hinzufuegen zu den do-actions der enthaltenen states. # entry- und exit actions: jeder state gehoert zu einer Menge von cluster_states. # Bei Uebergang wird entry fuer alle cluster_states aufgerufen, die in der Menge # der cluster_states des alten states nicht drin waren, aber in der des neuen # states drin sind. exit-action analog. # Problem: Ein switch in setState ueber alten bzw. neuen state reicht nicht mehr, # es muss ein switch uexxdiff . ueber die Kombi aus altem+neuem state sein (z.B. verschachtelter switch). # "wenn neuer state im cluster (erster switch) und alter state nicht im cluster (zweiter switch) # dann entry action des clusters" ist auch nichts anderes als verschachtelter switch, # mit dem Nachteil dass mans fuer jeden cluster extra machen muss. # --> Es darf nicht in setState sondern muss in die Ereignis-Funktionen (die entsprechen den edges)? # Am besten nicht die actions direkt eingefuegt sondern: # leaveCluster(C_old1) # leaveCluster(C_old2) # setState(s_new) # enterCluster(C_new1) # Trotzdem wie oben beschrieben: Cluster-edges vorher in (mehrere) state-edges # umwandeln, erst dann Menge von leave/enter-clusters bestimmen. # leave/enter soll naemlich auch für edges aufgerufen werden, die an normalen # states haengen aber die "Grenzen" eines clusters ueberschreiten. #~ for c in clusters: #~ print "Cluster", c.properties["text"].value.text #~ for s in cluster_states[c]: #~ print " ", dia_dict(s).name # fill transition_table: dictionary (trigger, src) --> list of (edge-object, src, tgt) for (e, src, tgt) in edges: trigger = make_t_cpp_id(e.properties["trigger"].value.strip()) if trigger != "otherwise": triggers.add(trigger) # Handle clusters if src in cluster_states: src_states = cluster_states[src] else: src_states = [src] if tgt in cluster_states: # TODO: # cluster_start = intersection(cluster_states[tgt], start_states) # assert len(cluster_start) == 1 # tgt = cluster_start[0] pass for src_state in src_states: # append (e, src_state, tgt) to transition_table[(trigger, src_state)] trans = transition_table.setdefault((trigger, src_state), []) trans.append((e, src_state, tgt)) transition_table[(trigger, src_state)] = trans # output hdr hdr.start(include_guard=include_guard, classname=classname) for i, s in enumerate(all_gen_info): hdr.enum_value(name=s.name, id=i) hdr.between_enum_and_triggers(state_count=len(all_states), classname=classname) for t in sorted(triggers): hdr.triggerdecl(trigger=t) hdr.end(include_guard=include_guard) # output cpp cpp.start(filename=filename, classname=classname) for s in all_gen_info: cpp.case(label=s.name) if s.exit_action: if s.exit_guard: cpp.guarded_action(guard=s.exit_guard, action=s.exit_action) else: cpp.action(action=s.exit_action) cpp.case_break() cpp.between_exit_and_enter() for s in all_gen_info: cpp.case(label=s.name) if s.entry_action: if s.entry_guard: cpp.guarded_action(guard=s.entry_guard, action=s.entry_action) else: cpp.action(action=s.entry_action) cpp.case_break() cpp.setstate_end() cpp.do_start(classname=classname) for s in all_gen_info: cpp.case(label=s.name) if s.do_action: if s.do_guard: cpp.guarded_action(guard=s.do_guard, action=s.do_action) else: cpp.action(action=s.do_action) cpp.case_break() cpp.do_end() for t in sorted(triggers): cpp.triggerdef_start(classname=classname, trigger=t) for o in all_states: translist = transition_table.setdefault((t, o), []) if not translist: translist = transition_table.setdefault(("otherwise", o), []) if translist: cpp.case(label=dia_dict(o).name) unguarded = None for (edge, src, tgt) in sorted(translist, key=lambda (edge, src, tgt): edge.properties["guard"].value): # Multiple edges with the same trigger (but different guards) # must go into the same "case". if edge.properties["guard"].value: cpp.triggerdef_guard_start(guard=edge.properties["guard"].value) if edge.properties["action"].value: cpp.triggerdef_guarded_action(action=edge.properties["action"].value) if src != tgt: cpp.triggerdef_guarded_action( action=sub.triggerdef_setstate(state=dia_dict(tgt).name)) cpp.triggerdef_guard_end() elif unguarded: cpp.error_multiple_unguarded_transitions() else: unguarded = (edge, src, tgt) if unguarded: (edge, src, tgt) = unguarded if edge.properties["action"].value: cpp.triggerdef_action(action=edge.properties["action"].value) if src != tgt: cpp.triggerdef_action( action=sub.triggerdef_setstate(state=dia_dict(tgt).name)) cpp.case_break() cpp.triggerdef_end() cpp.end(classname=classname) f_cpp.close() f_hdr.close() def is_uml_state(type): return (type == "UML - State") or (type == "UML - State Term") or (type == "UML - State Cluster") class StateRenderer : "Transforms a diagram to a C++ State-Chart" def __init__(self) : pass def begin_render (self, data, filename) : normal_states = [] start_states = [] final_states = [] clusters = [] edges = [] errors = [] for layer in data.layers : # for the moment ignore layer info. But we could use this to spread accross different files for o in layer.objects : if o.type.name == "UML - State": normal_states.append(o) elif o.type.name == "UML - State Term": if o.properties["is_final"].value: final_states.append(o) else: start_states.append(o) elif o.type.name == "UML - Transition" : t = "Transition %s " % transition_label(o) c_src = o.handles[0].connected_to c_tgt = o.handles[1].connected_to if not (c_src and c_tgt): errors.append("%s is not connected on both sides" % t) else: src = c_src.object tgt = c_tgt.object if not is_uml_state(src.type.name): errors.append("The source of %s is a %s" % (t, src.type.name)) elif not is_uml_state(tgt.type.name): errors.append("The target of %s is a %s" % (t, tgt.type.name)) else: edges.append((o, src, tgt)) elif o.type.name == "UML - State Cluster": clusters.append(o) #print start_states[0].properties.keys() output(filename, start_states, final_states, normal_states, clusters, edges, errors) def end_render(self) : # do cleanup here if you have set class members State_reset() # dia-python keeps a reference to the renderer class and uses it on demand dia.register_export ("State-Chart Code Generation (C++)", "cpp", StateRenderer())