composite_activity.py 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273
  1. from uuid import UUID
  2. from api.od import ODAPI
  3. from examples.ftg_pm_pt.ftg_pm_pt import FtgPmPt
  4. from examples.ftg_pm_pt.runner import FtgPmPtRunner
  5. def find_previous_artefact(od: ODAPI, linked_artefacts):
  6. return next((od.get_source(link) for link in linked_artefacts if
  7. not od.get_incoming(od.get_source(link), "pt_PrevVersion")), None)
  8. def create_activity_links(od: ODAPI, activity, prev_element, ctrl_port, end_trace=None,
  9. relation_type="pt_IsFollowedBy"):
  10. od.create_link(None, "pt_RelatesTo", activity, ctrl_port)
  11. od.create_link(None, relation_type, prev_element, activity)
  12. if end_trace:
  13. od.create_link(None, "pt_IsFollowedBy", activity, end_trace)
  14. def get_workflow_path(od: ODAPI, activity: UUID):
  15. return od.get_slot_value(activity, "subworkflow_path")
  16. def get_workflow(workflow_path: str):
  17. with open(workflow_path, "r") as f:
  18. return f.read()
  19. ############################
  20. def get_runtime_state(od: ODAPI, design_obj: UUID):
  21. states = od.get_incoming(design_obj, "pm_Of")
  22. if len(states) == 0:
  23. print(f"Design object '{od.get_name(design_obj)}' has no runtime state.")
  24. return None
  25. return od.get_source(states[0])
  26. def get_source_incoming(od: ODAPI, obj: UUID, link_name: str):
  27. links = od.get_incoming(obj, link_name)
  28. if len(links) == 0:
  29. print(f"Object '{od.get_name(obj)} has no incoming links of type '{link_name}'.")
  30. return None
  31. return od.get_source(links[0])
  32. def get_target_outgoing(od: ODAPI, obj: UUID, link_name: str):
  33. links = od.get_outgoing(obj, link_name)
  34. if len(links) == 0:
  35. print(f"Object '{od.get_name(obj)} has no outgoing links of type '{link_name}'.")
  36. return None
  37. return od.get_target(links[0])
  38. def set_control_port_value(od: ODAPI, port: UUID, value: bool):
  39. state = get_runtime_state(od, port)
  40. od.set_slot_value(state, "active", value)
  41. def set_artefact_data(od: ODAPI, artefact: UUID, value: bytes):
  42. state = artefact
  43. # Only the proces model of the artefact contains a runtime state
  44. if od.get_type_name(state) == "pm_Artefact":
  45. state = get_runtime_state(od, artefact)
  46. od.set_slot_value(state, "data", value)
  47. def get_artefact_data(od: ODAPI, artefact):
  48. state = artefact
  49. # Only the proces model of the artefact contains a runtime state
  50. if od.get_type_name(state) == "pm_Artefact":
  51. state = get_runtime_state(od, artefact)
  52. return od.get_slot_value(state, "data")
  53. ############################
  54. def set_workflow_control_source(workflow_model: FtgPmPt, ctrl_port_name: str, composite_linkage: dict):
  55. od = workflow_model.odapi
  56. source_port_name = composite_linkage[ctrl_port_name]
  57. source_port = od.get(source_port_name)
  58. set_control_port_value(od, source_port, True)
  59. def set_workflow_artefacts(act_od: ODAPI, activity: UUID, workflow_model: FtgPmPt, composite_linkage: dict):
  60. for data_port in [act_od.get_target(data_in) for data_in in act_od.get_outgoing(activity, "pm_HasDataIn")]:
  61. # Get the data source port of the inner workflow
  62. data_port_name = act_od.get_name(data_port)
  63. source_port_name = composite_linkage[data_port_name]
  64. source_port = workflow_model.odapi.get(source_port_name)
  65. # Get the artefact that is linked to the data port of the activity
  66. act_artefact = get_source_incoming(act_od, data_port, "pm_DataFlowOut")
  67. # Get the data of the artefact
  68. artefact_data = get_artefact_data(act_od, act_artefact)
  69. # Get the artefact that is linked to the data port of the inner workflow
  70. workflow_artefact = get_target_outgoing(workflow_model.odapi, source_port, "pm_DataFlowIn")
  71. set_artefact_data(workflow_model.odapi, workflow_artefact, artefact_data)
  72. def get_activity_port_from_inner_port(composite_linkage: dict, port_name: str):
  73. for act_port_name, work_port_name in composite_linkage.items():
  74. if work_port_name == port_name:
  75. return act_port_name
  76. def execute_composite_workflow(od: ODAPI, activity: UUID, ctrl_port: UUID, composite_linkage: dict,
  77. packages: dict | None, matched=None):
  78. activity_name = od.get_slot_value(activity, "name")
  79. # First get the path of the object diagram file that contains the inner workflow of the activity
  80. workflow_path = get_workflow_path(od, activity)
  81. # Read the object diagram file
  82. workflow = get_workflow(workflow_path)
  83. # Create an FtgPmPt object
  84. workflow_model = FtgPmPt(activity_name)
  85. # Load the workflow to the object
  86. workflow_model.load_model(workflow)
  87. # Set the correct control source port of the workflow to active
  88. set_workflow_control_source(workflow_model, od.get_name(ctrl_port), composite_linkage[activity_name])
  89. # If a data port is linked, set the data of the artefact
  90. set_workflow_artefacts(od, activity, workflow_model, composite_linkage[activity_name])
  91. # Create an FtgPmPtRunner object with the FtgPmPt object
  92. workflow_runner = FtgPmPtRunner(workflow_model)
  93. # Set the packages if present
  94. workflow_runner.set_packages(packages, is_path=False)
  95. # Run the FtgPmPtRunner (is a subprocess necessary? This makes it more complicated because now we have direct access to the object)
  96. workflow_runner.run()
  97. # Contains all the ports of the inner workflow -> map back to the activity ports, and so we can set the correct
  98. # Control ports to active and also set the data artefacts correctly
  99. ports = extract_inner_workflow(workflow_model.odapi)
  100. start_act = None
  101. end_act = None
  102. for port in [port for port in ports if port]:
  103. port_name = workflow_model.odapi.get_name(port)
  104. activity_port_name = get_activity_port_from_inner_port(composite_linkage[activity_name], port_name)
  105. activity_port = od.get(activity_port_name)
  106. match workflow_model.odapi.get_type_name(port):
  107. case "pm_CtrlSource":
  108. start_act = handle_control_source(od, activity_port, matched("prev_trace_element"))
  109. case "pm_CtrlSink":
  110. end_act = handle_control_sink(od, activity_port, start_act, matched("end_trace"))
  111. case "pm_DataSource":
  112. handle_data_source(od, activity_port, start_act)
  113. case "pm_DataSink":
  114. handle_data_sink(od, workflow_model.odapi, activity_port, port, end_act)
  115. def handle_control_source(od: ODAPI, port, prev_trace_elem):
  116. set_control_port_value(od, port, False)
  117. start_activity = od.create_object(None, "pt_StartActivity")
  118. create_activity_links(od, start_activity, prev_trace_elem, port)
  119. return start_activity
  120. def handle_control_sink(od: ODAPI, port, start_act, end_trace):
  121. set_control_port_value(od, port, True)
  122. end_activity = od.create_object(None, "pt_EndActivity")
  123. create_activity_links(od, end_activity, start_act, port, end_trace)
  124. return end_activity
  125. def handle_data_source(od: ODAPI, port, start_activity):
  126. pt_artefact = od.create_object(None, "pt_Artefact")
  127. od.create_link(None, "pt_Consumes", pt_artefact, start_activity)
  128. pm_artefact = get_source_incoming(od, port, "pm_DataFlowOut")
  129. pm_artefact_data = get_artefact_data(od, pm_artefact)
  130. set_artefact_data(od, pt_artefact, pm_artefact_data)
  131. prev_pt_artefact = find_previous_artefact(od, od.get_incoming(pm_artefact, "pt_BelongsTo"))
  132. if prev_pt_artefact:
  133. od.create_link(None, "pt_PrevVersion", pt_artefact, prev_pt_artefact)
  134. od.create_link(None, "pt_BelongsTo", pt_artefact, pm_artefact)
  135. def handle_data_sink(act_od: ODAPI, work_od: ODAPI, act_port, work_port, end_activity):
  136. pt_artefact = act_od.create_object(None, "pt_Artefact")
  137. act_od.create_link(None, "pt_Produces", end_activity, pt_artefact)
  138. work_artefact = get_source_incoming(work_od, work_port, "pm_DataFlowOut")
  139. work_artefact_data = get_artefact_data(work_od, work_artefact)
  140. act_artefact = get_target_outgoing(act_od, act_port, "pm_DataFlowIn")
  141. set_artefact_data(act_od, act_artefact, work_artefact_data)
  142. set_artefact_data(act_od, pt_artefact, work_artefact_data)
  143. prev_pt_artefact = find_previous_artefact(act_od, act_od.get_incoming(act_artefact, "pt_BelongsTo"))
  144. if prev_pt_artefact:
  145. act_od.create_link(None, "pt_PrevVersion", pt_artefact, prev_pt_artefact)
  146. act_od.create_link(None, "pt_BelongsTo", pt_artefact, act_artefact)
  147. def extract_inner_workflow(workflow: ODAPI):
  148. # Get the model, this should be only one
  149. name, model = workflow.get_all_instances("pm_Model")[0]
  150. # Get the start of the process trace
  151. start_trace = get_source_incoming(workflow, model, "pt_Starts")
  152. # Get the end of the process trace
  153. end_trace = get_source_incoming(workflow, model, "pt_Ends")
  154. # Get the first started activity
  155. first_activity = get_target_outgoing(workflow, start_trace, "pt_IsFollowedBy")
  156. # Get the last ended activity
  157. end_activity = get_source_incoming(workflow, end_trace, "pt_IsFollowedBy")
  158. # Get the control port that started the activity
  159. act_ctrl_in = get_target_outgoing(workflow, first_activity, "pt_RelatesTo")
  160. # Get the control port that is activated when the activity is executed
  161. act_ctrl_out = get_target_outgoing(workflow, end_activity, "pt_RelatesTo")
  162. # Get the control source of the workflow
  163. ports = []
  164. for port in workflow.get_incoming(act_ctrl_in, "pm_CtrlFlow"):
  165. source = workflow.get_source(port)
  166. if workflow.get_type_name(source) == "pm_CtrlSource":
  167. # Only one port can activate an activity
  168. ports.append(source)
  169. break
  170. # Get the control sink of the workflow
  171. for port in workflow.get_outgoing(act_ctrl_out, "pm_CtrlFlow"):
  172. sink = workflow.get_target(port)
  173. if workflow.get_type_name(sink) == "pm_CtrlSink":
  174. # Only one port can be set to active one an activity is ended
  175. ports.append(sink)
  176. break
  177. # Get the data port that the activity consumes (if used)
  178. consumed_links = workflow.get_incoming(first_activity, "pt_Consumes")
  179. if len(consumed_links) > 0:
  180. pt_artefact = None
  181. for link in consumed_links:
  182. pt_artefact = workflow.get_source(link)
  183. # Check if it is the first artefact -> contains no previous version
  184. if len(workflow.get_outgoing(pt_artefact, "pt_PrevVersion")) == 0:
  185. break
  186. pm_artefact = get_target_outgoing(workflow, pt_artefact, "pt_BelongsTo")
  187. # Find the data source port
  188. for link in workflow.get_incoming(pm_artefact, "pm_DataFlowIn"):
  189. source = workflow.get_source(link)
  190. if workflow.get_type_name(source) == "pm_DataSource":
  191. # An activity can only use one artefact as input
  192. ports.append(source)
  193. break
  194. # Get all data ports that are connected to an artefact that is produced by an activity in the workflow,
  195. # where the artefact is also part of main workflow
  196. for port_name, data_sink in workflow.get_all_instances("pm_DataSink"):
  197. pm_art = get_source_incoming(workflow, data_sink, "pm_DataFlowOut")
  198. # If the pm_artefact is linked to a proces trace artefact that is produced, we can add to port
  199. links = workflow.get_incoming(pm_art, "pt_BelongsTo")
  200. if not len(links):
  201. continue
  202. # A data sink port linkage will only be added to the proces trace when an activity is ended and so an artefact
  203. # is produced, meaning that if a belongsTo link exists, a proces trace artefact is linked to this data port
  204. ports.append(data_sink)
  205. return ports