mapping.py 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504
  1. import odml
  2. import odml.terminology as terminology
  3. import weakref
  4. from functools import wraps
  5. # late import, as the proxy module relies on the latest available
  6. # concrete odml-class implementations, that would not be available if they
  7. # were directly imported in the beginning
  8. proxy = None
  9. class mapped(object):
  10. """
  11. Keeps information for objects that can have a current mapped object associated
  12. with them
  13. """
  14. __active_mapping = None
  15. @property
  16. def _active_mapping(self):
  17. if self.__active_mapping is None: return None
  18. return self.__active_mapping()
  19. @_active_mapping.setter
  20. def _active_mapping(self, new_value):
  21. self.__active_mapping = weakref.ref(new_value)
  22. @_active_mapping.deleter
  23. def _active_mapping(self):
  24. if not self.__active_mapping is None:
  25. del self.__active_mapping
  26. class mapable(mapped):
  27. """
  28. Provides assisting functionality for objects that support
  29. the mapping attribute (i.e. sections and properties)
  30. """
  31. def get_mapping(self):
  32. """
  33. returns the current valid mapping for this section / property
  34. which may be defined by the object itsself, by its terminology equivalent
  35. or inherited from its parent
  36. """
  37. mapping = self.mapping
  38. if mapping is None:
  39. if self.parent.parent is not None: # i.e. not the root node
  40. return self.parent.get_mapping()
  41. return (self, mapping)
  42. @staticmethod
  43. def parse_mapping(url):
  44. """
  45. parse u url of format http://host/file.xml#section_type:property_name
  46. returns a 3-tuple: (url, stype, prop_name)
  47. where stype or prop_name may be None
  48. """
  49. stype = None
  50. prop_name = None
  51. if '#' in url:
  52. url, stype = url.split('#', 1)
  53. if ':' in stype:
  54. stype, prop_name = stype.split(':', 1)
  55. terminology.deferred_load(url)
  56. return (url, stype, prop_name)
  57. @staticmethod
  58. def get_mapping_object(url):
  59. """
  60. returns the object instance a mapping-url points to
  61. """
  62. url, stype, prop_name = mapable.parse_mapping(url)
  63. term = terminology.load(url)
  64. if term is None:
  65. raise MappingError("Terminology '%s' could not be loaded" % url)
  66. if stype is None:
  67. return term.sections[0]
  68. sec = term.find_related(type=stype)
  69. if sec is None:
  70. raise MappingError("No section of type '%s' could be found" % stype)
  71. if prop_name is None:
  72. return sec
  73. try:
  74. return sec.properties[prop_name]
  75. except KeyError:
  76. raise MappingError("No property named '%s' could be found in section '%s'" % (prop_name, sec.name))
  77. @property
  78. def mapped_object(self):
  79. """
  80. if there is a mapping defined (or inherited), returns the corresponding
  81. mapping object
  82. """
  83. url = self.mapping
  84. if url is not None:
  85. return self.get_mapping_object(url)
  86. return None
  87. #if self.parent is not None: # i.e. not the root node
  88. # obj = self.parent.mapped_object
  89. # if obj is not None:
  90. # return obj.contains(self)
  91. @property
  92. def mapping(self):
  93. """
  94. return the applicable mapping-url (which may be provided by the terminology)
  95. """
  96. if self._mapping is None:
  97. term = self.get_terminology_equivalent()
  98. if term is not None:
  99. return term._mapping
  100. return self._mapping
  101. @mapping.setter
  102. def mapping(self, new_value):
  103. if new_value == '':
  104. new_value = None
  105. term = self.get_terminology_equivalent()
  106. if term is not None and term._mapping == new_value:
  107. new_value = None
  108. if self._mapping == new_value: return
  109. remap = None
  110. # TODO should save the old index when unmapping and then use insert to
  111. # (re)introduce the new mapping, to keep order intact
  112. if self._active_mapping is not None:
  113. remap = self.unmap()
  114. self._mapping = new_value
  115. if remap is not None:
  116. self.remap(remap)
  117. def unmap(self):
  118. """
  119. unestablish the mapping for this object
  120. """
  121. raise NotImplementedError
  122. def remap(self, mobj):
  123. """
  124. reestablish the mapping for this object
  125. """
  126. raise NotImplementedError
  127. class mapableProperty(mapable):
  128. def unmap(self):
  129. """
  130. uninstall the property mapping by removing its proxy object from its mapped section
  131. """
  132. return unmap_property(prop=self)
  133. def remap(self, mprop):
  134. """
  135. install the mapping for this property
  136. """
  137. create_property_mapping(self.parent, self)
  138. class mapableSection(mapable):
  139. def unmap(self):
  140. """
  141. recursively unmap this section
  142. in certain configurations, a mapping cannot be completely removed for a
  143. whole section. In this case, this will raise a MappingError-Exception.
  144. To still do it, unmap the whole document (or destroy the mapped view)
  145. prior to the change execution.
  146. """
  147. return unmap_section(self)
  148. def remap(self, msec):
  149. """
  150. reestablish a mapping assuming that this section was successfully unmapped
  151. earlier. To avoid undefined side-effects, only call this, if this section
  152. is the only section without an established mapping.
  153. """
  154. # map the section subtree
  155. nmsec = create_section_mapping(self)
  156. # remap all the properties
  157. for child in self.itersections(recursive=True, yield_self=True):
  158. for prop in child.properties:
  159. create_property_mapping(child, prop)
  160. def remapable_append(func):
  161. """
  162. decorator for append-functions to deal with Proxy objects
  163. """
  164. @wraps(func)
  165. def f(self, obj):
  166. ret = func(self, obj)
  167. if (proxy and not isinstance(obj, proxy.Proxy)) and hasattr(obj, "_remap_info"):
  168. obj.remap(obj._remap_info)
  169. return ret
  170. return f
  171. def remapable_insert(func):
  172. """
  173. decorator for insert-functions to deal with Proxy objects
  174. """
  175. @wraps(func)
  176. def f(self, position, obj):
  177. ret = func(self, position, obj)
  178. if (proxy and not isinstance(obj, proxy.Proxy)) and hasattr(obj, "_remap_info"):
  179. obj.remap(obj._remap_info)
  180. return ret
  181. return f
  182. def remapable_remove(func):
  183. """
  184. decorator for remove-functions to deal with Proxy objects
  185. """
  186. @wraps(func)
  187. def f(self, obj):
  188. # don't attempt anything on proxy objects
  189. if (proxy and not isinstance(obj, proxy.Proxy)) and obj._active_mapping is not None:
  190. obj._remap_info = obj.unmap()
  191. return func(self, obj)
  192. return f
  193. class MappingError(TypeError):
  194. pass
  195. def create_mapping(doc):
  196. """
  197. install the mapping for the document
  198. 1. recursively map all sections
  199. 2. afterwards map all their properties
  200. """
  201. global proxy # we install the proxy only late time
  202. import odml.tools.proxy as proxy
  203. mdoc = proxy.DocumentProxy(doc)
  204. # TODO copy attributes, but also make this generic
  205. mdoc._proxy_obj = doc
  206. doc._active_mapping = mdoc
  207. # iterate each section and property
  208. # take the mapped object and try to put it at a meaningful place
  209. for sec in doc.sections:
  210. create_section_mapping(sec) # this recurses on its own
  211. for sec in doc.itersections(recursive=True):
  212. for prop in sec.properties: # not needed anymore: [:]:
  213. create_property_mapping(sec, prop)
  214. return mdoc
  215. def create_section_mapping(sec):
  216. """
  217. recursively install the mapping for a section
  218. Note: the mappings for the properties contained in the sections need to be
  219. installed afterwards (after all sections have been mapped) manually
  220. """
  221. obj = sec.mapped_object
  222. msec = proxy.MappedSection(sec, template=obj)
  223. sec._active_mapping = msec
  224. sec.parent._active_mapping.proxy_append(msec)
  225. if obj:
  226. term = obj.get_repository()
  227. if msec.get_repository() != term:
  228. msec.repository = term
  229. # map all child sections
  230. for child in sec.sections:
  231. create_section_mapping(child)
  232. return msec
  233. def create_property_mapping(sec, prop):
  234. """
  235. map a property to its destination place using the mapping rules (see test/mapping.py)
  236. Note: all sections of the document need already to be mapped
  237. """
  238. msec = sec._active_mapping
  239. mprop = proxy.PropertyProxy(prop)
  240. mprop._section = None
  241. prop._active_mapping = mprop
  242. mapping = prop.mapped_object
  243. if mapping is None: # easy case: just proxy the property
  244. msec.proxy_append(mprop)
  245. return
  246. mprop.name = mapping.name
  247. dst_type = mapping._section.type
  248. # rule 4c: target-type == section-type
  249. # copy attributes, keep property
  250. if dst_type == msec.type:
  251. msec.proxy_append(mprop)
  252. return mprop
  253. # rule 4d: one child has the type
  254. child = msec.find_related(type=dst_type, siblings=False, parents=False, findAll=True)
  255. if child is None:
  256. # rule 4e: a sibling has the type
  257. sibling = msec.find_related(type=dst_type, children=False, parents=False)
  258. if sibling is not None:
  259. rel = sibling.find_related(type=msec.type, findAll=True)
  260. if len(rel) > 1:
  261. # rule 4e2: create a subsection linked to the sibling
  262. # TODO set repository and other attributes?
  263. child = proxy.NonexistantSection(sibling.name, sibling.type)
  264. child.proxy_append(mprop)
  265. msec.proxy_append(child)
  266. # TODO the link will have trouble to be resolved, as the
  267. # nonexistant section does not allow to create any attributes in it
  268. # as it cannot be proxied
  269. child._link = sibling.get_path()
  270. return mprop
  271. # rule 4e1: exactly one relation for sibling
  272. child = sibling # once we found the target section, the code stays the same
  273. else:
  274. # rule 4f: no sibling, create a new section
  275. # TODO set repository and other attributes?
  276. child = proxy.NonexistantSection(mapping._section.name, dst_type)
  277. msec.proxy_append(child)
  278. elif len(child) > 1:
  279. raise MappingError("""Your data organisation does not make sense,
  280. there are %d children of type '%s'. Don't know how to handle.""" % (len(child), dst_type))
  281. else: # exactly one child found
  282. child = child[0]
  283. # corner-case: we are remapping and/or the section already contains a property
  284. # with the same name
  285. # however this will also hold true, if multiple properties map to the same target
  286. # for now live with it being there multiple times (TODO)
  287. obj = child.contains(mprop)
  288. if obj is not None:
  289. pass #child.proxy_remove(obj)
  290. child.proxy_append(mprop)
  291. return mprop
  292. def unmap_property(prop=None, mprop=None):
  293. """
  294. uninstall the property mapping by removing its proxy object from its mapped section
  295. """
  296. if mprop is None:
  297. if prop is None or prop._active_mapping is None: return
  298. mprop = prop._active_mapping
  299. if prop is None:
  300. prop = mprop._proxy_obj
  301. # the section where the property is mapped to
  302. msec = mprop.parent
  303. msec.proxy_remove(mprop)
  304. # figure out, if we can safely remove mprop's section
  305. # the section can either be a MappedSection that directly corresponds to a section
  306. # in the original document, or it is a NonexistantSection (either linked or plain)
  307. #
  308. if isinstance(msec, proxy.NonexistantSection):
  309. if msec.is_merged:
  310. # the section is merged, i.e. it has a link attribute set (can't be include
  311. # as it is a non-existant section)
  312. # for now all properties that are no proxies (they are installed using
  313. # clone()-call) are just copies
  314. l = len(filter(lambda x: isinstance(x, proxy.Proxy), msec.properties))
  315. else:
  316. l = len(msec.properties)
  317. if l == 0:
  318. # there are no subsections/properties left
  319. msec.parent.proxy_remove(msec)
  320. # TODO cascade till toplevel? probably not
  321. del prop._active_mapping
  322. return mprop
  323. def can_unmap_section(sec, top):
  324. """
  325. check if a section including its subsections, properties and properties of
  326. mapped subsections (i.e. NonexistantSections) can safely be unmapped
  327. """
  328. # all subsections must be unmap-able
  329. for sec in sec.sections:
  330. if not can_unmap_section(sec, top):
  331. return False
  332. msec = sec._active_mapping
  333. if not can_unmap_all_properties(msec, top):
  334. return False
  335. # proxy sections that only exist in the mapping, must fulfill the property condition too
  336. for mchild in msec.sections:
  337. if isinstance(mchild, proxy.NonexistantSection) and not can_unmap_all_properties(mchild, top):
  338. return False
  339. return True
  340. def can_unmap_all_properties(msec, top):
  341. """
  342. find out if the mapped section *msec* contains any property whose origin (its proxied object)
  343. is not within the subtree indicated by section *top*
  344. """
  345. for mprop in msec.properties:
  346. p = mprop._proxy_obj.parent
  347. while p is not None:
  348. if p is top:
  349. break
  350. p = p.parent
  351. else:
  352. # top is not a parent of p
  353. return False
  354. return True
  355. def unmap_section(sec, check=True):
  356. """
  357. try to unmap the section (including its children),
  358. but make sure, that there are no dependencies:
  359. i.e. we cannot unmap a section if properties
  360. from other sections are mapped here, this would break stuff
  361. """
  362. if sec._active_mapping is None:
  363. return
  364. msec = sec._active_mapping
  365. if check and not can_unmap_section(sec, sec):
  366. raise MappingError("""
  367. There are other active mappings to this section (or one of its children)
  368. Unmapping can't be done safely. Destroy the mapped view
  369. before editing.
  370. """)
  371. # first unmap all properties of this section
  372. for child in sec.itersections(recursive=True, yield_self=True):
  373. for prop in child.properties:
  374. mprop = prop._active_mapping
  375. if mprop is not None: # this may happen for linked sections
  376. unmap_property(mprop=mprop)
  377. # now each mapped section should not have any properties left
  378. for mchild in msec.itersections(recursive=True, yield_self=True):
  379. assert len(mchild.properties) == 0
  380. # and we are safe to remove the mappings
  381. for child in sec.itersections(recursive=True, yield_self=True):
  382. del child._active_mapping
  383. # TODO do we need to do anything more with this section?
  384. # finally we can remove this section from the tree
  385. assert isinstance(msec.parent, proxy.Proxy)
  386. msec.parent.proxy_remove(msec)
  387. return msec
  388. def unmap_document(doc):
  389. """
  390. clear all mappings from the document
  391. """
  392. for sec in doc.itersections(recursive=True):
  393. del sec._active_mapping
  394. for prop in sec.properties:
  395. del prop._active_mapping
  396. del doc._active_mapping
  397. def get_object_from_mapped_equivalent(mobj):
  398. """
  399. This function tries to find out which object was responsible for
  400. creating the *mobj*.
  401. This is straightforward in several cases (i.e. *mobj* being a BaseProxy instance)
  402. but is not for others (e.g. *mobj* being a section solely created
  403. for mapping of a property).
  404. """
  405. if isinstance(mobj, proxy.BaseProxy):
  406. return mobj._proxy_obj
  407. # TODO which other cases may we have?
  408. assert isinstance(mobj, proxy.NonexistantSection)
  409. # get the first property (TODO this may not always be the one who caused the section to be created)
  410. for mprop in mobj.properties:
  411. return get_object_from_mapped_equivalent(mprop)
  412. return None