ldpr.py 32 KB

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