ldpr.py 29 KB

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