resource.py 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365
  1. import logging
  2. from functools import wraps
  3. from itertools import groupby
  4. from multiprocessing import Process
  5. from threading import Lock, Thread
  6. import arrow
  7. from rdflib import Literal
  8. from rdflib.namespace import XSD
  9. from lakesuperior.config_parser import config
  10. from lakesuperior.dictionaries.namespaces import ns_collection as nsc
  11. from lakesuperior.exceptions import (
  12. InvalidResourceError, ResourceNotExistsError, TombstoneError)
  13. from lakesuperior import env, thread_env
  14. from lakesuperior.globals import RES_DELETED, RES_UPDATED
  15. from lakesuperior.model.ldp.ldp_factory import LDP_NR_TYPE, LdpFactory
  16. from lakesuperior.model.ldp.ldpr import Ldpr
  17. from lakesuperior.util.toolbox import rel_uri_to_urn
  18. logger = logging.getLogger(__name__)
  19. __doc__ = """
  20. Primary API for resource manipulation.
  21. Quickstart::
  22. >>> # First import default configuration and globals—only done once.
  23. >>> import lakesuperior.default_env
  24. >>> from lakesuperior.api import resource
  25. >>> # Get root resource.
  26. >>> rsrc = resource.get('/')
  27. >>> # Dump graph.
  28. >>> with rsrc.imr.store.txn_ctx():
  29. >>> print({*rsrc.imr.as_rdflib()})
  30. {(rdflib.term.URIRef('info:fcres/'),
  31. rdflib.term.URIRef('http://purl.org/dc/terms/title'),
  32. rdflib.term.Literal('Repository Root')),
  33. (rdflib.term.URIRef('info:fcres/'),
  34. rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
  35. rdflib.term.URIRef('http://fedora.info/definitions/fcrepo#Container')),
  36. (rdflib.term.URIRef('info:fcres/'),
  37. rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
  38. rdflib.term.URIRef('http://fedora.info/definitions/fcrepo#RepositoryRoot')),
  39. (rdflib.term.URIRef('info:fcres/'),
  40. rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
  41. rdflib.term.URIRef('http://fedora.info/definitions/fcrepo#Resource')),
  42. (rdflib.term.URIRef('info:fcres/'),
  43. rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
  44. rdflib.term.URIRef('http://www.w3.org/ns/ldp#BasicContainer')),
  45. (rdflib.term.URIRef('info:fcres/'),
  46. rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
  47. rdflib.term.URIRef('http://www.w3.org/ns/ldp#Container')),
  48. (rdflib.term.URIRef('info:fcres/'),
  49. rdflib.term.URIRef('http://www.w3.org/1999/02/22-rdf-syntax-ns#type'),
  50. rdflib.term.URIRef('http://www.w3.org/ns/ldp#RDFSource'))}
  51. """
  52. def transaction(write=False):
  53. """
  54. Handle atomic operations in a store.
  55. This wrapper ensures that a write operation is performed atomically. It
  56. also takes care of sending a message for each resource changed in the
  57. transaction.
  58. ALL write operations on the LDP-RS and LDP-NR stores go through this
  59. wrapper.
  60. """
  61. def _transaction_deco(fn):
  62. @wraps(fn)
  63. def _wrapper(*args, **kwargs):
  64. # Mark transaction begin timestamp. This is used for create and
  65. # update timestamps on resources.
  66. thread_env.timestamp = arrow.utcnow()
  67. thread_env.timestamp_term = Literal(
  68. thread_env.timestamp, datatype=XSD.dateTime)
  69. with env.app_globals.rdf_store.txn_ctx(write):
  70. ret = fn(*args, **kwargs)
  71. if len(env.app_globals.changelog):
  72. job = Thread(target=_process_queue)
  73. job.start()
  74. delattr(thread_env, 'timestamp')
  75. delattr(thread_env, 'timestamp_term')
  76. return ret
  77. return _wrapper
  78. return _transaction_deco
  79. def _process_queue():
  80. """
  81. Process the message queue on a separate thread.
  82. """
  83. lock = Lock()
  84. lock.acquire()
  85. while len(env.app_globals.changelog):
  86. _send_event_msg(*env.app_globals.changelog.popleft())
  87. lock.release()
  88. def _send_event_msg(remove_trp, add_trp, metadata):
  89. """
  90. Send messages about a changed LDPR.
  91. A single LDPR message packet can contain multiple resource subjects, e.g.
  92. if the resource graph contains hash URIs or even other subjects. This
  93. method groups triples by subject and sends a message for each of the
  94. subjects found.
  95. """
  96. # Group delta triples by subject.
  97. remove_grp = groupby(remove_trp, lambda x : x[0])
  98. remove_dict = {k[0]: k[1] for k in remove_grp}
  99. add_grp = groupby(add_trp, lambda x : x[0])
  100. add_dict = {k[0]: k[1] for k in add_grp}
  101. subjects = set(remove_dict.keys()) | set(add_dict.keys())
  102. for rsrc_uri in subjects:
  103. logger.debug('Processing event for subject: {}'.format(rsrc_uri))
  104. env.app_globals.messenger.send(rsrc_uri, **metadata)
  105. ### API METHODS ###
  106. @transaction()
  107. def exists(uid):
  108. """
  109. Return whether a resource exists (is stored) in the repository.
  110. :param string uid: Resource UID.
  111. """
  112. try:
  113. exists = LdpFactory.from_stored(uid).is_stored
  114. except ResourceNotExistsError:
  115. exists = False
  116. return exists
  117. @transaction()
  118. def get_metadata(uid):
  119. """
  120. Get metadata (admin triples) of an LDPR resource.
  121. :param string uid: Resource UID.
  122. """
  123. return LdpFactory.from_stored(uid).metadata
  124. @transaction()
  125. def get(uid, repr_options={}):
  126. """
  127. Get an LDPR resource.
  128. The resource comes preloaded with user data and metadata as indicated by
  129. the `repr_options` argument. Any further handling of this resource is done
  130. outside of a transaction.
  131. :param string uid: Resource UID.
  132. :param repr_options: (dict(bool)) Representation options. This is a dict
  133. that is unpacked downstream in the process. The default empty dict
  134. results in default values. The accepted dict keys are:
  135. - incl_inbound: include inbound references. Default: False.
  136. - incl_children: include children URIs. Default: True.
  137. - embed_children: Embed full graph of all child resources. Default: False
  138. """
  139. rsrc = LdpFactory.from_stored(uid, repr_opts=repr_options)
  140. # Load graph before leaving the transaction.
  141. rsrc.imr
  142. return rsrc
  143. @transaction()
  144. def get_version_info(uid):
  145. """
  146. Get version metadata (fcr:versions).
  147. """
  148. return LdpFactory.from_stored(uid).version_info
  149. @transaction()
  150. def get_version(uid, ver_uid):
  151. """
  152. Get version metadata (fcr:versions).
  153. """
  154. return LdpFactory.from_stored(uid).get_version(ver_uid)
  155. @transaction(True)
  156. def create(parent, slug=None, **kwargs):
  157. r"""
  158. Mint a new UID and create a resource.
  159. The UID is computed from a given parent UID and a "slug", a proposed path
  160. relative to the parent. The application will attempt to use the suggested
  161. path but it may use a different one if a conflict with an existing resource
  162. arises.
  163. :param str parent: UID of the parent resource.
  164. :param str slug: Tentative path relative to the parent UID.
  165. :param \*\*kwargs: Other parameters are passed to the
  166. :py:meth:`~lakesuperior.model.ldp.ldp_factory.LdpFactory.from_provided`
  167. method.
  168. :rtype: tuple(str, lakesuperior.model.ldp.ldpr.Ldpr)
  169. :return: A tuple of:
  170. 1. Event type (str): whether the resource was created or updated.
  171. 2. Resource (lakesuperior.model.ldp.ldpr.Ldpr): The new or updated resource.
  172. """
  173. uid = LdpFactory.mint_uid(parent, slug)
  174. logger.debug('Minted UID for new resource: {}'.format(uid))
  175. rsrc = LdpFactory.from_provided(uid, **kwargs)
  176. rsrc.create_or_replace(create_only=True)
  177. return rsrc
  178. @transaction(True)
  179. def create_or_replace(uid, **kwargs):
  180. r"""
  181. Create or replace a resource with a specified UID.
  182. :param string uid: UID of the resource to be created or updated.
  183. :param \*\*kwargs: Other parameters are passed to the
  184. :py:meth:`~lakesuperior.model.ldp.ldp_factory.LdpFactory.from_provided`
  185. method.
  186. :rtype: tuple(str, lakesuperior.model.ldp.ldpr.Ldpr)
  187. :return: A tuple of:
  188. 1. Event type (str): whether the resource was created or updated.
  189. 2. Resource (lakesuperior.model.ldp.ldpr.Ldpr): The new or updated
  190. resource.
  191. """
  192. rsrc = LdpFactory.from_provided(uid, **kwargs)
  193. return rsrc.create_or_replace(), rsrc
  194. @transaction(True)
  195. def update(uid, update_str, is_metadata=False, handling='strict'):
  196. """
  197. Update a resource with a SPARQL-Update string.
  198. :param string uid: Resource UID.
  199. :param string update_str: SPARQL-Update statements.
  200. :param bool is_metadata: Whether the resource metadata are being updated.
  201. :param str handling: How to handle server-managed triples. ``strict``
  202. (the default) rejects the update with an exception if server-managed
  203. triples are being changed. ``lenient`` modifies the update graph so
  204. offending triples are removed and the update can be applied.
  205. :raise InvalidResourceError: If ``is_metadata`` is False and the resource
  206. being updated is a LDP-NR.
  207. """
  208. rsrc = LdpFactory.from_stored(uid, handling=handling)
  209. if LDP_NR_TYPE in rsrc.ldp_types and not is_metadata:
  210. raise InvalidResourceError(
  211. 'Cannot use this method to update an LDP-NR content.')
  212. delta = rsrc.sparql_delta(update_str)
  213. rsrc.modify(RES_UPDATED, *delta)
  214. return rsrc
  215. @transaction(True)
  216. def update_delta(uid, remove_trp, add_trp):
  217. """
  218. Update a resource graph (LDP-RS or LDP-NR) with sets of add/remove triples.
  219. A set of triples to add and/or a set of triples to remove may be provided.
  220. :param string uid: Resource UID.
  221. :param set(tuple(rdflib.term.Identifier)) remove_trp: Triples to
  222. remove, as 3-tuples of RDFLib terms.
  223. :param set(tuple(rdflib.term.Identifier)) add_trp: Triples to
  224. add, as 3-tuples of RDFLib terms.
  225. """
  226. rsrc = LdpFactory.from_stored(uid)
  227. # FIXME Wrong place to put this, should be at the LDP level.
  228. remove_trp = {
  229. (rel_uri_to_urn(s, uid), p, rel_uri_to_urn(o, uid))
  230. for s, p, o in remove_trp
  231. }
  232. add_trp = {
  233. (rel_uri_to_urn(s, uid), p, rel_uri_to_urn(o, uid))
  234. for s, p, o in add_trp
  235. }
  236. remove_trp = rsrc.check_mgd_terms(remove_trp)
  237. add_trp = rsrc.check_mgd_terms(add_trp)
  238. return rsrc.modify(RES_UPDATED, remove_trp, add_trp)
  239. @transaction(True)
  240. def create_version(uid, ver_uid):
  241. """
  242. Create a resource version.
  243. :param string uid: Resource UID.
  244. :param string ver_uid: Version UID to be appended to the resource URI.
  245. NOTE: this is a "slug", i.e. the version URI is not guaranteed to be the
  246. one indicated.
  247. :rtype: str
  248. :return: Version UID.
  249. """
  250. return LdpFactory.from_stored(uid).create_version(ver_uid)
  251. @transaction(True)
  252. def delete(uid, soft=True, inbound=True):
  253. """
  254. Delete a resource.
  255. :param string uid: Resource UID.
  256. :param bool soft: Whether to perform a soft-delete and leave a
  257. tombstone resource, or wipe any memory of the resource.
  258. """
  259. # If referential integrity is enforced, grab all inbound relationships
  260. # to break them.
  261. refint = env.app_globals.rdfly.config['referential_integrity']
  262. inbound = True if refint else inbound
  263. if soft:
  264. repr_opts = {'incl_inbound' : True} if inbound else {}
  265. rsrc = LdpFactory.from_stored(uid, repr_opts)
  266. return rsrc.bury(inbound)
  267. else:
  268. Ldpr.forget(uid, inbound)
  269. @transaction(True)
  270. def revert_to_version(uid, ver_uid):
  271. """
  272. Restore a resource to a previous version state.
  273. :param str uid: Resource UID.
  274. :param str ver_uid: Version UID.
  275. """
  276. return LdpFactory.from_stored(uid).revert_to_version(ver_uid)
  277. @transaction(True)
  278. def resurrect(uid):
  279. """
  280. Reinstate a buried (soft-deleted) resource.
  281. :param str uid: Resource UID.
  282. """
  283. try:
  284. rsrc = LdpFactory.from_stored(uid)
  285. except TombstoneError as e:
  286. if e.uid != uid:
  287. raise
  288. else:
  289. return LdpFactory.from_stored(uid, strict=False).resurrect()
  290. else:
  291. raise InvalidResourceError(
  292. uid, msg='Resource {} is not dead.'.format(uid))