ldpr.py 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973
  1. import logging
  2. import re
  3. from abc import ABCMeta
  4. from collections import defaultdict
  5. from hashlib import sha256
  6. # The threading and multiprocessing options below can be toggled by commenting
  7. # either one.
  8. #from threading import Thread as _Defer
  9. from multiprocessing import Process as _Defer
  10. from urllib.parse import urldefrag
  11. from uuid import uuid4
  12. import arrow
  13. import rdflib
  14. from rdflib import URIRef, Literal
  15. from rdflib.compare import to_isomorphic
  16. from rdflib.namespace import RDF
  17. from lakesuperior import env, thread_env
  18. from lakesuperior.globals import (
  19. RES_CREATED, RES_DELETED, RES_UPDATED, ROOT_UID)
  20. from lakesuperior.dictionaries.namespaces import ns_collection as nsc
  21. from lakesuperior.dictionaries.srv_mgd_terms import (
  22. srv_mgd_subjects, srv_mgd_predicates, srv_mgd_types)
  23. from lakesuperior.exceptions import (
  24. InvalidResourceError, RefIntViolationError, ResourceNotExistsError,
  25. ServerManagedTermError, TombstoneError)
  26. from lakesuperior.model.rdf.graph import Graph
  27. from lakesuperior.store.ldp_rs.rsrc_centric_layout import VERS_CONT_LABEL
  28. from lakesuperior.util.toolbox import (
  29. rel_uri_to_urn_string, replace_term_domain)
  30. DEF_MBR_REL_URI = nsc['ldp'].member
  31. DEF_INS_CNT_REL_URI = nsc['ldp'].memberSubject
  32. rdfly = env.app_globals.rdfly
  33. logger = logging.getLogger(__name__)
  34. class Ldpr(metaclass=ABCMeta):
  35. """
  36. LDPR (LDP Resource).
  37. This class and related subclasses contain the implementation pieces of
  38. the `LDP Resource <https://www.w3.org/TR/ldp/#ldpr-resource>`__
  39. specifications, according to their `inheritance graph
  40. <https://www.w3.org/TR/ldp/#fig-ldpc-types>`__.
  41. **Note**: Even though LdpNr (which is a subclass of Ldpr) handles binary
  42. files, it still has an RDF representation in the triplestore. Hence, some
  43. of the RDF-related methods are defined in this class rather than in
  44. :class:`~lakesuperior.model.ldp.ldp_rs.LdpRs`.
  45. **Note:** Only internal facing (``info:fcres``-prefixed) URIs are handled
  46. in this class. Public-facing URI conversion is handled in the
  47. :mod:`~lakesuperior.endpoints.ldp` module.
  48. """
  49. EMBED_CHILD_RES_URI = nsc['fcrepo'].EmbedResources
  50. FCREPO_PTREE_TYPE = nsc['fcrepo'].Pairtree
  51. INS_CNT_REL_URI = nsc['ldp'].insertedContentRelation
  52. MBR_RSRC_URI = nsc['ldp'].membershipResource
  53. MBR_REL_URI = nsc['ldp'].hasMemberRelation
  54. RETURN_CHILD_RES_URI = nsc['fcrepo'].Children
  55. RETURN_INBOUND_REF_URI = nsc['fcrepo'].InboundReferences
  56. RETURN_SRV_MGD_RES_URI = nsc['fcrepo'].ServerManaged
  57. # Workflow type. Inbound means that the resource is being written to the
  58. # store, outbounnd is being retrieved for output.
  59. WRKF_INBOUND = '_workflow:inbound_'
  60. WRKF_OUTBOUND = '_workflow:outbound_'
  61. DEFAULT_USER = Literal('BypassAdmin')
  62. """
  63. Default user to be used for the `createdBy` and `lastUpdatedBy` if a user
  64. is not provided.
  65. """
  66. base_types = {
  67. nsc['fcrepo'].Resource,
  68. nsc['ldp'].Resource,
  69. nsc['ldp'].RDFSource,
  70. }
  71. """RDF Types that populate a new resource."""
  72. protected_pred = (
  73. nsc['fcrepo'].created,
  74. nsc['fcrepo'].createdBy,
  75. nsc['ldp'].contains,
  76. )
  77. """Predicates that do not get removed when a resource is replaced."""
  78. smt_allow_on_create = {
  79. nsc['ldp'].DirectContainer,
  80. nsc['ldp'].IndirectContainer,
  81. }
  82. """
  83. Server-managed RDF types ignored in the RDF payload if the resource is
  84. being created. N.B. These still raise an error if the resource exists.
  85. """
  86. delete_preds_on_replace = {
  87. nsc['ebucore'].hasMimeType,
  88. nsc['fcrepo'].lastModified,
  89. nsc['fcrepo'].lastModifiedBy,
  90. nsc['premis'].hasSize,
  91. nsc['premis'].hasMessageDigest,
  92. }
  93. """Predicates to remove when a resource is replaced."""
  94. _ignore_version_preds = {
  95. nsc['fcrepo'].hasParent,
  96. nsc['fcrepo'].hasVersions,
  97. nsc['fcrepo'].hasVersion,
  98. nsc['premis'].hasMessageDigest,
  99. nsc['ldp'].contains,
  100. }
  101. """Predicates that don't get versioned."""
  102. _ignore_version_types = {
  103. nsc['fcrepo'].Binary,
  104. nsc['fcrepo'].Container,
  105. nsc['fcrepo'].Pairtree,
  106. nsc['fcrepo'].Resource,
  107. nsc['fcrepo'].Version,
  108. nsc['ldp'].BasicContainer,
  109. nsc['ldp'].Container,
  110. nsc['ldp'].DirectContainer,
  111. nsc['ldp'].Resource,
  112. nsc['ldp'].RDFSource,
  113. nsc['ldp'].NonRDFSource,
  114. }
  115. """RDF types that don't get versioned."""
  116. _cached_attrs = (
  117. '_imr',
  118. '_metadata',
  119. '_version_info',
  120. '_is_stored',
  121. '_types',
  122. '_ldp_types',
  123. )
  124. """
  125. Attributes cached from the store.
  126. These are used by setters and can be cleared with :py:meth:`_clear_cache`.
  127. """
  128. ## MAGIC METHODS ##
  129. def __init__(self, uid, repr_opts={}, provided_imr=None, **kwargs):
  130. """
  131. Instantiate an in-memory LDP resource.
  132. :param str uid: uid of the resource. If None (must be explicitly
  133. set) it refers to the root node. It can also be the full URI or
  134. URN, in which case it will be converted.
  135. :param dict repr_opts: Options used to retrieve the IMR. See
  136. :py:meth:`~lakesuperior.endpoints.ldp.parse_repr_options` for
  137. format details.
  138. :param str provided_imr: RDF data provided by the client in
  139. operations such as `PUT` or `POST`, serialized as a string. This
  140. sets the :py:data:`~Ldpr.provided_imr` property.
  141. """
  142. self.uid = (
  143. rdfly.uri_to_uid(uid) if isinstance(uid, URIRef) else uid)
  144. self.uri = nsc['fcres'][uid]
  145. # @FIXME Not ideal, should separate app-context dependent functions in
  146. # a different toolbox.
  147. self.provided_imr = provided_imr
  148. # This gets overridden by LDP-NR.
  149. self.mimetype = None
  150. # Disable all internal checks e.g. for raw I/O.
  151. #@property
  152. #def rsrc(self):
  153. # """
  154. # The RDFLib resource representing this LDPR. This is a live
  155. # representation of the stored data if present.
  156. # :rtype: rdflib.Resource
  157. # """
  158. # if not hasattr(self, '_rsrc'):
  159. # self._rsrc = rdfly.ds.resource(self.uri)
  160. # return self._rsrc
  161. @property
  162. def imr(self):
  163. """
  164. In-Memory Resource.
  165. This is a copy of the resource extracted from the graph store. It is a
  166. graph resource whose identifier is the URI of the resource.
  167. >>> rsrc = rsrc_api.get('/')
  168. >>> rsrc.imr.identifier
  169. rdflib.term.URIRef('info:fcres/')
  170. >>> rsrc.imr.value(nsc['fcrepo'].lastModified)
  171. rdflib.term.Literal(
  172. '2018-04-03T05:20:33.774746+00:00',
  173. datatype=rdflib.term.URIRef(
  174. 'http://www.w3.org/2001/XMLSchema#dateTime'))
  175. The IMR can be read and manipulated, as well as used to
  176. update the stored resource.
  177. :rtype: rdflib.Graph
  178. :raise lakesuperior.exceptions.ResourceNotExistsError: If the resource
  179. is not stored (yet).
  180. """
  181. if not hasattr(self, '_imr'):
  182. if hasattr(self, '_imr_options'):
  183. logger.debug(
  184. 'Getting RDF triples for resource {}'.format(self.uid))
  185. imr_options = self._imr_options
  186. else:
  187. imr_options = {}
  188. options = dict(imr_options, strict=True)
  189. self._imr = rdfly.get_imr(self.uid, **options)
  190. return self._imr
  191. @imr.setter
  192. def imr(self, data):
  193. """
  194. Replace in-memory buffered resource.
  195. :param v: New set of triples to populate the IMR with.
  196. :type v: set or rdflib.Graph
  197. """
  198. self._imr = Graph(uri=self.uri, data=set(data))
  199. @imr.deleter
  200. def imr(self):
  201. """
  202. Delete in-memory buffered resource.
  203. """
  204. delattr(self, '_imr')
  205. @property
  206. def metadata(self):
  207. """
  208. Get resource metadata.
  209. """
  210. if not hasattr(self, '_metadata'):
  211. if hasattr(self, '_imr'):
  212. logger.info('Metadata is IMR.')
  213. self._metadata = self._imr
  214. else:
  215. logger.info(
  216. 'Getting metadata for resource {}'.format(self.uid))
  217. self._metadata = rdfly.get_metadata(self.uid)
  218. return self._metadata
  219. @property
  220. def user_data(self):
  221. """
  222. User-defined triples.
  223. """
  224. if not hasattr(self, '_user_data'):
  225. self._user_data = rdfly.get_user_data(self.uid)
  226. return self._user_data
  227. @metadata.setter
  228. def metadata(self, rsrc):
  229. """
  230. Set resource metadata.
  231. """
  232. if not isinstance(rsrc, Graph):
  233. raise TypeError('Provided metadata is not a Graph object.')
  234. self._metadata = rsrc
  235. @property
  236. def out_graph(self):
  237. """
  238. Retun a graph of the resource's IMR formatted for output.
  239. """
  240. out_trp = set()
  241. for t in self.imr:
  242. if (
  243. # Exclude digest hash and version information.
  244. t[1] not in {
  245. #nsc['premis'].hasMessageDigest,
  246. nsc['fcrepo'].hasVersion,
  247. }
  248. ) and (
  249. # Only include server managed triples if requested.
  250. self._imr_options.get('incl_srv_mgd', True) or
  251. not self._is_trp_managed(t)
  252. ):
  253. out_trp.add(t)
  254. return Graph(uri=self.uri, data=out_trp)
  255. @property
  256. def version_info(self):
  257. """
  258. Return version metadata (`fcr:versions`).
  259. """
  260. if not hasattr(self, '_version_info'):
  261. try:
  262. self._version_info = rdfly.get_version_info(self.uid)
  263. except ResourceNotExistsError as e:
  264. self._version_info = Graph(uri=self.uri)
  265. return self._version_info
  266. @property
  267. def version_uids(self):
  268. """
  269. Return a set of version UIDs relative to their parent resource.
  270. """
  271. uids = set()
  272. for vuri in self.version_info[nsc['fcrepo'].hasVersion]:
  273. for vlabel in self.version_info[
  274. vuri : nsc['fcrepo'].hasVersionLabel]:
  275. uids.add(str(vlabel))
  276. return uids
  277. @property
  278. def is_stored(self):
  279. if not hasattr(self, '_is_stored'):
  280. if hasattr(self, '_imr'):
  281. self._is_stored = len(self.imr) > 0
  282. else:
  283. self._is_stored = rdfly.ask_rsrc_exists(self.uid)
  284. return self._is_stored
  285. @property
  286. def types(self):
  287. """
  288. All RDF types, both server-managed and user-defined.
  289. :rtype: set(rdflib.term.URIRef)
  290. """
  291. if not hasattr(self, '_types'):
  292. if len(self.metadata):
  293. metadata = self.metadata
  294. elif getattr(self, 'provided_imr', None) and \
  295. len(self.provided_imr):
  296. metadata = self.provided_imr
  297. else:
  298. return set()
  299. self._types = set(metadata[self.uri: RDF.type]) | self.user_types
  300. return self._types
  301. @property
  302. def ldp_types(self):
  303. """The LDP types.
  304. :rtype: set(rdflib.term.URIRef)
  305. """
  306. if not hasattr(self, '_ldp_types'):
  307. self._ldp_types = {t for t in self.types if nsc['ldp'] in t}
  308. return self._ldp_types
  309. @property
  310. def user_types(self):
  311. """User-defined types.
  312. :rtype: set(rdflib.term.URIRef)
  313. """
  314. if not hasattr(self, '_ud_types'):
  315. self._ud_types = self.user_data[self.uri: RDF.type]
  316. return self._ud_types
  317. ## LDP METHODS ##
  318. def head(self):
  319. """
  320. Return values for the headers.
  321. """
  322. out_headers = defaultdict(list)
  323. digest = self.metadata.value(nsc['premis'].hasMessageDigest)
  324. if digest:
  325. etag = digest.identifier.split(':')[-1]
  326. out_headers['ETag'] = 'W/"{}"'.format(etag),
  327. last_updated_term = self.metadata.value(nsc['fcrepo'].lastModified)
  328. if last_updated_term:
  329. out_headers['Last-Modified'] = arrow.get(last_updated_term)\
  330. .format('ddd, D MMM YYYY HH:mm:ss Z')
  331. for t in self.ldp_types:
  332. out_headers['Link'].append(
  333. '{};rel="type"'.format(t.n3()))
  334. return out_headers
  335. def get_version(self, ver_uid, **kwargs):
  336. """
  337. Get a version by label.
  338. """
  339. return rdfly.get_imr(self.uid, ver_uid, **kwargs)
  340. def create_or_replace(self, create_only=False):
  341. """
  342. Create or update a resource. PUT and POST methods, which are almost
  343. identical, are wrappers for this method.
  344. :param boolean create_only: Whether this is a create-only operation.
  345. :rtype: str
  346. :return: Event type: whether the resource was created or updated.
  347. """
  348. create = create_only or not self.is_stored
  349. ev_type = RES_CREATED if create else RES_UPDATED
  350. self._add_srv_mgd_triples(create)
  351. ref_int = rdfly.config['referential_integrity']
  352. if ref_int:
  353. self._check_ref_int(ref_int)
  354. # Delete existing triples if replacing.
  355. if not create:
  356. rdfly.truncate_rsrc(self.uid)
  357. remove_trp = {
  358. (self.uri, pred, None) for pred in self.delete_preds_on_replace}
  359. add_trp = {*self.provided_imr} | self._containment_rel(create)
  360. self.modify(ev_type, remove_trp, add_trp)
  361. #self.imr = add_trp
  362. return ev_type
  363. def bury(self, inbound, tstone_pointer=None):
  364. """
  365. Delete a single resource and create a tombstone.
  366. :param bool inbound: Whether inbound relationships are
  367. removed. If ``False``, resources will keep referring
  368. to the deleted resource; their link will point to a tombstone
  369. (which will raise a ``TombstoneError`` in the Python API or a
  370. ``410 Gone`` in the LDP API).
  371. :param rdflib.URIRef tstone_pointer: If set to a URI, this creates a
  372. pointer to the tombstone of the resource that used to contain the
  373. deleted resource. Otherwise the deleted resource becomes a
  374. tombstone.
  375. """
  376. logger.info('Burying resource {}'.format(self.uid))
  377. # ldp:Resource is also used in rdfly.ask_rsrc_exists.
  378. remove_trp = {
  379. (nsc['fcrepo'].uid, nsc['rdf'].type, nsc['ldp'].Resource)
  380. }
  381. if tstone_pointer:
  382. add_trp = {
  383. (self.uri, nsc['fcsystem'].tombstone, tstone_pointer)}
  384. else:
  385. add_trp = {
  386. (self.uri, RDF.type, nsc['fcsystem'].Tombstone),
  387. (self.uri, nsc['fcsystem'].buried, thread_env.timestamp_term),
  388. }
  389. # Bury descendants.
  390. from lakesuperior.model.ldp.ldp_factory import LdpFactory
  391. for desc_uri in rdfly.get_descendants(self.uid):
  392. try:
  393. desc_rsrc = LdpFactory.from_stored(
  394. env.app_globals.rdfly.uri_to_uid(desc_uri),
  395. repr_opts={'incl_children' : False})
  396. except (TombstoneError, ResourceNotExistsError):
  397. continue
  398. desc_rsrc.bury(inbound, tstone_pointer=self.uri)
  399. # Cut inbound relationships
  400. if inbound:
  401. for ib_rsrc_uri in self.imr[ : : self.uri]:
  402. remove_trp = {(ib_rsrc_uri, None, self.uri)}
  403. ib_rsrc = Ldpr(ib_rsrc_uri)
  404. # To preserve inbound links in history, create a snapshot
  405. ib_rsrc.create_version()
  406. ib_rsrc.modify(RES_UPDATED, remove_trp)
  407. self.modify(RES_DELETED, remove_trp, add_trp)
  408. return RES_DELETED
  409. @staticmethod
  410. def forget(uid, inbound=True):
  411. """
  412. Remove all traces of a resource and versions.
  413. """
  414. logger.info('Forgetting resource {}'.format(uid))
  415. rdfly.forget_rsrc(uid, inbound)
  416. return RES_DELETED
  417. def resurrect(self):
  418. """
  419. Resurrect a resource from a tombstone.
  420. """
  421. remove_trp = {
  422. (self.uri, nsc['rdf'].type, nsc['fcsystem'].Tombstone),
  423. (self.uri, nsc['fcsystem'].tombstone, None),
  424. (self.uri, nsc['fcsystem'].buried, None),
  425. }
  426. add_trp = {
  427. (self.uri, nsc['rdf'].type, nsc['ldp'].Resource),
  428. }
  429. self.modify(RES_CREATED, remove_trp, add_trp)
  430. # Resurrect descendants.
  431. from lakesuperior.model.ldp.ldp_factory import LdpFactory
  432. descendants = env.app_globals.rdfly.get_descendants(self.uid)
  433. for desc_uri in descendants:
  434. LdpFactory.from_stored(
  435. rdfly.uri_to_uid(desc_uri), strict=False).resurrect()
  436. return self.uri
  437. def create_version(self, ver_uid=None):
  438. """
  439. Create a new version of the resource.
  440. **Note:** This creates an event only for the resource being updated
  441. (due to the added `hasVersion` triple and possibly to the
  442. ``hasVersions`` one) but not for the version being created.
  443. :param str ver_uid: Version UID. If already existing, a new version UID
  444. is minted.
  445. """
  446. if not ver_uid or ver_uid in self.version_uids:
  447. ver_uid = str(uuid4())
  448. # Create version resource from copying the current state.
  449. logger.info(
  450. 'Creating version snapshot {} for resource {}.'.format(
  451. ver_uid, self.uid))
  452. ver_add_gr = set()
  453. vers_uid = '{}/{}'.format(self.uid, VERS_CONT_LABEL)
  454. ver_uid = '{}/{}'.format(vers_uid, ver_uid)
  455. ver_uri = nsc['fcres'][ver_uid]
  456. ver_add_gr.add((ver_uri, RDF.type, nsc['fcrepo'].Version))
  457. for t in self.imr:
  458. if (
  459. t[1] == RDF.type and t[2] in self._ignore_version_types
  460. ) or t[1] in self._ignore_version_preds:
  461. pass
  462. else:
  463. ver_add_gr.add((
  464. replace_term_domain(t[0], self.uri, ver_uri),
  465. t[1], t[2]))
  466. rdfly.modify_rsrc(ver_uid, add_trp=ver_add_gr)
  467. # Update resource admin data.
  468. rsrc_add_gr = {
  469. (self.uri, nsc['fcrepo'].hasVersion, ver_uri),
  470. (self.uri, nsc['fcrepo'].hasVersions, nsc['fcres'][vers_uid]),
  471. }
  472. self.modify(RES_UPDATED, add_trp=rsrc_add_gr)
  473. return ver_uid
  474. def revert_to_version(self, ver_uid, backup=True):
  475. """
  476. Revert to a previous version.
  477. :param str ver_uid: Version UID.
  478. :param boolean backup: Whether to create a backup snapshot. Default is
  479. True.
  480. """
  481. # Create a backup snapshot.
  482. if backup:
  483. self.create_version()
  484. ver_gr = rdfly.get_imr(
  485. self.uid, ver_uid=ver_uid, incl_children=False)
  486. self.provided_imr = Graph(uri=self.uri)
  487. for t in ver_gr:
  488. if not self._is_trp_managed(t):
  489. self.provided_imr.add(((self.uri, t[1], t[2]),))
  490. # @TODO Check individual objects: if they are repo-managed URIs
  491. # and not existing or tombstones, they are not added.
  492. return self.create_or_replace(create_only=False)
  493. def check_mgd_terms(self, trp):
  494. """
  495. Check whether server-managed terms are in a RDF payload.
  496. :param SimpleGraph trp: The graph to validate.
  497. """
  498. offending_subjects = (
  499. {t[0] for t in trp if t[0] is not None} & srv_mgd_subjects
  500. )
  501. if offending_subjects:
  502. if self.handling == 'strict':
  503. raise ServerManagedTermError(offending_subjects, 's')
  504. else:
  505. for s in offending_subjects:
  506. logger.info('Removing offending subj: {}'.format(s))
  507. for t in trp:
  508. if t[0] == s:
  509. trp.remove(t)
  510. offending_predicates = (
  511. {t[1] for t in trp if t[1] is not None} & srv_mgd_predicates
  512. )
  513. # Allow some predicates if the resource is being created.
  514. if offending_predicates:
  515. if self.handling == 'strict':
  516. raise ServerManagedTermError(offending_predicates, 'p')
  517. else:
  518. for p in offending_predicates:
  519. logger.info('Removing offending pred: {}'.format(p))
  520. for t in trp:
  521. if t[1] == p:
  522. trp.remove(t)
  523. offending_types = (
  524. {t[2] for t in trp if t[1] == RDF.type and t[2] is not None}
  525. & srv_mgd_types
  526. )
  527. if not self.is_stored:
  528. offending_types -= self.smt_allow_on_create
  529. if offending_types:
  530. if self.handling == 'strict':
  531. raise ServerManagedTermError(offending_types, 't')
  532. else:
  533. for to in offending_types:
  534. logger.info('Removing offending type: {}'.format(to))
  535. trp.remove_triples((None, RDF.type, to))
  536. #logger.debug('Sanitized graph: {}'.format(trp.serialize(
  537. # format='turtle').decode('utf-8')))
  538. return trp
  539. def sparql_delta(self, qry_str):
  540. """
  541. Calculate the delta obtained by a SPARQL Update operation.
  542. This is a critical component of the SPARQL update prcess and does a
  543. couple of things:
  544. 1. It ensures that no resources outside of the subject of the request
  545. are modified (e.g. by variable subjects)
  546. 2. It verifies that none of the terms being modified is server managed.
  547. This method extracts an in-memory copy of the resource and performs the
  548. query on that once it has checked if any of the server managed terms is
  549. in the delta. If it is, it raises an exception.
  550. NOTE: This only checks if a server-managed term is effectively being
  551. modified. If a server-managed term is present in the query but does not
  552. cause any change in the updated resource, no error is raised.
  553. :rtype: tuple(rdflib.Graph)
  554. :return: Remove and add graphs. These can be used
  555. with ``BaseStoreLayout.update_resource`` and/or recorded as separate
  556. events in a provenance tracking system.
  557. """
  558. logger.debug('Provided SPARQL query: {}'.format(qry_str))
  559. # Workaround for RDFLib bug. See
  560. # https://github.com/RDFLib/rdflib/issues/824
  561. qry_str = rel_uri_to_urn_string(qry_str, self.uid)
  562. pre_gr = self.imr.as_rdflib()
  563. post_gr = rdflib.Graph(identifier=self.uri)
  564. post_gr |= pre_gr
  565. post_gr.update(qry_str)
  566. # FIXME Fix and use SimpleGraph's native subtraction operation.
  567. remove_gr = self.check_mgd_terms(Graph(data=set(pre_gr - post_gr)))
  568. add_gr = self.check_mgd_terms(Graph(data=set(post_gr - pre_gr)))
  569. return remove_gr, add_gr
  570. ## PROTECTED METHODS ##
  571. def _is_trp_managed(self, t):
  572. """
  573. Whether a triple is server-managed.
  574. :param tuple t: Triple as a 3-tuple of terms.
  575. :rtype: boolean
  576. """
  577. return t[1] in srv_mgd_predicates or (
  578. t[1] == RDF.type and t[2] in srv_mgd_types)
  579. def modify(
  580. self, ev_type, remove_trp=set(), add_trp=set()):
  581. """
  582. Low-level method to modify a graph for a single resource.
  583. This is a crucial point for messaging. Any write operation on the RDF
  584. store that needs to be notified should be performed by invoking this
  585. method.
  586. :param ev_type: The type of event (create, update,
  587. delete) or None. In the latter case, no notification is sent.
  588. :type ev_type: str or None
  589. :param set remove_trp: Triples to be removed.
  590. # Add metadata.
  591. :param set add_trp: Triples to be added.
  592. """
  593. rdfly.modify_rsrc(self.uid, remove_trp, add_trp)
  594. self._clear_cache()
  595. # Reload IMR because if we exit the LMDB txn we lose access to stored
  596. # memory locations.
  597. try:
  598. self.imr
  599. except:
  600. pass
  601. if (
  602. ev_type is not None and
  603. env.app_globals.config['application'].get('messaging')):
  604. logger.debug(f'Enqueuing message for {self.uid}')
  605. self._enqueue_msg(ev_type, remove_trp, add_trp)
  606. def _clear_cache(self):
  607. """
  608. Clear stale model attributes.
  609. This method removes class members populated with data pulled from the
  610. store that may have become stale after a resource update.
  611. """
  612. for attr in self._cached_attrs:
  613. try:
  614. delattr(self, attr)
  615. except AttributeError:
  616. pass
  617. def _enqueue_msg(self, ev_type, remove_trp=None, add_trp=None):
  618. """
  619. Compose a message about a resource change.
  620. The message is enqueued for asynchronous processing.
  621. :param str ev_type: The event type. See global constants.
  622. :param set remove_trp: Triples removed. Only used if the
  623. """
  624. try:
  625. rsrc_type = tuple(str(t) for t in self.types)
  626. actor = self.metadata.value(nsc['fcrepo'].createdBy)
  627. except (ResourceNotExistsError, TombstoneError):
  628. rsrc_type = set()
  629. actor = None
  630. for t in add_trp:
  631. if t[1] == RDF.type:
  632. rsrc_type.add(t[2])
  633. elif actor is None and t[1] == nsc['fcrepo'].createdBy:
  634. actor = t[2]
  635. env.app_globals.changelog.append((set(remove_trp), set(add_trp), {
  636. 'ev_type': ev_type,
  637. 'timestamp': thread_env.timestamp.format(),
  638. 'rsrc_type': rsrc_type,
  639. 'actor': actor,
  640. }))
  641. def _check_ref_int(self, config):
  642. """
  643. Check referential integrity of a resource.
  644. :param str config: If set to ``strict``, a
  645. :class:`lakesuperior.exceptions.RefIntViolationError` is raised.
  646. Otherwise, the violation is simply logged.
  647. """
  648. remove_set = set()
  649. for trp in self.provided_imr:
  650. o = trp[2]
  651. if(
  652. isinstance(o, URIRef) and
  653. str(o).startswith(nsc['fcres']) and
  654. urldefrag(o).url.rstrip('/') != str(self.uri)):
  655. obj_uid = rdfly.uri_to_uid(o)
  656. if not rdfly.ask_rsrc_exists(obj_uid):
  657. if config == 'strict':
  658. raise RefIntViolationError(obj_uid)
  659. else:
  660. # Gather invalid object to sanitize.
  661. remove_set.add(o)
  662. # Remove invalid triples.
  663. for obj in remove_set:
  664. logger.info(
  665. 'Removing link to non-existent repo resource: {}'
  666. .format(obj))
  667. self.provided_imr.remove((None, None, obj))
  668. def _add_srv_mgd_triples(self, create=False):
  669. """
  670. Add server-managed triples to a provided IMR.
  671. :param create: Whether the resource is being created.
  672. """
  673. # Base LDP types.
  674. self.provided_imr.add(
  675. [(self.uri, RDF.type, t) for t in self.base_types]
  676. )
  677. # Create and modify timestamp.
  678. if create:
  679. self.provided_imr.set((
  680. self.uri, nsc['fcrepo'].created, thread_env.timestamp_term))
  681. self.provided_imr.set((
  682. self.uri, nsc['fcrepo'].createdBy, self.DEFAULT_USER))
  683. else:
  684. self.provided_imr.set((
  685. self.uri, nsc['fcrepo'].created,
  686. self.metadata.value(nsc['fcrepo'].created)))
  687. self.provided_imr.set((
  688. self.uri, nsc['fcrepo'].createdBy,
  689. self.metadata.value(nsc['fcrepo'].createdBy)))
  690. self.provided_imr.set((
  691. self.uri, nsc['fcrepo'].lastModified, thread_env.timestamp_term))
  692. self.provided_imr.set((
  693. self.uri, nsc['fcrepo'].lastModifiedBy, self.DEFAULT_USER))
  694. def _containment_rel(self, create, ignore_type=True):
  695. """Find the closest parent in the path indicated by the uid and
  696. establish a containment triple.
  697. Check the path-wise parent of the new resource. If it exists, add the
  698. containment relationship with this UID. Otherwise, create a container
  699. resource as the parent.
  700. This function may recurse up the path tree until an existing container
  701. is found.
  702. E.g. if only fcres:/a exists:
  703. - If ``fcres:/a/b/c/d`` is being created, a becomes container of
  704. ``fcres:/a/b/c/d``. Also, containers are created for fcres:a/b and
  705. ``fcres:/a/b/c``.
  706. - If ``fcres:/e`` is being created, the root node becomes container of
  707. ``fcres:/e``.
  708. :param bool create: Whether the resource is being created. If false,
  709. the parent container is not updated.
  710. "param bool ignore_type: If False (the default), an exception is raised
  711. if trying to create a resource under a non-container. This can be
  712. overridden in special cases (e.g. when migrating a repository in which
  713. a LDP-NR has "children" under ``fcr:versions``) by setting this to
  714. True.
  715. """
  716. from lakesuperior.model.ldp.ldp_factory import LdpFactory
  717. if '/' in self.uid.lstrip('/'):
  718. # Traverse up the hierarchy to find the parent.
  719. path_components = self.uid.lstrip('/').split('/')
  720. cnd_parent_uid = '/' + '/'.join(path_components[:-1])
  721. if rdfly.ask_rsrc_exists(cnd_parent_uid):
  722. parent_rsrc = LdpFactory.from_stored(cnd_parent_uid)
  723. if (
  724. not ignore_type
  725. and nsc['ldp'].Container not in parent_rsrc.types):
  726. raise InvalidResourceError(
  727. cnd_parent_uid, 'Parent {} is not a container.')
  728. parent_uid = cnd_parent_uid
  729. else:
  730. parent_rsrc = LdpFactory.new_container(cnd_parent_uid)
  731. # This will trigger this method again and recurse until an
  732. # existing container or the root node is reached.
  733. parent_rsrc.create_or_replace()
  734. parent_uid = parent_rsrc.uid
  735. else:
  736. parent_uid = ROOT_UID
  737. parent_rsrc = LdpFactory.from_stored(
  738. parent_uid, repr_opts={'incl_children': False}, handling='none')
  739. # Only update parent if the resource is new.
  740. if create:
  741. add_gr = Graph()
  742. add_gr.add({
  743. (nsc['fcres'][parent_uid], nsc['ldp'].contains, self.uri)
  744. })
  745. parent_rsrc.modify(RES_UPDATED, add_trp=add_gr)
  746. # Direct or indirect container relationship.
  747. return self._add_ldp_dc_ic_rel(parent_rsrc)
  748. def _add_ldp_dc_ic_rel(self, cont_rsrc):
  749. """
  750. Add relationship triples from a parent direct or indirect container.
  751. :param rdflib.resource.Resouce cont_rsrc: The container resource.
  752. """
  753. logger.info('Checking direct or indirect containment.')
  754. add_trp = {(self.uri, nsc['fcrepo'].hasParent, cont_rsrc.uri)}
  755. if (
  756. nsc['ldp'].DirectContainer in cont_rsrc.ldp_types
  757. or nsc['ldp'].IndirectContainer in cont_rsrc.ldp_types
  758. ):
  759. from lakesuperior.model.ldp.ldp_factory import LdpFactory
  760. cont_p = cont_rsrc.metadata.terms_by_type('p')
  761. logger.debug('Parent predicates: {}'.format(cont_p))
  762. s = cont_rsrc.metadata.value(self.MBR_RSRC_URI) or cont_rsrc.uri
  763. p = cont_rsrc.metadata.value(self.MBR_REL_URI) or DEF_MBR_REL_URI
  764. #import pdb; pdb.set_trace()
  765. if nsc['ldp'].IndirectContainer in cont_rsrc.ldp_types:
  766. logger.info('Parent is an indirect container.')
  767. cont_rel_uri = cont_rsrc.metadata.value(self.INS_CNT_REL_URI)
  768. o = (
  769. self.provided_imr.value(cont_rel_uri)
  770. or DEF_INS_CNT_REL_URI
  771. )
  772. logger.debug(f'Target URI: {o}')
  773. else:
  774. logger.info('Parent is a direct container.')
  775. o = self.uri
  776. target_rsrc = LdpFactory.from_stored(rdfly.uri_to_uid(s))
  777. target_rsrc.modify(RES_UPDATED, add_trp={(s, p, o)})
  778. return add_trp