ldpr.py 29 KB

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