ldpr.py 34 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  1. import logging
  2. from abc import ABCMeta
  3. from collections import defaultdict
  4. from copy import deepcopy
  5. from itertools import accumulate, groupby
  6. from pprint import pformat
  7. from uuid import uuid4
  8. import arrow
  9. from flask import current_app, g, request
  10. from rdflib import Graph
  11. from rdflib.resource import Resource
  12. from rdflib.namespace import RDF
  13. from rdflib.term import URIRef, Literal
  14. from lakesuperior.dictionaries.namespaces import ns_collection as nsc
  15. from lakesuperior.dictionaries.srv_mgd_terms import srv_mgd_subjects, \
  16. srv_mgd_predicates, srv_mgd_types
  17. from lakesuperior.exceptions import *
  18. from lakesuperior.model.ldp_factory import LdpFactory
  19. def atomic(fn):
  20. '''
  21. Handle atomic operations in an RDF store.
  22. This wrapper ensures that a write operation is performed atomically. It
  23. also takes care of sending a message for each resource changed in the
  24. transaction.
  25. '''
  26. def wrapper(self, *args, **kwargs):
  27. request.changelog = []
  28. try:
  29. ret = fn(self, *args, **kwargs)
  30. except:
  31. self._logger.warn('Rolling back transaction.')
  32. self.rdfly.store.rollback()
  33. raise
  34. else:
  35. self._logger.info('Committing transaction.')
  36. self.rdfly.store.commit()
  37. for ev in request.changelog:
  38. self._logger.info('Message: {}'.format(pformat(ev)))
  39. self._send_event_msg(*ev)
  40. return ret
  41. return wrapper
  42. class Ldpr(metaclass=ABCMeta):
  43. '''LDPR (LDP Resource).
  44. Definition: https://www.w3.org/TR/ldp/#ldpr-resource
  45. This class and related subclasses contain the implementation pieces of
  46. the vanilla LDP specifications. This is extended by the
  47. `lakesuperior.fcrepo.Resource` class.
  48. Inheritance graph: https://www.w3.org/TR/ldp/#fig-ldpc-types
  49. Note: Even though LdpNr (which is a subclass of Ldpr) handles binary files,
  50. it still has an RDF representation in the triplestore. Hence, some of the
  51. RDF-related methods are defined in this class rather than in the LdpRs
  52. class.
  53. Convention notes:
  54. All the methods in this class handle internal UUIDs (URN). Public-facing
  55. URIs are converted from URNs and passed by these methods to the methods
  56. handling HTTP negotiation.
  57. The data passed to the store layout for processing should be in a graph.
  58. All conversion from request payload strings is done here.
  59. '''
  60. EMBED_CHILD_RES_URI = nsc['fcrepo'].EmbedResources
  61. FCREPO_PTREE_TYPE = nsc['fcrepo'].Pairtree
  62. INS_CNT_REL_URI = nsc['ldp'].insertedContentRelation
  63. MBR_RSRC_URI = nsc['ldp'].membershipResource
  64. MBR_REL_URI = nsc['ldp'].hasMemberRelation
  65. RETURN_CHILD_RES_URI = nsc['fcrepo'].Children
  66. RETURN_INBOUND_REF_URI = nsc['fcrepo'].InboundReferences
  67. RETURN_SRV_MGD_RES_URI = nsc['fcrepo'].ServerManaged
  68. ROOT_NODE_URN = nsc['fcsystem'].root
  69. # Workflow type. Inbound means that the resource is being written to the
  70. # store, outbounnd is being retrieved for output.
  71. WRKF_INBOUND = '_workflow:inbound_'
  72. WRKF_OUTBOUND = '_workflow:outbound_'
  73. # Default user to be used for the `createdBy` and `lastUpdatedBy` if a user
  74. # is not provided.
  75. DEFAULT_USER = Literal('BypassAdmin')
  76. RES_CREATED = '_create_'
  77. RES_DELETED = '_delete_'
  78. RES_UPDATED = '_update_'
  79. RES_VER_CONT_LABEL = 'fcr:versions'
  80. base_types = {
  81. nsc['fcrepo'].Resource,
  82. nsc['ldp'].Resource,
  83. nsc['ldp'].RDFSource,
  84. }
  85. protected_pred = (
  86. nsc['fcrepo'].created,
  87. nsc['fcrepo'].createdBy,
  88. nsc['ldp'].contains,
  89. )
  90. _logger = logging.getLogger(__name__)
  91. ## MAGIC METHODS ##
  92. def __init__(self, uuid, repr_opts={}, provided_imr=None, **kwargs):
  93. '''Instantiate an in-memory LDP resource that can be loaded from and
  94. persisted to storage.
  95. Persistence is done in this class. None of the operations in the store
  96. layout should commit an open transaction. Methods are wrapped in a
  97. transaction by using the `@atomic` decorator.
  98. @param uuid (string) UUID of the resource. If None (must be explicitly
  99. set) it refers to the root node. It can also be the full URI or URN,
  100. in which case it will be converted.
  101. @param repr_opts (dict) Options used to retrieve the IMR. See
  102. `parse_rfc7240` for format details.
  103. @Param provd_rdf (string) RDF data provided by the client in
  104. operations isuch as `PUT` or `POST`, serialized as a string. This sets
  105. the `provided_imr` property.
  106. '''
  107. self.uuid = g.tbox.uri_to_uuid(uuid) \
  108. if isinstance(uuid, URIRef) else uuid
  109. self.urn = nsc['fcres'][uuid] \
  110. if self.uuid else self.ROOT_NODE_URN
  111. self.uri = g.tbox.uuid_to_uri(self.uuid)
  112. self.rdfly = current_app.rdfly
  113. self.nonrdfly = current_app.nonrdfly
  114. self.provided_imr = provided_imr
  115. @property
  116. def rsrc(self):
  117. '''
  118. The RDFLib resource representing this LDPR. This is a live
  119. representation of the stored data if present.
  120. @return rdflib.resource.Resource
  121. '''
  122. if not hasattr(self, '_rsrc'):
  123. self._rsrc = self.rdfly.ds.resource(self.urn)
  124. return self._rsrc
  125. @property
  126. def imr(self):
  127. '''
  128. Extract an in-memory resource from the graph store.
  129. If the resource is not stored (yet), a `ResourceNotExistsError` is
  130. raised.
  131. @return rdflib.resource.Resource
  132. '''
  133. if not hasattr(self, '_imr'):
  134. if hasattr(self, '_imr_options'):
  135. self._logger.debug('IMR options: {}'.format(self._imr_options))
  136. imr_options = self._imr_options
  137. else:
  138. imr_options = {}
  139. options = dict(imr_options, strict=True)
  140. self._imr = self.rdfly.extract_imr(self.urn, **options)
  141. return self._imr
  142. @property
  143. def stored_or_new_imr(self):
  144. '''
  145. Extract an in-memory resource for harmless manipulation and output.
  146. If the resource is not stored (yet), initialize a new IMR with basic
  147. triples.
  148. @return rdflib.resource.Resource
  149. '''
  150. if not hasattr(self, '_imr'):
  151. if hasattr(self, '_imr_options'):
  152. self._logger.debug('IMR options: {}'.format(self._imr_options))
  153. imr_options = self._imr_options
  154. else:
  155. imr_options = {}
  156. options = dict(imr_options, strict=True)
  157. try:
  158. self._imr = self.rdfly.extract_imr(self.urn, **options)
  159. except ResourceNotExistsError:
  160. self._imr = Resource(Graph(), self.urn)
  161. for t in self.base_types:
  162. self.imr.add(RDF.type, t)
  163. return self._imr
  164. @imr.deleter
  165. def imr(self):
  166. '''
  167. Delete in-memory buffered resource.
  168. '''
  169. delattr(self, '_imr')
  170. @property
  171. def out_graph(self):
  172. '''
  173. Retun a globalized graph of the resource's IMR.
  174. Internal URNs are replaced by global URIs using the endpoint webroot.
  175. '''
  176. out_gr = Graph()
  177. for t in self.imr.graph:
  178. if (
  179. # Exclude digest hash and version information.
  180. t[1] not in {
  181. nsc['premis'].hasMessageDigest,
  182. nsc['fcrepo'].hasVersion,
  183. }
  184. ) and (
  185. # Only include server managed triples if requested.
  186. self._imr_options.get('incl_srv_mgd', True)
  187. or (
  188. not t[1] in srv_mgd_predicates
  189. and not (t[1] == RDF.type or t[2] in srv_mgd_types)
  190. )
  191. ):
  192. out_gr.add(t)
  193. return out_gr
  194. @property
  195. def version_info(self):
  196. '''
  197. Return version metadata (`fcr:versions`).
  198. '''
  199. if not hasattr(self, '_version_info'):
  200. try:
  201. self._version_info = self.rdfly.get_version_info(self.urn)
  202. except ResourceNotExistsError as e:
  203. self._version_info = Graph()
  204. return self._version_info
  205. @property
  206. def versions(self):
  207. '''
  208. Return a generator of version URIs.
  209. '''
  210. return set(self.version_info[self.urn : nsc['fcrepo'].hasVersion :])
  211. @property
  212. def version_uids(self):
  213. '''
  214. Return a generator of version UIDs (relative to their parent resource).
  215. '''
  216. gen = self.version_info[
  217. self.urn
  218. : nsc['fcrepo'].hasVersion / nsc['fcrepo'].hasVersionLabel
  219. :]
  220. return { str(uid) for uid in gen }
  221. @property
  222. def is_stored(self):
  223. if hasattr(self, '_imr'):
  224. return len(self.imr.graph) > 0
  225. else:
  226. return self.rdfly.ask_rsrc_exists(self.urn)
  227. @property
  228. def types(self):
  229. '''All RDF types.
  230. @return set(rdflib.term.URIRef)
  231. '''
  232. if not hasattr(self, '_types'):
  233. if hasattr(self, 'imr') and len(self.imr.graph):
  234. imr = self.imr
  235. elif hasattr(self, 'provided_imr') and \
  236. len(self.provided_imr.graph):
  237. imr = provided_imr
  238. self._types = set(imr.graph[self.urn : RDF.type])
  239. return self._types
  240. @property
  241. def ldp_types(self):
  242. '''The LDP types.
  243. @return set(rdflib.term.URIRef)
  244. '''
  245. if not hasattr(self, '_ldp_types'):
  246. self._ldp_types = { t for t in self.types if nsc['ldp'] in t }
  247. return self._ldp_types
  248. ## LDP METHODS ##
  249. def head(self):
  250. '''
  251. Return values for the headers.
  252. '''
  253. out_headers = defaultdict(list)
  254. digest = self.imr.value(nsc['premis'].hasMessageDigest)
  255. if digest:
  256. etag = digest.identifier.split(':')[-1]
  257. out_headers['ETag'] = 'W/"{}"'.format(etag),
  258. last_updated_term = self.imr.value(nsc['fcrepo'].lastModified)
  259. if last_updated_term:
  260. out_headers['Last-Modified'] = arrow.get(last_updated_term)\
  261. .format('ddd, D MMM YYYY HH:mm:ss Z')
  262. for t in self.ldp_types:
  263. out_headers['Link'].append(
  264. '{};rel="type"'.format(t.n3()))
  265. return out_headers
  266. def get(self):
  267. '''
  268. This gets the RDF metadata. The binary retrieval is handled directly
  269. by the route.
  270. '''
  271. return g.tbox.globalize_graph(self.out_graph)
  272. @atomic
  273. def post(self):
  274. '''
  275. https://www.w3.org/TR/ldp/#ldpr-HTTP_POST
  276. Perform a POST action after a valid resource URI has been found.
  277. '''
  278. return self._create_or_replace_rsrc(create_only=True)
  279. @atomic
  280. def put(self):
  281. '''
  282. https://www.w3.org/TR/ldp/#ldpr-HTTP_PUT
  283. '''
  284. return self._create_or_replace_rsrc()
  285. def patch(self, *args, **kwargs):
  286. raise NotImplementedError()
  287. @atomic
  288. def delete(self, inbound=True, delete_children=True, leave_tstone=True):
  289. '''
  290. https://www.w3.org/TR/ldp/#ldpr-HTTP_DELETE
  291. @param inbound (boolean) If specified, delete all inbound relationships
  292. as well. This is the default and is always the case if referential
  293. integrity is enforced by configuration.
  294. @param delete_children (boolean) Whether to delete all child resources.
  295. This is the default.
  296. '''
  297. refint = current_app.config['store']['ldp_rs']['referential_integrity']
  298. inbound = True if refint else inbound
  299. children = self.imr[nsc['ldp'].contains * '+'] \
  300. if delete_children else []
  301. if leave_tstone:
  302. ret = self._bury_rsrc(inbound)
  303. else:
  304. ret = self._purge_rsrc(inbound)
  305. for child_uri in children:
  306. child_rsrc = LdpFactory.from_stored(
  307. g.tbox.uri_to_uuid(child_uri.identifier),
  308. repr_opts={'incl_children' : False})
  309. if leave_tstone:
  310. child_rsrc._bury_rsrc(inbound, tstone_pointer=self.urn)
  311. else:
  312. child_rsrc._purge_rsrc(inbound)
  313. return ret
  314. @atomic
  315. def resurrect(self):
  316. '''
  317. Resurrect a resource from a tombstone.
  318. @EXPERIMENTAL
  319. '''
  320. tstone_trp = set(self.rdfly.extract_imr(self.urn, strict=False).graph)
  321. ver_rsp = self.version_info.query('''
  322. SELECT ?uid {
  323. ?latest fcrepo:hasVersionLabel ?uid ;
  324. fcrepo:created ?ts .
  325. }
  326. ORDER BY DESC(?ts)
  327. LIMIT 1
  328. ''')
  329. ver_uid = str(ver_rsp.bindings[0]['uid'])
  330. ver_trp = set(self.rdfly.get_version(self.urn, ver_uid))
  331. laz_gr = Graph()
  332. for t in ver_trp:
  333. if t[1] != RDF.type or t[2] not in {
  334. nsc['fcrepo'].Version,
  335. }:
  336. laz_gr.add((self.urn, t[1], t[2]))
  337. laz_gr.add((self.urn, RDF.type, nsc['fcrepo'].Resource))
  338. if nsc['ldp'].NonRdfSource in laz_gr[: RDF.type :]:
  339. laz_gr.add((self.urn, RDF.type, nsc['fcrepo'].Binary))
  340. elif nsc['ldp'].Container in laz_gr[: RDF.type :]:
  341. laz_gr.add((self.urn, RDF.type, nsc['fcrepo'].Container))
  342. self._modify_rsrc(self.RES_CREATED, tstone_trp, set(laz_gr))
  343. self._set_containment_rel()
  344. return self.uri
  345. @atomic
  346. def purge(self, inbound=True):
  347. '''
  348. Delete a tombstone and all historic snapstots.
  349. N.B. This does not trigger an event.
  350. '''
  351. refint = current_app.config['store']['ldp_rs']['referential_integrity']
  352. inbound = True if refint else inbound
  353. return self._purge_rsrc(inbound)
  354. def get_version_info(self):
  355. '''
  356. Get the `fcr:versions` graph.
  357. '''
  358. return g.tbox.globalize_graph(self.version_info)
  359. def get_version(self, ver_uid):
  360. '''
  361. Get a version by label.
  362. '''
  363. ver_gr = self.rdfly.get_version(self.urn, ver_uid)
  364. return g.tbox.globalize_graph(ver_gr)
  365. @atomic
  366. def create_version(self, ver_uid):
  367. '''
  368. Create a new version of the resource.
  369. NOTE: This creates an event only for the resource being updated (due
  370. to the added `hasVersion` triple and possibly to the `hasVersions` one)
  371. but not for the version being created.
  372. @param ver_uid Version ver_uid. If already existing, an exception is
  373. raised.
  374. '''
  375. if not ver_uid or ver_uid in self.version_uids:
  376. ver_uid = str(uuid4())
  377. return g.tbox.globalize_term(self._create_rsrc_version(ver_uid))
  378. @atomic
  379. def revert_to_version(self, ver_uid, backup=True):
  380. '''
  381. Revert to a previous version.
  382. NOTE: this will create a new version.
  383. @param ver_uid (string) Version UID.
  384. @param backup (boolean) Whether to create a backup copy. Default is
  385. true.
  386. '''
  387. # Create a backup snapshot.
  388. if backup:
  389. self.create_version(uuid4())
  390. ver_gr = self.rdfly.get_version(self.urn, ver_uid)
  391. revert_gr = Graph()
  392. for t in ver_gr:
  393. if t[1] not in srv_mgd_predicates and not(
  394. t[1] == RDF.type and t[2] in srv_mgd_types
  395. ):
  396. revert_gr.add((self.urn, t[1], t[2]))
  397. self.provided_imr = revert_gr.resource(self.urn)
  398. return self._create_or_replace_rsrc(create_only=False)
  399. ## PROTECTED METHODS ##
  400. def _create_or_replace_rsrc(self, create_only=False):
  401. '''
  402. Create or update a resource. PUT and POST methods, which are almost
  403. identical, are wrappers for this method.
  404. @param create_only (boolean) Whether this is a create-only operation.
  405. '''
  406. create = create_only or not self.is_stored
  407. self._add_srv_mgd_triples(create)
  408. self._ensure_single_subject_rdf(self.provided_imr.graph)
  409. ref_int = self.rdfly.config['referential_integrity']
  410. if ref_int:
  411. self._check_ref_int(ref_int)
  412. if create:
  413. ev_type = self._create_rsrc()
  414. else:
  415. ev_type = self._replace_rsrc()
  416. self._set_containment_rel()
  417. return ev_type
  418. def _create_rsrc(self):
  419. '''
  420. Create a new resource by comparing an empty graph with the provided
  421. IMR graph.
  422. '''
  423. self._modify_rsrc(self.RES_CREATED, add_trp=self.provided_imr.graph)
  424. return self.RES_CREATED
  425. def _replace_rsrc(self):
  426. '''
  427. Replace a resource.
  428. The existing resource graph is removed except for the protected terms.
  429. '''
  430. # The extracted IMR is used as a "minus" delta, so protected predicates
  431. # must be removed.
  432. for p in self.protected_pred:
  433. self.imr.remove(p)
  434. delta = self._dedup_deltas(self.imr.graph, self.provided_imr.graph)
  435. self._modify_rsrc(self.RES_UPDATED, *delta)
  436. # Reset the IMR because it has changed.
  437. delattr(self, 'imr')
  438. return self.RES_UPDATED
  439. def _bury_rsrc(self, inbound, tstone_pointer=None):
  440. '''
  441. Delete a single resource and create a tombstone.
  442. @param inbound (boolean) Whether to delete the inbound relationships.
  443. @param tstone_pointer (URIRef) If set to a URN, this creates a pointer
  444. to the tombstone of the resource that used to contain the deleted
  445. resource. Otherwise the deleted resource becomes a tombstone.
  446. '''
  447. self._logger.info('Removing resource {}'.format(self.urn))
  448. # Create a backup snapshot for resurrection purposes.
  449. self._create_rsrc_version(uuid4())
  450. remove_trp = self.imr.graph
  451. add_trp = Graph()
  452. if tstone_pointer:
  453. add_trp.add((self.urn, nsc['fcsystem'].tombstone,
  454. tstone_pointer))
  455. else:
  456. add_trp.add((self.urn, RDF.type, nsc['fcsystem'].Tombstone))
  457. add_trp.add((self.urn, nsc['fcrepo'].created, g.timestamp_term))
  458. self._modify_rsrc(self.RES_DELETED, remove_trp, add_trp)
  459. if inbound:
  460. for ib_rsrc_uri in self.imr.graph.subjects(None, self.urn):
  461. remove_trp = {(ib_rsrc_uri, None, self.urn)}
  462. Ldpr(ib_rsrc_uri)._modify_rsrc(self.RES_UPDATED, remove_trp)
  463. return self.RES_DELETED
  464. def _purge_rsrc(self, inbound):
  465. '''
  466. Remove all traces of a resource and versions.
  467. '''
  468. self._logger.info('Purging resource {}'.format(self.urn))
  469. # Remove resource itself.
  470. self.rdfly.modify_dataset({(self.urn, None, None)}, types=None)
  471. # Remove snapshots.
  472. for snap_urn in self.versions:
  473. remove_trp = {
  474. (snap_urn, None, None),
  475. (None, None, snap_urn),
  476. }
  477. self.rdfly.modify_dataset(remove_trp, types={})
  478. # Remove inbound references.
  479. if inbound:
  480. imr = self.rdfly.extract_imr(
  481. self.urn, incl_inbound=True, strict=False)
  482. for ib_rsrc_uri in imr.graph.subjects(None, self.urn):
  483. remove_trp = {(ib_rsrc_uri, None, self.urn)}
  484. Ldpr(ib_rsrc_uri)._modify_rsrc(self.RES_UPDATED, remove_trp)
  485. # @TODO This could be a different event type.
  486. return self.RES_DELETED
  487. def _create_rsrc_version(self, ver_uid):
  488. '''
  489. Perform version creation and return the internal URN.
  490. '''
  491. # Create version resource from copying the current state.
  492. ver_add_gr = Graph()
  493. vers_uuid = '{}/{}'.format(self.uuid, self.RES_VER_CONT_LABEL)
  494. ver_uuid = '{}/{}'.format(vers_uuid, ver_uid)
  495. ver_urn = nsc['fcres'][ver_uuid]
  496. ver_add_gr.add((ver_urn, RDF.type, nsc['fcrepo'].Version))
  497. for t in self.imr.graph:
  498. if (
  499. t[1] == RDF.type and t[2] in {
  500. nsc['fcrepo'].Binary,
  501. nsc['fcrepo'].Container,
  502. nsc['fcrepo'].Resource,
  503. }
  504. ) or (
  505. t[1] in {
  506. nsc['fcrepo'].hasParent,
  507. nsc['fcrepo'].hasVersions,
  508. nsc['premis'].hasMessageDigest,
  509. }
  510. ):
  511. pass
  512. else:
  513. ver_add_gr.add((
  514. g.tbox.replace_term_domain(t[0], self.urn, ver_urn),
  515. t[1], t[2]))
  516. self.rdfly.modify_dataset(
  517. add_trp=ver_add_gr, types={nsc['fcrepo'].Version})
  518. # Add version metadata.
  519. meta_add_gr = Graph()
  520. meta_add_gr.add((
  521. self.urn, nsc['fcrepo'].hasVersion, ver_urn))
  522. meta_add_gr.add(
  523. (ver_urn, nsc['fcrepo'].created, g.timestamp_term))
  524. meta_add_gr.add(
  525. (ver_urn, nsc['fcrepo'].hasVersionLabel, Literal(ver_uid)))
  526. self.rdfly.modify_dataset(
  527. add_trp=meta_add_gr, types={nsc['fcrepo'].Metadata})
  528. # Update resource.
  529. rsrc_add_gr = Graph()
  530. rsrc_add_gr.add((
  531. self.urn, nsc['fcrepo'].hasVersions, nsc['fcres'][vers_uuid]))
  532. self._modify_rsrc(self.RES_UPDATED, add_trp=rsrc_add_gr, notify=False)
  533. return nsc['fcres'][ver_uuid]
  534. def _modify_rsrc(self, ev_type, remove_trp=Graph(), add_trp=Graph(),
  535. notify=True):
  536. '''
  537. Low-level method to modify a graph for a single resource.
  538. This is a crucial point for messaging. Any write operation on the RDF
  539. store that needs to be notified should be performed by invoking this
  540. method.
  541. @param ev_type (string) The type of event (create, update, delete).
  542. @param remove_trp (rdflib.Graph) Triples to be removed.
  543. @param add_trp (rdflib.Graph) Triples to be added.
  544. '''
  545. # If one of the triple sets is not a graph, do a set merge and
  546. # filtering. This is necessary to support non-RDF terms (e.g.
  547. # variables).
  548. if not isinstance(remove_trp, Graph) or not isinstance(add_trp, Graph):
  549. if isinstance(remove_trp, Graph):
  550. remove_trp = set(remove_trp)
  551. if isinstance(add_trp, Graph):
  552. add_trp = set(add_trp)
  553. merge_gr = remove_trp | add_trp
  554. type = { trp[2] for trp in merge_gr if trp[1] == RDF.type }
  555. actor = { trp[2] for trp in merge_gr \
  556. if trp[1] == nsc['fcrepo'].createdBy }
  557. else:
  558. merge_gr = remove_trp | add_trp
  559. type = merge_gr[self.urn : RDF.type]
  560. actor = merge_gr[self.urn : nsc['fcrepo'].createdBy]
  561. ret = self.rdfly.modify_dataset(remove_trp, add_trp)
  562. if notify and current_app.config.get('messaging'):
  563. request.changelog.append((set(remove_trp), set(add_trp), {
  564. 'ev_type' : ev_type,
  565. 'time' : g.timestamp,
  566. 'type' : type,
  567. 'actor' : actor,
  568. }))
  569. return ret
  570. def _ensure_single_subject_rdf(self, gr):
  571. '''
  572. Ensure that a RDF payload for a POST or PUT has a single resource.
  573. '''
  574. for s in set(gr.subjects()):
  575. if not s == self.urn:
  576. raise SingleSubjectError(s, self.uuid)
  577. def _check_ref_int(self, config):
  578. gr = self.provided_imr.graph
  579. for o in gr.objects():
  580. if isinstance(o, URIRef) and str(o).startswith(g.webroot)\
  581. and not self.rdfly.ask_rsrc_exists(o):
  582. if config == 'strict':
  583. raise RefIntViolationError(o)
  584. else:
  585. self._logger.info(
  586. 'Removing link to non-existent repo resource: {}'
  587. .format(o))
  588. gr.remove((None, None, o))
  589. def _check_mgd_terms(self, gr):
  590. '''
  591. Check whether server-managed terms are in a RDF payload.
  592. '''
  593. # @FIXME Need to be more consistent
  594. if getattr(self, 'handling', 'none') == 'none':
  595. return gr
  596. offending_subjects = set(gr.subjects()) & srv_mgd_subjects
  597. if offending_subjects:
  598. if self.handling=='strict':
  599. raise ServerManagedTermError(offending_subjects, 's')
  600. else:
  601. for s in offending_subjects:
  602. self._logger.info('Removing offending subj: {}'.format(s))
  603. gr.remove((s, None, None))
  604. offending_predicates = set(gr.predicates()) & srv_mgd_predicates
  605. if offending_predicates:
  606. if self.handling=='strict':
  607. raise ServerManagedTermError(offending_predicates, 'p')
  608. else:
  609. for p in offending_predicates:
  610. self._logger.info('Removing offending pred: {}'.format(p))
  611. gr.remove((None, p, None))
  612. offending_types = set(gr.objects(predicate=RDF.type)) & srv_mgd_types
  613. if offending_types:
  614. if self.handling=='strict':
  615. raise ServerManagedTermError(offending_types, 't')
  616. else:
  617. for t in offending_types:
  618. self._logger.info('Removing offending type: {}'.format(t))
  619. gr.remove((None, RDF.type, t))
  620. self._logger.debug('Sanitized graph: {}'.format(gr.serialize(
  621. format='turtle').decode('utf-8')))
  622. return gr
  623. def _sparql_delta(self, q):
  624. '''
  625. Calculate the delta obtained by a SPARQL Update operation.
  626. This is a critical component of the SPARQL query prcess and does a
  627. couple of things:
  628. 1. It ensures that no resources outside of the subject of the request
  629. are modified (e.g. by variable subjects)
  630. 2. It verifies that none of the terms being modified is server managed.
  631. This method extracts an in-memory copy of the resource and performs the
  632. query on that once it has checked if any of the server managed terms is
  633. in the delta. If it is, it raises an exception.
  634. NOTE: This only checks if a server-managed term is effectively being
  635. modified. If a server-managed term is present in the query but does not
  636. cause any change in the updated resource, no error is raised.
  637. @return tuple(rdflib.Graph) Remove and add graphs. These can be used
  638. with `BaseStoreLayout.update_resource` and/or recorded as separate
  639. events in a provenance tracking system.
  640. '''
  641. pre_gr = self.imr.graph
  642. post_gr = deepcopy(pre_gr)
  643. post_gr.update(q)
  644. remove_gr, add_gr = self._dedup_deltas(pre_gr, post_gr)
  645. #self._logger.info('Removing: {}'.format(
  646. # remove_gr.serialize(format='turtle').decode('utf8')))
  647. #self._logger.info('Adding: {}'.format(
  648. # add_gr.serialize(format='turtle').decode('utf8')))
  649. remove_gr = self._check_mgd_terms(remove_gr)
  650. add_gr = self._check_mgd_terms(add_gr)
  651. return remove_gr, add_gr
  652. def _add_srv_mgd_triples(self, create=False):
  653. '''
  654. Add server-managed triples to a provided IMR.
  655. @param create (boolean) Whether the resource is being created.
  656. '''
  657. # Base LDP types.
  658. for t in self.base_types:
  659. self.provided_imr.add(RDF.type, t)
  660. # Message digest.
  661. cksum = g.tbox.rdf_cksum(self.provided_imr.graph)
  662. self.provided_imr.set(nsc['premis'].hasMessageDigest,
  663. URIRef('urn:sha1:{}'.format(cksum)))
  664. # Create and modify timestamp.
  665. if create:
  666. self.provided_imr.set(nsc['fcrepo'].created, g.timestamp_term)
  667. self.provided_imr.set(nsc['fcrepo'].createdBy, self.DEFAULT_USER)
  668. self.provided_imr.set(nsc['fcrepo'].lastModified, g.timestamp_term)
  669. self.provided_imr.set(nsc['fcrepo'].lastModifiedBy, self.DEFAULT_USER)
  670. def _set_containment_rel(self):
  671. '''Find the closest parent in the path indicated by the UUID and
  672. establish a containment triple.
  673. E.g. if only urn:fcres:a (short: a) exists:
  674. - If a/b/c/d is being created, a becomes container of a/b/c/d. Also,
  675. pairtree nodes are created for a/b and a/b/c.
  676. - If e is being created, the root node becomes container of e.
  677. '''
  678. # @FIXME Circular reference.
  679. from lakesuperior.model.ldp_rs import Ldpc
  680. if self.urn == self.ROOT_NODE_URN:
  681. return
  682. elif '/' in self.uuid:
  683. # Traverse up the hierarchy to find the parent.
  684. parent_uri = self._find_parent_or_create_pairtree(self.uuid)
  685. else:
  686. parent_uri = self.ROOT_NODE_URN
  687. add_gr = Graph()
  688. add_gr.add((parent_uri, nsc['ldp'].contains, self.urn))
  689. parent_rsrc = LdpFactory.from_stored(
  690. g.tbox.uri_to_uuid(parent_uri), repr_opts={
  691. 'incl_children' : False}, handling='none')
  692. parent_rsrc._modify_rsrc(self.RES_UPDATED, add_trp=add_gr)
  693. # Direct or indirect container relationship.
  694. self._add_ldp_dc_ic_rel(parent_uri)
  695. def _find_parent_or_create_pairtree(self, uuid):
  696. '''
  697. Check the path-wise parent of the new resource. If it exists, return
  698. its URI. Otherwise, create pairtree resources up the path until an
  699. actual resource or the root node is found.
  700. @return rdflib.term.URIRef
  701. '''
  702. path_components = uuid.split('/')
  703. # If there is only on element, the parent is the root node.
  704. if len(path_components) < 2:
  705. return self.ROOT_NODE_URN
  706. # Build search list, e.g. for a/b/c/d/e would be a/b/c/d, a/b/c, a/b, a
  707. self._logger.info('Path components: {}'.format(path_components))
  708. fwd_search_order = accumulate(
  709. list(path_components)[:-1],
  710. func=lambda x,y : x + '/' + y
  711. )
  712. rev_search_order = reversed(list(fwd_search_order))
  713. cur_child_uri = nsc['fcres'][uuid]
  714. for cparent_uuid in rev_search_order:
  715. cparent_uri = nsc['fcres'][cparent_uuid]
  716. if self.rdfly.ask_rsrc_exists(cparent_uri):
  717. return cparent_uri
  718. else:
  719. self._create_path_segment(cparent_uri, cur_child_uri)
  720. cur_child_uri = cparent_uri
  721. return self.ROOT_NODE_URN
  722. def _dedup_deltas(self, remove_gr, add_gr):
  723. '''
  724. Remove duplicate triples from add and remove delta graphs, which would
  725. otherwise contain unnecessary statements that annul each other.
  726. '''
  727. return (
  728. remove_gr - add_gr,
  729. add_gr - remove_gr
  730. )
  731. def _create_path_segment(self, uri, child_uri):
  732. '''
  733. Create a path segment with a non-LDP containment statement.
  734. This diverges from the default fcrepo4 behavior which creates pairtree
  735. resources.
  736. If a resource such as `fcres:a/b/c` is created, and neither fcres:a or
  737. fcres:a/b exists, we have to create two "hidden" containment statements
  738. between a and a/b and between a/b and a/b/c in order to maintain the
  739. `containment chain.
  740. '''
  741. imr = Resource(Graph(), uri)
  742. imr.add(RDF.type, nsc['ldp'].Container)
  743. imr.add(RDF.type, nsc['ldp'].BasicContainer)
  744. imr.add(RDF.type, nsc['ldp'].RDFSource)
  745. imr.add(RDF.type, nsc['fcrepo'].Pairtree)
  746. imr.add(nsc['fcrepo'].contains, child_uri)
  747. # If the path segment is just below root
  748. if '/' not in str(uri):
  749. imr.graph.add((nsc['fcsystem'].root, nsc['fcrepo'].contains, uri))
  750. self.rdfly.modify_dataset(add_trp=imr.graph)
  751. def _add_ldp_dc_ic_rel(self, cont_uri):
  752. '''
  753. Add relationship triples from a parent direct or indirect container.
  754. @param cont_uri (rdflib.term.URIRef) The container URI.
  755. '''
  756. cont_uuid = g.tbox.uri_to_uuid(cont_uri)
  757. cont_rsrc = LdpFactory.from_stored(cont_uuid,
  758. repr_opts={'incl_children' : False})
  759. cont_p = set(cont_rsrc.imr.graph.predicates())
  760. add_gr = Graph()
  761. self._logger.info('Checking direct or indirect containment.')
  762. self._logger.debug('Parent predicates: {}'.format(cont_p))
  763. if self.MBR_RSRC_URI in cont_p and self.MBR_REL_URI in cont_p:
  764. s = g.tbox.localize_term(
  765. cont_rsrc.imr.value(self.MBR_RSRC_URI).identifier)
  766. p = cont_rsrc.imr.value(self.MBR_REL_URI).identifier
  767. if cont_rsrc.imr[RDF.type : nsc['ldp'].DirectContainer]:
  768. self._logger.info('Parent is a direct container.')
  769. self._logger.debug('Creating DC triples.')
  770. add_gr.add((s, p, self.urn))
  771. elif cont_rsrc.imr[RDF.type : nsc['ldp'].IndirectContainer] \
  772. and self.INS_CNT_REL_URI in cont_p:
  773. self._logger.info('Parent is an indirect container.')
  774. cont_rel_uri = cont_rsrc.imr.value(self.INS_CNT_REL_URI).identifier
  775. target_uri = self.provided_imr.value(cont_rel_uri).identifier
  776. self._logger.debug('Target URI: {}'.format(target_uri))
  777. if target_uri:
  778. self._logger.debug('Creating IC triples.')
  779. add_gr.add((s, p, target_uri))
  780. if len(add_gr):
  781. add_gr = self._check_mgd_terms(add_gr)
  782. self._logger.debug('Adding DC/IC triples: {}'.format(
  783. add_gr.serialize(format='turtle').decode('utf-8')))
  784. self._modify_rsrc(self.RES_UPDATED, add_trp=add_gr)
  785. def _send_event_msg(self, remove_trp, add_trp, metadata):
  786. '''
  787. Break down delta triples, find subjects and send event message.
  788. '''
  789. remove_grp = groupby(remove_trp, lambda x : x[0])
  790. remove_dict = { k[0] : k[1] for k in remove_grp }
  791. add_grp = groupby(add_trp, lambda x : x[0])
  792. add_dict = { k[0] : k[1] for k in add_grp }
  793. subjects = set(remove_dict.keys()) | set(add_dict.keys())
  794. for rsrc_uri in subjects:
  795. self._logger.info('subject: {}'.format(rsrc_uri))
  796. #current_app.messenger.send