ldpr.py 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514
  1. import logging
  2. from abc import ABCMeta
  3. from collections import defaultdict
  4. from importlib import import_module
  5. from itertools import accumulate
  6. from uuid import uuid4
  7. import arrow
  8. from rdflib import Graph
  9. from rdflib.resource import Resource
  10. from rdflib.namespace import RDF, XSD
  11. from lakesuperior.config_parser import config
  12. from lakesuperior.dictionaries.namespaces import ns_collection as nsc
  13. from lakesuperior.exceptions import InvalidResourceError, \
  14. ResourceNotExistsError, ServerManagedTermError
  15. from lakesuperior.store_layouts.rdf.base_rdf_layout import BaseRdfLayout
  16. from lakesuperior.util.translator import Translator
  17. def transactional(fn):
  18. '''
  19. Decorator for methods of the Ldpr class to handle transactions in an RDF
  20. store.
  21. '''
  22. def wrapper(self, *args, **kwargs):
  23. try:
  24. ret = fn(self, *args, **kwargs)
  25. self._logger.info('Committing transaction.')
  26. self.rdfly.store.commit()
  27. return ret
  28. except:
  29. self._logger.warn('Rolling back transaction.')
  30. self.rdfly.store.rollback()
  31. raise
  32. return wrapper
  33. def must_exist(fn):
  34. '''
  35. Ensures that a method is applied to a stored resource.
  36. Decorator for methods of the Ldpr class.
  37. '''
  38. def wrapper(self, *args, **kwargs):
  39. if not self.is_stored:
  40. raise ResourceNotExistsError(self.uuid)
  41. return fn(self, *args, **kwargs)
  42. return wrapper
  43. def must_not_exist(fn):
  44. '''
  45. Ensures that a method is applied to a resource that is not stored.
  46. Decorator for methods of the Ldpr class.
  47. '''
  48. def wrapper(self, *args, **kwargs):
  49. if self.is_stored:
  50. raise ResourceExistsError(self.uuid)
  51. return fn(self, *args, **kwargs)
  52. return wrapper
  53. class Ldpr(metaclass=ABCMeta):
  54. '''LDPR (LDP Resource).
  55. Definition: https://www.w3.org/TR/ldp/#ldpr-resource
  56. This class and related subclasses contain the implementation pieces of
  57. the vanilla LDP specifications. This is extended by the
  58. `lakesuperior.fcrepo.Resource` class.
  59. Inheritance graph: https://www.w3.org/TR/ldp/#fig-ldpc-types
  60. Note: Even though LdpNr (which is a subclass of Ldpr) handles binary files,
  61. it still has an RDF representation in the triplestore. Hence, some of the
  62. RDF-related methods are defined in this class rather than in the LdpRs
  63. class.
  64. Convention notes:
  65. All the methods in this class handle internal UUIDs (URN). Public-facing
  66. URIs are converted from URNs and passed by these methods to the methods
  67. handling HTTP negotiation.
  68. The data passed to the store layout for processing should be in a graph.
  69. All conversion from request payload strings is done here.
  70. '''
  71. FCREPO_PTREE_TYPE = nsc['fcrepo'].Pairtree
  72. LDP_NR_TYPE = nsc['ldp'].NonRDFSource
  73. LDP_RS_TYPE = nsc['ldp'].RDFSource
  74. _logger = logging.getLogger(__name__)
  75. rdf_store_layout = config['application']['store']['ldp_rs']['layout']
  76. non_rdf_store_layout = config['application']['store']['ldp_nr']['layout']
  77. ## MAGIC METHODS ##
  78. def __init__(self, uuid):
  79. '''Instantiate an in-memory LDP resource that can be loaded from and
  80. persisted to storage.
  81. Persistence is done in this class. None of the operations in the store
  82. layout should commit an open transaction. Methods are wrapped in a
  83. transaction by using the `@transactional` decorator.
  84. @param uuid (string) UUID of the resource.
  85. '''
  86. self.uuid = uuid
  87. self._urn = nsc['fcres'][uuid] if self.uuid is not None \
  88. else BaseRdfLayout.ROOT_NODE_URN
  89. @property
  90. def urn(self):
  91. '''
  92. The internal URI (URN) for the resource as stored in the triplestore.
  93. This is a URN that needs to be converted to a global URI for the LDP
  94. API.
  95. @return rdflib.URIRef
  96. '''
  97. return self._urn
  98. @property
  99. def uri(self):
  100. '''
  101. The URI for the resource as published by the REST API.
  102. @return rdflib.URIRef
  103. '''
  104. return Translator.uuid_to_uri(self.uuid)
  105. @property
  106. def rdfly(self):
  107. '''
  108. Load RDF store layout.
  109. '''
  110. if not hasattr(self, '_rdfly'):
  111. self._rdfly = __class__.load_layout('rdf')
  112. return self._rdfly
  113. @property
  114. def rsrc(self):
  115. '''
  116. The RDFLib resource representing this LDPR. This is a live
  117. representation of the stored data if present.
  118. @return rdflib.resource.Resource
  119. '''
  120. if not hasattr(self, '_rsrc'):
  121. self._rsrc = self.rdfly.rsrc(self.urn)
  122. return self._rsrc
  123. @property
  124. def imr(self):
  125. '''
  126. Extract an in-memory resource for harmless manipulation and output.
  127. If the resource is not stored (yet), initialize a new IMR with basic
  128. triples.
  129. @return rdflib.resource.Resource
  130. '''
  131. if not hasattr(self, '_imr'):
  132. if not self.is_stored:
  133. self._imr = Resource(Graph(), self.urn)
  134. for t in self.base_types:
  135. self.imr.add(RDF.type, t)
  136. else:
  137. self._imr = self.rdfly.extract_imr(self.urn)
  138. return self._imr
  139. @imr.deleter
  140. def imr(self):
  141. '''
  142. Delete in-memory buffered resource.
  143. '''
  144. delattr(self, '_imr')
  145. @property
  146. def is_stored(self):
  147. return self.rdfly.ask_rsrc_exists(self.urn)
  148. @property
  149. def types(self):
  150. '''All RDF types.
  151. @return set(rdflib.term.URIRef)
  152. '''
  153. if not hasattr(self, '_types'):
  154. self._types = set(self.rsrc[RDF.type])
  155. return self._types
  156. @property
  157. def ldp_types(self):
  158. '''The LDP types.
  159. @return set(rdflib.term.URIRef)
  160. '''
  161. if not hasattr(self, '_ldp_types'):
  162. self._ldp_types = set()
  163. for t in self.types:
  164. if t.qname()[:4] == 'ldp:':
  165. self._ldp_types.add(t)
  166. return self._ldp_types
  167. @property
  168. def containment(self):
  169. if not hasattr(self, '_containment'):
  170. q = '''
  171. SELECT ?container ?contained {
  172. {
  173. ?s ldp:contains ?contained .
  174. } UNION {
  175. ?container ldp:contains ?s .
  176. }
  177. }
  178. '''
  179. qres = self.rsrc.graph.query(q, initBindings={'s' : self.urn})
  180. # There should only be one container.
  181. for t in qres:
  182. if t[0]:
  183. container = self.rdfly.ds.resource(t[0])
  184. contains = ( self.rdfly.ds.resource(t[1]) for t in qres if t[1] )
  185. self._containment = {
  186. 'container' : container, 'contains' : contains}
  187. return self._containment
  188. @containment.deleter
  189. def containment(self):
  190. '''
  191. Reset containment variable when changing containment triples.
  192. '''
  193. del self._containment
  194. @property
  195. def container(self):
  196. return self.containment['container']
  197. @property
  198. def contains(self):
  199. return self.containment['contains']
  200. ## STATIC & CLASS METHODS ##
  201. @classmethod
  202. def load_layout(cls, type, uuid=None):
  203. '''
  204. Dynamically load the store layout indicated in the configuration.
  205. @param type (string) One of `rdf` or `non_rdf`. Determines the type of
  206. layout to be loaded.
  207. @param uuid (string) UUID of the base resource. For RDF layouts only.
  208. '''
  209. layout_cls = getattr(cls, '{}_store_layout'.format(type))
  210. store_mod = import_module('lakesuperior.store_layouts.{0}.{1}'.format(
  211. type, layout_cls))
  212. layout_cls = getattr(store_mod, Translator.camelcase(layout_cls))
  213. return layout_cls()
  214. @classmethod
  215. def readonly_inst(cls, uuid):
  216. '''
  217. Factory method that creates and returns an instance of an LDPR subclass
  218. based on information that needs to be queried from the underlying
  219. graph store.
  220. This is used with retrieval methods for resources that already exist.
  221. @param uuid UUID of the instance.
  222. '''
  223. rdfly = cls.load_layout('rdf')
  224. imr_urn = nsc['fcres'][uuid] if uuid else rdfly.ROOT_NODE_URN
  225. imr = rdfly.extract_imr(imr_urn, minimal=True)
  226. rdf_types = imr.objects(RDF.type)
  227. for t in rdf_types:
  228. cls._logger.debug('Checking RDF type: {}'.format(t.identifier))
  229. if t.identifier == cls.LDP_NR_TYPE:
  230. from lakesuperior.model.ldp_nr import LdpNr
  231. cls._logger.info('Resource is a LDP-NR.')
  232. return LdpNr(uuid)
  233. if t.identifier == cls.LDP_RS_TYPE:
  234. from lakesuperior.model.ldp_rs import LdpRs
  235. cls._logger.info('Resource is a LDP-RS.')
  236. return LdpRs(uuid)
  237. raise ResourceNotExistsError(uuid)
  238. @classmethod
  239. def inst_for_post(cls, parent_uuid=None, slug=None):
  240. '''
  241. Validate conditions to perform a POST and return an LDP resource
  242. instancefor using with the `post` method.
  243. This may raise an exception resulting in a 404 if the parent is not
  244. found or a 409 if the parent is not a valid container.
  245. '''
  246. # Shortcut!
  247. if not slug and not parent_uuid:
  248. return cls(str(uuid4()))
  249. rdfly = cls.load_layout('rdf')
  250. parent_imr_urn = nsc['fcres'][parent_uuid] if parent_uuid \
  251. else rdfly.ROOT_NODE_URN
  252. parent_imr = rdfly.extract_imr(parent_imr_urn, minimal=True)
  253. if not len(parent_imr.graph):
  254. raise ResourceNotExistsError(parent_uuid)
  255. # Set prefix.
  256. if parent_uuid:
  257. parent_types = { t.identifier for t in \
  258. parent_imr.objects(RDF.type) }
  259. cls._logger.debug('Parent types: {}'.format(
  260. parent_types))
  261. if nsc['ldp'].Container not in parent_types:
  262. raise InvalidResourceError('Parent {} is not a container.'
  263. .format(parent_uuid))
  264. pfx = parent_uuid + '/'
  265. else:
  266. pfx = ''
  267. # Create candidate UUID and validate.
  268. if slug:
  269. cnd_uuid = pfx + slug
  270. cnd_rsrc = Resource(rdfly.ds, nsc['fcres'][cnd_uuid])
  271. if rdfly.ask_rsrc_exists(cnd_rsrc.identifier):
  272. return cls(pfx + str(uuid4()))
  273. else:
  274. return cls(cnd_uuid)
  275. else:
  276. return cls(pfx + str(uuid4()))
  277. ## LDP METHODS ##
  278. def head(self):
  279. '''
  280. Return values for the headers.
  281. '''
  282. out_headers = defaultdict(list)
  283. digest = self.imr.value(nsc['premis'].hasMessageDigest)
  284. if digest:
  285. etag = digest.identifier.split(':')[-1]
  286. out_headers['ETag'] = 'W/"{}"'.format(etag),
  287. last_updated_term = self.imr.value(nsc['fcrepo'].lastModified)
  288. if last_updated_term:
  289. out_headers['Last-Modified'] = arrow.get(last_updated_term)\
  290. .format('ddd, D MMM YYYY HH:mm:ss Z')
  291. for t in self.ldp_types:
  292. out_headers['Link'].append(
  293. '{};rel="type"'.format(t.identifier.n3()))
  294. return out_headers
  295. def get(self, *args, **kwargs):
  296. raise NotImplementedError()
  297. def post(self, *args, **kwargs):
  298. raise NotImplementedError()
  299. def put(self, *args, **kwargs):
  300. raise NotImplementedError()
  301. def patch(self, *args, **kwargs):
  302. raise NotImplementedError()
  303. @transactional
  304. @must_exist
  305. def delete(self):
  306. '''
  307. https://www.w3.org/TR/ldp/#ldpr-HTTP_DELETE
  308. '''
  309. self.rdfly.delete_rsrc(self.urn)
  310. ## PROTECTED METHODS ##
  311. def _set_containment_rel(self):
  312. '''Find the closest parent in the path indicated by the UUID and
  313. establish a containment triple.
  314. E.g.
  315. - If only urn:fcres:a (short: a) exists:
  316. - If a/b/c/d is being created, a becomes container of a/b/c/d. Also,
  317. pairtree nodes are created for a/b and a/b/c.
  318. - If e is being created, the root node becomes container of e.
  319. '''
  320. if '/' in self.uuid:
  321. # Traverse up the hierarchy to find the parent.
  322. cparent_uri = self._find_parent_or_create_pairtree(self.uuid)
  323. # Reroute possible containment relationships between parent and new
  324. # resource.
  325. #self._splice_in(cparent)
  326. if cparent_uri:
  327. self.rdfly.ds.add((cparent_uri, nsc['ldp'].contains,
  328. self.rsrc.identifier))
  329. else:
  330. self.rsrc.graph.add((nsc['fcsystem'].root, nsc['ldp'].contains,
  331. self.rsrc.identifier))
  332. # If a resource has no parent and should be parent of the new resource,
  333. # add the relationship.
  334. #for child_uri in self.find_lost_children():
  335. # self.rsrc.add(nsc['ldp'].contains, child_uri)
  336. def _find_parent_or_create_pairtree(self, uuid):
  337. '''
  338. Check the path-wise parent of the new resource. If it exists, return
  339. its URI. Otherwise, create pairtree resources up the path until an
  340. actual resource or the root node is found.
  341. @return rdflib.term.URIRef
  342. '''
  343. path_components = uuid.split('/')
  344. if len(path_components) < 2:
  345. return None
  346. # Build search list, e.g. for a/b/c/d/e would be a/b/c/d, a/b/c, a/b, a
  347. self._logger.info('Path components: {}'.format(path_components))
  348. fwd_search_order = accumulate(
  349. list(path_components)[:-1],
  350. func=lambda x,y : x + '/' + y
  351. )
  352. rev_search_order = reversed(list(fwd_search_order))
  353. cur_child_uri = nsc['fcres'][uuid]
  354. for cparent_uuid in rev_search_order:
  355. cparent_uri = nsc['fcres'][cparent_uuid]
  356. if self.rdfly.ask_rsrc_exists(cparent_uri):
  357. return cparent_uri
  358. else:
  359. self._create_path_segment(cparent_uri, cur_child_uri)
  360. cur_child_uri = cparent_uri
  361. return None
  362. def _create_path_segment(self, uri, child_uri):
  363. '''
  364. Create a path segment with a non-LDP containment statement.
  365. This diverges from the default fcrepo4 behavior which creates pairtree
  366. resources.
  367. If a resource such as `fcres:a/b/c` is created, and neither fcres:a or
  368. fcres:a/b exists, we have to create two "hidden" containment statements
  369. between a and a/b and between a/b and a/b/c in order to maintain the
  370. `containment chain.
  371. '''
  372. g = Graph()
  373. g.add((uri, RDF.type, nsc['ldp'].Container))
  374. g.add((uri, RDF.type, nsc['ldp'].BasicContainer))
  375. g.add((uri, RDF.type, nsc['ldp'].RDFSource))
  376. g.add((uri, nsc['fcrepo'].contains, child_uri))
  377. # If the path segment is just below root
  378. if '/' not in str(uri):
  379. g.add((nsc['fcsystem'].root, nsc['fcrepo'].contains, uri))
  380. self.rdfly.create_rsrc(g)