od.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253
  1. from services import od
  2. from api import cd
  3. from services.bottom.V0 import Bottom
  4. from uuid import UUID
  5. from typing import Optional
  6. NEXT_ID = 0
  7. # Models map names to elements
  8. # This builds the inverse mapping, so we can quickly lookup the name of an element
  9. def build_name_mapping(state, m):
  10. mapping = {}
  11. bottom = Bottom(state)
  12. for name in bottom.read_keys(m):
  13. element, = bottom.read_outgoing_elements(m, name)
  14. mapping[element] = name
  15. return mapping
  16. class NoSuchSlotException(Exception):
  17. pass
  18. # Object Diagram API
  19. # Intended to replace the 'services.od.OD' class eventually
  20. class ODAPI:
  21. def __init__(self, state, m: UUID, mm: UUID):
  22. self.state = state
  23. self.bottom = Bottom(state)
  24. self.m = m
  25. self.mm = mm
  26. self.od = od.OD(mm, m, state)
  27. self.cdapi = cd.CDAPI(state, mm)
  28. self.create_boolean_value = self.od.create_boolean_value
  29. self.create_integer_value = self.od.create_integer_value
  30. self.create_string_value = self.od.create_string_value
  31. self.create_actioncode_value = self.od.create_actioncode_value
  32. self.__recompute_mappings()
  33. # Called after every change - makes querying faster but modifying slower
  34. def __recompute_mappings(self):
  35. self.m_obj_to_name = build_name_mapping(self.state, self.m)
  36. self.mm_obj_to_name = build_name_mapping(self.state, self.mm)
  37. self.type_to_objs = { type_name : set() for type_name in self.bottom.read_keys(self.mm)}
  38. for m_name in self.bottom.read_keys(self.m):
  39. m_element, = self.bottom.read_outgoing_elements(self.m, m_name)
  40. tm_element = self.get_type(m_element)
  41. if tm_element in self.mm_obj_to_name:
  42. tm_name = self.mm_obj_to_name[tm_element]
  43. # self.obj_to_type[m_name] = tm_name
  44. self.type_to_objs[tm_name].add(m_name)
  45. def get_value(self, obj: UUID):
  46. return od.read_primitive_value(self.bottom, obj, self.mm)[0]
  47. def get_target(self, link: UUID):
  48. return self.bottom.read_edge_target(link)
  49. def get_source(self, link: UUID):
  50. return self.bottom.read_edge_source(link)
  51. def get_slot(self, obj: UUID, attr_name: str):
  52. slot = self.od.get_slot(obj, attr_name)
  53. if slot == None:
  54. raise NoSuchSlotException(f"Object '{self.m_obj_to_name[obj]}' has no slot '{attr_name}'")
  55. return slot
  56. def get_slot_link(self, obj: UUID, attr_name: str):
  57. return self.od.get_slot_link(obj, attr_name)
  58. # Parameter 'include_subtypes': whether to include subtypes of the given association
  59. def get_outgoing(self, obj: UUID, assoc_name: str, include_subtypes=True):
  60. outgoing = self.bottom.read_outgoing_edges(obj)
  61. result = []
  62. for o in outgoing:
  63. try:
  64. type_of_outgoing_link = self.get_type_name(o)
  65. except:
  66. continue # OK, not all edges are typed
  67. if (include_subtypes and self.cdapi.is_subtype(super_type_name=assoc_name, sub_type_name=type_of_outgoing_link)
  68. or not include_subtypes and type_of_outgoing_link == assoc_name):
  69. result.append(o)
  70. return result
  71. # Parameter 'include_subtypes': whether to include subtypes of the given association
  72. def get_incoming(self, obj: UUID, assoc_name: str, include_subtypes=True):
  73. incoming = self.bottom.read_incoming_edges(obj)
  74. result = []
  75. for i in incoming:
  76. try:
  77. type_of_incoming_link = self.get_type_name(i)
  78. except:
  79. continue # OK, not all edges are typed
  80. if (include_subtypes and self.cdapi.is_subtype(super_type_name=assoc_name, sub_type_name=type_of_incoming_link)
  81. or not include_subtypes and type_of_incoming_link == assoc_name):
  82. result.append(i)
  83. return result
  84. def get_all_instances(self, type_name: str, include_subtypes=True):
  85. if include_subtypes:
  86. all_types = self.cdapi.transitive_sub_types[type_name]
  87. else:
  88. all_types = set([type_name])
  89. obj_names = [obj_name for type_name in all_types for obj_name in self.type_to_objs[type_name]]
  90. return [(obj_name, self.bottom.read_outgoing_elements(self.m, obj_name)[0]) for obj_name in obj_names]
  91. def get_type(self, obj: UUID):
  92. types = self.bottom.read_outgoing_elements(obj, "Morphism")
  93. if len(types) != 1:
  94. raise Exception(f"Expected obj to have 1 type, instead got {len(types)} types.")
  95. return types[0]
  96. def get_name(self, obj: UUID):
  97. return (
  98. [name for name in self.bottom.read_keys(self.m) if self.bottom.read_outgoing_elements(self.m, name)[0] == obj] +
  99. [name for name in self.bottom.read_keys(self.mm) if self.bottom.read_outgoing_elements(self.mm, name)[0] == obj]
  100. )[0]
  101. def get(self, name: str):
  102. results = self.bottom.read_outgoing_elements(self.m, name)
  103. if len(results) == 1:
  104. return results[0]
  105. elif len(results) >= 2:
  106. raise Exception("this should never happen")
  107. else:
  108. raise Exception(f"No such element in model: '{name}'")
  109. def get_type_name(self, obj: UUID):
  110. return self.get_name(self.get_type(obj))
  111. def is_instance(obj: UUID, type_name: str, include_subtypes=True):
  112. typ = self.cdapi.get_type(type_name)
  113. types = set(typ) if not include_subtypes else self.cdapi.transitive_sub_types[type_name]
  114. for type_of_obj in self.bottom.read_outgoing_elements(obj, "Morphism"):
  115. if type_of_obj in types:
  116. return True
  117. return False
  118. def delete(self, obj: UUID):
  119. self.bottom.delete_element(obj)
  120. self.__recompute_mappings()
  121. # Does the class of the object have the given attribute?
  122. def has_slot(self, obj: UUID, attr_name: str):
  123. class_name = self.get_name(self.get_type(obj))
  124. return self.od.get_attr_link_name(class_name, attr_name) != None
  125. def get_slot_value(self, obj: UUID, attr_name: str):
  126. slot = self.get_slot(obj, attr_name)
  127. return self.get_value(slot)
  128. # Returns the given default value if the slot does not exist on the object.
  129. # The attribute must exist in the object's class, or an exception will be thrown.
  130. # The slot may not exist however, if the attribute is defined as 'optional' in the class.
  131. def get_slot_value_default(self, obj: UUID, attr_name: str, default: any):
  132. try:
  133. return self.get_slot_value(obj, attr_name)
  134. except NoSuchSlotException:
  135. return default
  136. # create or update slot value
  137. def set_slot_value(self, obj: UUID, attr_name: str, new_value: any, is_code=False):
  138. obj_name = self.get_name(obj)
  139. link_name = f"{obj_name}_{attr_name}"
  140. target_name = f"{obj_name}.{attr_name}"
  141. old_slot_link = self.get_slot_link(obj, attr_name)
  142. if old_slot_link != None:
  143. old_target = self.get_target(old_slot_link)
  144. # if old_target != None:
  145. self.bottom.delete_element(old_target) # this also deletes the slot-link
  146. new_target = self.create_primitive_value(target_name, new_value, is_code)
  147. slot_type = self.cdapi.find_attribute_type(self.get_type_name(obj), attr_name)
  148. new_link = self.od._create_link(link_name, slot_type, obj, new_target)
  149. self.__recompute_mappings()
  150. def create_primitive_value(self, name: str, value: any, is_code=False):
  151. # watch out: in Python, 'bool' is subtype of 'int'
  152. # so we must check for 'bool' first
  153. if isinstance(value, bool):
  154. tgt = self.create_boolean_value(name, value)
  155. elif isinstance(value, int):
  156. tgt = self.create_integer_value(name, value)
  157. elif isinstance(value, str):
  158. if is_code:
  159. tgt = self.create_actioncode_value(name, value)
  160. else:
  161. tgt = self.create_string_value(name, value)
  162. else:
  163. raise Exception("Unimplemented type "+value)
  164. self.__recompute_mappings()
  165. return tgt
  166. def create_link(self, link_name: Optional[str], assoc_name: str, src: UUID, tgt: UUID):
  167. global NEXT_ID
  168. types = self.bottom.read_outgoing_elements(self.mm, assoc_name)
  169. if len(types) == 0:
  170. raise Exception(f"No such association: '{assoc_name}'")
  171. elif len(types) >= 2:
  172. raise Exception(f"More than one association exists with name '{assoc_name}' - this means the MM is invalid.")
  173. typ = types[0]
  174. if link_name == None:
  175. link_name = f"__{assoc_name}{NEXT_ID}"
  176. NEXT_ID += 1
  177. link_id = self.od._create_link(link_name, typ, src, tgt)
  178. self.__recompute_mappings()
  179. return link_id
  180. def create_object(self, object_name: Optional[str], class_name: str):
  181. return self.od.create_object(object_name, class_name)
  182. # internal use
  183. # Get API methods as bound functions, to pass as globals to 'eval'
  184. # Readonly version is used for:
  185. # - Conformance checking
  186. # - Pattern matching (LHS/NAC of rule)
  187. def bind_api_readonly(odapi):
  188. funcs = {
  189. 'read_value': odapi.state.read_value,
  190. 'get': odapi.get,
  191. 'get_value': odapi.get_value,
  192. 'get_target': odapi.get_target,
  193. 'get_source': odapi.get_source,
  194. 'get_slot': odapi.get_slot,
  195. 'get_slot_value': odapi.get_slot_value,
  196. 'get_slot_value_default': odapi.get_slot_value_default,
  197. 'get_all_instances': odapi.get_all_instances,
  198. 'get_name': odapi.get_name,
  199. 'get_type_name': odapi.get_type_name,
  200. 'get_outgoing': odapi.get_outgoing,
  201. 'get_incoming': odapi.get_incoming,
  202. 'has_slot': odapi.has_slot,
  203. }
  204. return funcs
  205. # internal use
  206. # Get API methods as bound functions, to pass as globals to 'eval'
  207. # Read/write version is used for:
  208. # - Graph rewriting (RHS of rule)
  209. def bind_api(odapi):
  210. funcs = {
  211. **bind_api_readonly(odapi),
  212. 'create_object': odapi.create_object,
  213. 'create_link': odapi.create_link,
  214. 'delete': odapi.delete,
  215. 'set_slot_value': odapi.set_slot_value,
  216. }
  217. return funcs