ldpr.py 36 KB

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