1 | % (c) 2009-2024 Lehrstuhl fuer Softwaretechnik und Programmiersprachen, | |
2 | % Heinrich Heine Universitaet Duesseldorf | |
3 | % This software is licenced under EPL 1.0 (http://www.eclipse.org/org/documents/epl-v10.html) | |
4 | ||
5 | :- module(uml_generator,[write_uml_sequence_chart_all_states/1, | |
6 | write_uml_sequence_chart/1]). | |
7 | ||
8 | :- use_module(probsrc(module_information)). | |
9 | :- module_info(group,dot). | |
10 | :- module_info(description,'This module generates UML diagrams for PlantUML.'). | |
11 | ||
12 | :- use_module(library(lists)). | |
13 | ||
14 | :- use_module(probsrc(specfile)). | |
15 | :- use_module(probsrc(tcltk_interface)). | |
16 | :- use_module(probsrc(state_space), [transition/4, op_trace_ids/1, visited_expression/2]). | |
17 | :- use_module(probsrc(bmachine), [b_definition_prefixed/5, b_get_definition/5, type_with_errors/4]). | |
18 | :- use_module(probsrc(debug), [debug_println/2]). | |
19 | :- use_module(probsrc(translate), [translate_bvalue/2, translate_event_with_limit/3]). | |
20 | :- use_module(probsrc(eval_let_store),[extend_typing_scope_for_stored_lets/2]). | |
21 | :- use_module(probsrc(error_manager),[add_internal_error/2, add_error/3, add_message/4, add_warning/4]). | |
22 | :- use_module(probsrc(self_check)). | |
23 | ||
24 | ||
25 | % generate UML sequence chart from history for use with PlantUML | |
26 | write_uml_sequence_chart_all_states(Files) :- | |
27 | op_trace_ids(TIDs), | |
28 | length(Files, FLen), | |
29 | length(TIDs, TLen), | |
30 | (FLen == TLen -> true | |
31 | ; add_internal_error('Number of files does not match trace length.',write_uml_sequence_chart_all_states(Files))), | |
32 | write_uml_sequence_chart_all_states(Files,TIDs). | |
33 | write_uml_sequence_chart_all_states([],[]). | |
34 | write_uml_sequence_chart_all_states([File|T],[ID|IDT]) :- | |
35 | write_uml_sequence_chart(File,[ID|IDT]), | |
36 | write_uml_sequence_chart_all_states(T,IDT). | |
37 | ||
38 | write_uml_sequence_chart(File) :- | |
39 | op_trace_ids(TIDs), | |
40 | write_uml_sequence_chart(File,TIDs). | |
41 | write_uml_sequence_chart(File,TIDs) :- | |
42 | open(File,write,Stream), | |
43 | call_cleanup(print_uml_sequence_chart_aux(TIDs,Stream), | |
44 | close(Stream)). | |
45 | ||
46 | print_uml_sequence_chart_aux(TIDs,Stream) :- | |
47 | get_history_transition_terms(TIDs,Terms), | |
48 | format(Stream,'@startuml~n',[]), | |
49 | % format(Stream,'autonumber~n',[]), | |
50 | (last(transition_from(StateId,_),Terms) -> true ; StateId=root), | |
51 | print_uml_actors(Stream,StateId), | |
52 | maplist(print_seq_chart_transition(Stream),Terms), | |
53 | format(Stream,'@enduml~n',[]). | |
54 | ||
55 | :- use_module(probsrc(bmachine),[b_get_typed_expression_definition/3]). | |
56 | :- use_module(probsrc(custom_explicit_sets),[expand_custom_set_to_list/4]). | |
57 | :- use_module(probsrc(bsyntaxtree), [get_texpr_pos/2]). | |
58 | :- use_module(probsrc(state_space),[visited_expression/2]). | |
59 | :- use_module(probsrc(specfile),[expand_const_and_vars_to_full_store/2]). | |
60 | ||
61 | print_uml_actors(Stream,StateId) :- | |
62 | b_get_typed_expression_definition('SEQUENCE_CHART_ACTORS',[variables_and_additional_defs],AExpr), | |
63 | % should be constants instead of variables; TODO: evaluate in StateId | |
64 | get_texpr_pos(AExpr,Pos), | |
65 | visited_expression(StateId,State),expand_const_and_vars_to_full_store(State,BState), | |
66 | b_interpreter:b_compute_expression_nowf(AExpr,[],BState,Val,'SEQUENCE_CHART_ACTORS',0), | |
67 | expand_custom_set_to_list(Val,ExpandedVal,_Done,'SEQUENCE_CHART_ACTORS'), | |
68 | if(get_actor_fields(Fields,ExpandedVal), | |
69 | print_uml_actor(Stream,Fields,Pos), | |
70 | add_warning(uml_generator,'SEQUENCE_CHART_ACTORS should return set of records {rec(name:"N",kind:"actor",color:"green")}, not: ',Val,Pos)), | |
71 | fail. | |
72 | print_uml_actors(_,_). | |
73 | ||
74 | get_actor_fields(Fields,ExpandedVal) :- | |
75 | member(rec(Fields),ExpandedVal). | |
76 | get_actor_fields(Fields,ExpandedVal) :- | |
77 | member((int(_),rec(Fields)),ExpandedVal). % we can influence the order of actors by using a sequence | |
78 | ||
79 | :- use_module(probsrc(kernel_strings),[to_b_string/2]). | |
80 | print_uml_actor(Stream,Fields,Pos) :- | |
81 | (member(field(id,NN),Fields) -> true ; member(field(name,NN),Fields) -> true | |
82 | ; add_warning(uml_generator,'Missing name field: ',Kind,Pos), fail), | |
83 | to_b_string(NN,string(Name)), | |
84 | (member(field(color,CC),Fields) -> to_b_string(CC,string(Color)) ; Color=''), | |
85 | (member(field(kind,KK),Fields) | |
86 | -> to_b_string(KK,string(Kind)), | |
87 | (actor_kind(Kind) -> true ; add_warning(uml_generator,'Unknown actor kind: ',Kind,Pos)) | |
88 | ; Kind='participant'), | |
89 | (Color='' -> format(Stream,'~w ~w~n',[Kind,Name]) | |
90 | ; format(Stream,'~w ~w #~w~n',[Kind,Name,Color])). | |
91 | ||
92 | actor_kind(actor). | |
93 | actor_kind(boundary). | |
94 | actor_kind(control). | |
95 | actor_kind(entity). | |
96 | actor_kind(database). | |
97 | actor_kind(collections). | |
98 | actor_kind(queue). | |
99 | ||
100 | ||
101 | % try to find user-provided SEQUENCE_CHART definition for event, indicating the labels and arrow kind | |
102 | print_seq_chart_transition(Stream,transition_from(CurStateID,Term)) :- | |
103 | specfile:get_operation_name(Term,OpName), | |
104 | b_definition_prefixed(expression,'SEQUENCE_CHART_',OpName,DefName,_Pos), | |
105 | debug_println(19,found_def(DefName)), | |
106 | get_operation_arguments(Term,OpArgs), | |
107 | b_get_definition(DefName,expression,DefArgs,RawSubst,_Deps), | |
108 | arg(1,RawSubst,Pos), | |
109 | length(DefArgs,DefNrArgs), | |
110 | maplist(gen_raw_arg(Pos),OpArgs,RawArgs), | |
111 | (prefix_length(RawArgs,DefRawArgs,DefNrArgs) | |
112 | -> true % we allow the DEFINITION to take fewer arguments; possibly useful when ANY variables lifted to args | |
113 | ; add_warning(write_uml_sequence_chart,'DEFINITION has too many arguments: ',DefName/DefNrArgs,Pos), | |
114 | fail | |
115 | ), | |
116 | extend_typing_scope_for_stored_lets([prob_ids(visible),variables_and_additional_defs],Scope), | |
117 | %add_message(seq_chart,'Typing DEFINITION for UML Sequence Chart: ',DefName,Pos), | |
118 | type_with_errors(definition(Pos,DefName,DefRawArgs),Scope,_Type,TypedExpr), | |
119 | visited_expression(CurStateID,State), | |
120 | (state_corresponds_to_set_up_constants(State,BState) -> true | |
121 | ; BState=[] | |
122 | ), | |
123 | tcltk_interface:b_compute_expression_with_prob_ids(TypedExpr,BState,Value), | |
124 | process_uml_bvalue(Value,Term,DefName,Pos,Stream), | |
125 | !. | |
126 | print_seq_chart_transition(Stream,transition_from(_,Term)) :- | |
127 | specfile:get_operation_name(Term,OpName), | |
128 | specfile:get_operation_arguments(Term,OpArgs), length(OpArgs,Len), | |
129 | translate_event_with_limit(Term,100,TS), | |
130 | format('No SEQUENCE_CHART_~w/~w DEFINITION found for transition ~w~n',[OpName,Len,TS]), | |
131 | format(Stream,'main --> main: ~w~n',[TS]). | |
132 | ||
133 | % we support ("ActorFrom","ActorTo") | |
134 | % and ("ActorFrom","ArrowStyle","ActorTo") which corresponds to (("ActorFrom","ArrowStyle"),"ActorTo") | |
135 | % and ("ActorFrom","ArrowStyle","ArrowSuffix","ActorTo") | |
136 | % and rec(from:AF, to:AT, arrow:"-->", suffix: "") | |
137 | process_uml_bvalue(rec(Fields),OperationTerm,DefName,Pos,Stream) :- | |
138 | (select(field(from,ActorFrom),Fields,Fields1) | |
139 | -> translate_bvalue(ActorFrom,AF) | |
140 | ; add_message(uml_generator,'Record in definition has no \'from\' field: ',DefName,Pos), | |
141 | AF = '??', Fields1=Fields), | |
142 | (select(field(to,ActorTo),Fields1,Fields2) -> translate_bvalue(ActorTo,AT) | |
143 | ; add_message(uml_generator,'Record in definition has no \'to\' field: ',DefName,Pos), | |
144 | AT=AF, Fields2=Fields1), | |
145 | (member(field(arrow,string(ArrowStyle)),Fields2) | |
146 | -> (valid_plant_uml_arrow_style(ArrowStyle,Pos) -> true | |
147 | ; add_message(uml_generator,'Unrecognised arrow field: ',ArrowStyle,Pos) | |
148 | ) | |
149 | ; ArrowStyle='-->'), | |
150 | (member(field(suffix,SuffixVal),Fields2) | |
151 | -> to_b_string(SuffixVal,string(StrSuffix)), | |
152 | check_valid_plant_uml_arrow_suffix(StrSuffix,Suffix,Pos) | |
153 | ; Suffix=''), | |
154 | translate_event_with_limit(OperationTerm,100,TS), !, | |
155 | format(Stream,'~w ~w ~w ~w : ~w~n',[AF,ArrowStyle,AT, Suffix,TS]). | |
156 | process_uml_bvalue((From,ActorTo),OperationTerm,_,Pos,Stream) :- | |
157 | get_arrow_style(From,ActorFrom,ArrowStyle,Suffix,Pos), | |
158 | translate_bvalue(ActorFrom,AF), translate_bvalue(ActorTo,AT), | |
159 | translate_event_with_limit(OperationTerm,100,TS), !, | |
160 | format(Stream,'~w ~w ~w ~w : ~w~n',[AF,ArrowStyle,AT, Suffix,TS]). | |
161 | process_uml_bvalue(string(''),_,_,_,_Stream) :- !. % ignore this event | |
162 | process_uml_bvalue(string(Str),_,_,_,Stream) :- | |
163 | !, | |
164 | format(Stream, '~w~n', [Str]). % raw print, user has specified the whole line | |
165 | process_uml_bvalue(Value,_,_DefName,Pos,_) :- | |
166 | add_warning(write_uml_sequence_chart,'Unexpected value for UML sequence chart: ',Value,Pos), | |
167 | fail. | |
168 | ||
169 | ||
170 | get_arrow_style(((From,string(Arrow)),string(StrSuffix)),From,Arrow,Suffix,Pos) :- | |
171 | valid_plant_uml_arrow_style(Arrow,Pos), | |
172 | check_valid_plant_uml_arrow_suffix(StrSuffix,Suffix,Pos),!. | |
173 | get_arrow_style((From,string(Arrow)),From,Arrow,'',Pos) :- valid_plant_uml_arrow_style(Arrow,Pos),!. | |
174 | get_arrow_style(From,From,'-->','',_). | |
175 | ||
176 | ||
177 | % see https://plantuml.com/sequence-diagram | |
178 | valid_plant_uml_arrow_suffix(nothing,''). | |
179 | valid_plant_uml_arrow_suffix('',''). % nothing | |
180 | valid_plant_uml_arrow_suffix(activate,'++'). | |
181 | valid_plant_uml_arrow_suffix('++','++'). % activate the target; TO DO: can be followed by color | |
182 | valid_plant_uml_arrow_suffix(deactivate,'--'). | |
183 | valid_plant_uml_arrow_suffix('--','--'). % deactivate source | |
184 | valid_plant_uml_arrow_suffix(create,'**'). | |
185 | valid_plant_uml_arrow_suffix('**','**'). % Create an instance of the target | |
186 | valid_plant_uml_arrow_suffix(destroy,'!!'). | |
187 | valid_plant_uml_arrow_suffix('!!','!!'). % Destroy an instance of the target | |
188 | ||
189 | ||
190 | check_valid_plant_uml_arrow_suffix(Arrow,Res,_) :- valid_plant_uml_arrow_suffix(Arrow,R),!, Res=R. | |
191 | check_valid_plant_uml_arrow_suffix(Arrow,Res,Pos) :- | |
192 | add_message(write_uml_sequence_chart,'Unrecognized plantUML arrow suffix (use nothing,++,--,**,!!): ',Arrow,Pos), | |
193 | Res=Arrow. | |
194 | ||
195 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#green]>',unknown) ). | |
196 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#green]->',unknown) ). | |
197 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#blue]>',unknown) ). | |
198 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#red]>',unknown) ). | |
199 | :- assert_must_succeed( uml_generator:valid_plant_uml_arrow_style('-[#0000FF]>',unknown) ). | |
200 | ||
201 | % add a final x to denote a lost message | |
202 | % use \ or / instead of < or > to have only the bottom or top part of the arrow | |
203 | % repeat the arrow head (for example, >> or //) head to have a thin drawing | |
204 | % use -- instead of - to have a dotted arrow | |
205 | % add a final "o" at arrow head | |
206 | % use bidirectional arrow <-> | |
207 | valid_plant_uml_arrow_style('-->',_). | |
208 | valid_plant_uml_arrow_style('->',_). | |
209 | valid_plant_uml_arrow_style('->x',_). | |
210 | valid_plant_uml_arrow_style('->>',_). | |
211 | valid_plant_uml_arrow_style('-\\',_). | |
212 | valid_plant_uml_arrow_style('\\\\-',_). | |
213 | valid_plant_uml_arrow_style('//--',_). | |
214 | valid_plant_uml_arrow_style('->o',_). | |
215 | valid_plant_uml_arrow_style('o\\\\--',_). | |
216 | valid_plant_uml_arrow_style('<->',_). | |
217 | valid_plant_uml_arrow_style('<->o',_). | |
218 | valid_plant_uml_arrow_style('<-',_). | |
219 | valid_plant_uml_arrow_style('<--',_). % TO DO: other reverse arrows | |
220 | % TODO: things like o<<--[#green]>o are also allowed | |
221 | valid_plant_uml_arrow_style(Arrow,Pos) :- atom_codes(Arrow,Codes), | |
222 | % detect patterns like '-[#green]->' | |
223 | Codes = [0'-, 0'[ | RestCodes], | |
224 | append(ColorCodes,[0']|Rest],RestCodes), | |
225 | ? | valid_end_arrow_codes(Rest),!, |
226 | ? | (valid_plant_uml_color_codes(ColorCodes) -> true |
227 | ; add_message(write_uml_sequence_chart,'Unrecognized plantUML color in arrow: ',Arrow,Pos)). | |
228 | valid_plant_uml_arrow_style(Arrow,Pos) :- | |
229 | add_message(write_uml_sequence_chart,'Unrecognized plantUML arrow: ',Arrow,Pos). | |
230 | ||
231 | valid_end_arrow_codes(">"). | |
232 | valid_end_arrow_codes("->"). | |
233 | ||
234 | % from https://plantuml.com/color | |
235 | % You can use specify fill and line colors either: | |
236 | % with its standard name or CSS name | |
237 | % using HEX value (6 digits): #RRGGBB | |
238 | % using HEX value (8 digits) with alpha compositing or RGBA color model: #RRGGBBaa | |
239 | % using short HEX value (3 digits): #RGB (so #ABC means #AABBCC) | |
240 | % using very short HEX value (1 digits): #x which is a shortcut for #xxxxxx (so you get some gray) | |
241 | % A special color values: transparent can be used, synonym of transparent black (#00000000). | |
242 | % see also https://www.w3schools.com/colors/colors_names.asp | |
243 | % TODO: support gradients | |
244 | ||
245 | :- use_module(probsrc(tools_matching),[is_svg_color_name/1]). | |
246 | valid_plant_uml_color_codes([0'#|TC]) :- % accept things like #green | |
247 | atom_codes(Color,TC), | |
248 | (maplist(hex_code,TC) -> true %we do not check length is 1,3,6,8 | |
249 | ; is_svg_color_name(Color) | |
250 | ; Color = transparent). | |
251 | ||
252 | hex_code(C) :- C >= 48, C =< 57, !. | |
253 | hex_code(C) :- C >= 97, C =< 102, !. | |
254 | hex_code(C) :- C >= 65, C =< 70, !. | |
255 | ||
256 | ||
257 | % gen a raw value argument to pass to a DEFINITION call | |
258 | gen_raw_arg(Pos,Val,value(Pos,Val)). | |
259 | ||
260 | ||
261 | % get current history as transition terms with starting state id: | |
262 | get_history_transition_terms(IDT,Terms) :- | |
263 | reverse(IDT,RIDT), | |
264 | get_transition_terms(RIDT,root,Terms). | |
265 | ||
266 | get_transition_terms([],_CurrentState,[]). | |
267 | get_transition_terms([TransId|Rest],CurID,[transition_from(CurID,Term)|TRes]) :- | |
268 | transition(CurID,Term,TransId,DestID),!, | |
269 | get_transition_terms(Rest,DestID,TRes). | |
270 | get_transition_terms([TransId|_],CurrentState,_) :- | |
271 | add_error(get_transition_terms,'Could not execute transition id: ', TransId:from(CurrentState)),fail. |