ldpr.py 34 KB

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