ldpr.py 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675
  1. import logging
  2. from abc import ABCMeta
  3. from importlib import import_module
  4. from itertools import accumulate
  5. from uuid import uuid4
  6. import arrow
  7. from rdflib import Graph
  8. from rdflib.resource import Resource
  9. from rdflib.namespace import RDF, XSD
  10. from rdflib.term import Literal
  11. from lakesuperior.config_parser import config
  12. from lakesuperior.connectors.filesystem_connector import FilesystemConnector
  13. from lakesuperior.core.namespaces import ns_collection as nsc
  14. from lakesuperior.util.translator import Translator
  15. class ResourceExistsError(RuntimeError):
  16. '''
  17. Raised in an attempt to create a resource a URN that already exists and is
  18. not supposed to.
  19. This usually surfaces at the HTTP level as a 409.
  20. '''
  21. pass
  22. class ResourceNotExistsError(RuntimeError):
  23. '''
  24. Raised in an attempt to create a resource a URN that does not exist and is
  25. supposed to.
  26. This usually surfaces at the HTTP level as a 404.
  27. '''
  28. pass
  29. class InvalidResourceError(RuntimeError):
  30. '''
  31. Raised when an invalid resource is found.
  32. This usually surfaces at the HTTP level as a 409 or other error.
  33. '''
  34. pass
  35. def transactional(fn):
  36. '''
  37. Decorator for methods of the Ldpr class to handle transactions in an RDF
  38. store.
  39. '''
  40. def wrapper(self, *args, **kwargs):
  41. try:
  42. ret = fn(self, *args, **kwargs)
  43. print('Committing transaction.')
  44. self.gs.conn.store.commit()
  45. return ret
  46. except:
  47. print('Rolling back transaction.')
  48. self.gs.conn.store.rollback()
  49. raise
  50. return wrapper
  51. def must_exist(fn):
  52. '''
  53. Ensures that a method is applied to a stored resource.
  54. Decorator for methods of the Ldpr class.
  55. '''
  56. def wrapper(self, *args, **kwargs):
  57. if not self.is_stored:
  58. raise ResourceNotExistsError(
  59. 'Resource #{} not found'.format(self.uuid))
  60. return fn(self, *args, **kwargs)
  61. return wrapper
  62. def must_not_exist(fn):
  63. '''
  64. Ensures that a method is applied to a resource that is not stored.
  65. Decorator for methods of the Ldpr class.
  66. '''
  67. def wrapper(self, *args, **kwargs):
  68. if self.is_stored:
  69. raise ResourceExistsError(
  70. 'Resource #{} already exists.'.format(self.uuid))
  71. return fn(self, *args, **kwargs)
  72. return wrapper
  73. class Ldpr(metaclass=ABCMeta):
  74. '''LDPR (LDP Resource).
  75. Definition: https://www.w3.org/TR/ldp/#ldpr-resource
  76. This class and related subclasses contain the implementation pieces of
  77. the vanilla LDP specifications. This is extended by the
  78. `lakesuperior.fcrepo.Resource` class.
  79. Inheritance graph: https://www.w3.org/TR/ldp/#fig-ldpc-types
  80. Note: Even though LdpNr (which is a subclass of Ldpr) handles binary files,
  81. it still has an RDF representation in the triplestore. Hence, some of the
  82. RDF-related methods are defined in this class rather than in the LdpRs
  83. class.
  84. Convention notes:
  85. All the methods in this class handle internal UUIDs (URN). Public-facing
  86. URIs are converted from URNs and passed by these methods to the methods
  87. handling HTTP negotiation.
  88. The data passed to the store strategy for processing should be in a graph.
  89. All conversion from request payload strings is done here.
  90. '''
  91. FCREPO_PTREE_TYPE = nsc['fedora'].Pairtree
  92. LDP_NR_TYPE = nsc['ldp'].NonRDFSource
  93. LDP_RS_TYPE = nsc['ldp'].RDFSource
  94. _logger = logging.getLogger(__module__)
  95. store_strategy = config['application']['store']['ldp_rs']['strategy']
  96. ## MAGIC METHODS ##
  97. def __init__(self, uuid):
  98. '''Instantiate an in-memory LDP resource that can be loaded from and
  99. persisted to storage.
  100. Persistence is done in this class. None of the operations in the store
  101. strategy should commit an open transaction. Methods are wrapped in a
  102. transaction by using the `@transactional` decorator.
  103. @param uuid (string) UUID of the resource.
  104. '''
  105. self.uuid = uuid
  106. # Dynamically load the store strategy indicated in the configuration.
  107. store_mod = import_module(
  108. 'lakesuperior.store_strategies.rdf.{}'.format(
  109. self.store_strategy))
  110. self._rdf_store_cls = getattr(store_mod, Translator.camelcase(
  111. self.store_strategy))
  112. self.gs = self._rdf_store_cls(self.urn)
  113. # Same thing coud be done for the filesystem store strategy, but we
  114. # will keep it simple for now.
  115. self.fs = FilesystemConnector()
  116. @property
  117. def urn(self):
  118. '''
  119. The internal URI (URN) for the resource as stored in the triplestore.
  120. This is a URN that needs to be converted to a global URI for the REST
  121. API.
  122. @return rdflib.URIRef
  123. '''
  124. return nsc['fcres'][self.uuid]
  125. @property
  126. def uri(self):
  127. '''
  128. The URI for the resource as published by the REST API.
  129. @return rdflib.URIRef
  130. '''
  131. return Translator.uuid_to_uri(self.uuid)
  132. @property
  133. def rsrc(self):
  134. '''
  135. The RDFLib resource representing this LDPR. This is a copy of the
  136. stored data if present, and what gets passed to most methods of the
  137. store strategy methods.
  138. @return rdflib.resource.Resource
  139. '''
  140. if not hasattr(self, '_rsrc'):
  141. self._rsrc = self.gs.rsrc
  142. return self._rsrc
  143. @property
  144. def is_stored(self):
  145. return self.gs.ask_rsrc_exists()
  146. @property
  147. def types(self):
  148. '''All RDF types.
  149. @return generator
  150. '''
  151. if not hasattr(self, '_types'):
  152. self._types = set(self.rsrc[RDF.type])
  153. return self._types
  154. @property
  155. def ldp_types(self):
  156. '''The LDP types.
  157. @return set(rdflib.term.URIRef)
  158. '''
  159. if not hasattr(self, '_ldp_types'):
  160. self._ldp_types = set()
  161. for t in self.types:
  162. if t.qname()[:4] == 'ldp:':
  163. self._ldp_types.add(t)
  164. return self._ldp_types
  165. @property
  166. def containment(self):
  167. if not hasattr(self, '_containment'):
  168. q = '''
  169. SELECT ?container ?contained {
  170. {
  171. ?s ldp:contains ?contained .
  172. } UNION {
  173. ?container ldp:contains ?s .
  174. }
  175. }
  176. '''
  177. qres = self.rsrc.graph.query(q, initBindings={'s' : self.urn})
  178. # There should only be one container.
  179. for t in qres:
  180. if t[0]:
  181. container = self.gs.ds.resource(t[0])
  182. contains = ( self.gs.ds.resource(t[1]) for t in qres if t[1] )
  183. self._containment = {
  184. 'container' : container, 'contains' : contains}
  185. return self._containment
  186. @containment.deleter
  187. def containment(self):
  188. '''
  189. Reset containment variable when changing containment triples.
  190. '''
  191. del self._containment
  192. @property
  193. def container(self):
  194. return self.containment['container']
  195. @property
  196. def contains(self):
  197. return self.containment['contains']
  198. ## STATIC & CLASS METHODS ##
  199. @classmethod
  200. def inst(cls, uuid):
  201. '''
  202. Fatory method that creates and returns an instance of an LDPR subclass
  203. based on information that needs to be queried from the underlying
  204. graph store.
  205. This is used with retrieval methods for resources that already exist.
  206. @param uuid UUID of the instance.
  207. '''
  208. gs = cls.load_gs_static(cls, uuid)
  209. rdf_types = gs.rsrc[nsc['res'][uuid] : RDF.type]
  210. for t in rdf_types:
  211. if t == cls.LDP_NR_TYPE:
  212. return LdpNr(uuid)
  213. if t == cls.LDP_RS_TYPE:
  214. return LdpRs(uuid)
  215. raise ValueError('Resource #{} does not exist or does not have a '
  216. 'valid LDP type.'.format(uuid))
  217. @classmethod
  218. def load_gs_static(cls, uuid=None):
  219. '''
  220. Dynamically load the store strategy indicated in the configuration.
  221. This essentially replicates the init() code in a static context.
  222. '''
  223. store_mod = import_module(
  224. 'lakesuperior.store_strategies.rdf.{}'.format(
  225. cls.store_strategy))
  226. rdf_store_cls = getattr(store_mod, Translator.camelcase(
  227. cls.store_strategy))
  228. return rdf_store_cls(uuid)
  229. @classmethod
  230. def inst_for_post(cls, parent_uuid=None, slug=None):
  231. '''
  232. Validate conditions to perform a POST and return an LDP resource
  233. instancefor using with the `post` method.
  234. This may raise an exception resulting in a 404 if the parent is not
  235. found or a 409 if the parent is not a valid container.
  236. '''
  237. # Shortcut!
  238. if not slug and not parent_uuid:
  239. return cls(str(uuid4()))
  240. gs = cls.load_gs_static()
  241. parent_rsrc = Resource(gs.ds, nsc['fcres'][parent_uuid])
  242. # Set prefix.
  243. if parent_uuid:
  244. parent_exists = gs.ask_rsrc_exists(parent_rsrc)
  245. if not parent_exists:
  246. raise ResourceNotExistsError('Parent not found: {}.'
  247. .format(parent_uuid))
  248. if nsc['ldp'].Container not in gs.rsrc.values(RDF.type):
  249. raise InvalidResourceError('Parent {} is not a container.'
  250. .format(parent_uuid))
  251. pfx = parent_uuid + '/'
  252. else:
  253. pfx = ''
  254. # Create candidate UUID and validate.
  255. if slug:
  256. cnd_uuid = pfx + slug
  257. cnd_rsrc = Resource(gs.ds, nsc['fcres'][cnd_uuid])
  258. if gs.ask_rsrc_exists(cnd_rsrc):
  259. return cls(pfx + str(uuid4()))
  260. else:
  261. return cls(cnd_uuid)
  262. else:
  263. return cls(pfx + str(uuid4()))
  264. ## LDP METHODS ##
  265. def head(self):
  266. '''
  267. Return values for the headers.
  268. '''
  269. headers = self.gs.headers
  270. for t in self.ldp_types:
  271. headers['Link'].append('{};rel="type"'.format(t.identifier.n3()))
  272. return headers
  273. def get(self, inbound=False):
  274. '''
  275. https://www.w3.org/TR/ldp/#ldpr-HTTP_GET
  276. '''
  277. try:
  278. g = self.gs.out_graph(inbound)
  279. except ResultException:
  280. # RDFlib bug? https://github.com/RDFLib/rdflib/issues/775
  281. raise ResourceNotExistsError()
  282. return Translator.globalize_rsrc(g)
  283. @transactional
  284. def post(self, data, format='text/turtle'):
  285. '''
  286. https://www.w3.org/TR/ldp/#ldpr-HTTP_POST
  287. Perform a POST action after a valid resource URI has been found.
  288. '''
  289. g = Graph()
  290. g.parse(data=data, format=format, publicID=self.urn)
  291. for t in self.base_types:
  292. g.add((self.urn, RDF.type, t))
  293. self.gs.create_rsrc(g)
  294. self._set_containment_rel()
  295. @transactional
  296. def put(self, data, format='text/turtle'):
  297. '''
  298. https://www.w3.org/TR/ldp/#ldpr-HTTP_PUT
  299. '''
  300. g = Graph()
  301. g.parse(data=data, format=format, publicID=self.urn)
  302. for t in self.base_types:
  303. g.add((self.urn, RDF.type, t))
  304. self.gs.create_or_replace_rsrc(g)
  305. self._set_containment_rel()
  306. @transactional
  307. @must_exist
  308. def delete(self):
  309. '''
  310. https://www.w3.org/TR/ldp/#ldpr-HTTP_DELETE
  311. '''
  312. self.gs.delete_rsrc(self.urn)
  313. ## PROTECTED METHODS ##
  314. def _set_containment_rel(self):
  315. '''Find the closest parent in the path indicated by the UUID and
  316. establish a containment triple.
  317. E.g.
  318. - If only urn:fcres:a (short: a) exists:
  319. - If a/b/c/d is being created, a becomes container of a/b/c/d. Also,
  320. pairtree nodes are created for a/b and a/b/c.
  321. - If e is being created, the root node becomes container of e.
  322. '''
  323. if '/' in self.uuid:
  324. # Traverse up the hierarchy to find the parent.
  325. #candidate_parent_urn = self._find_first_ancestor()
  326. #cparent = self.gs.ds.resource(candidate_parent_urn)
  327. cparent_uri = self._find_parent_or_create_pairtree(self.uuid)
  328. # Reroute possible containment relationships between parent and new
  329. # resource.
  330. #self._splice_in(cparent)
  331. if cparent_uri:
  332. self.gs.ds.add((cparent_uri, nsc['ldp'].contains,
  333. self.rsrc.identifier))
  334. else:
  335. self.rsrc.graph.add((nsc['fcsystem'].root, nsc['ldp'].contains,
  336. self.rsrc.identifier))
  337. # If a resource has no parent and should be parent of the new resource,
  338. # add the relationship.
  339. #for child_uri in self.find_lost_children():
  340. # self.rsrc.add(nsc['ldp'].contains, child_uri)
  341. def _find_parent_or_create_pairtree(self, uuid):
  342. '''
  343. Check the path-wise parent of the new resource. If it exists, return
  344. its URI. Otherwise, create pairtree resources up the path until an
  345. actual resource or the root node is found.
  346. @return rdflib.term.URIRef
  347. '''
  348. path_components = uuid.split('/')
  349. if len(path_components) < 2:
  350. return None
  351. # Build search list, e.g. for a/b/c/d/e would be a/b/c/d, a/b/c, a/b, a
  352. self._logger.info('Path components: {}'.format(path_components))
  353. fwd_search_order = accumulate(
  354. list(path_components)[:-1],
  355. func=lambda x,y : x + '/' + y
  356. )
  357. rev_search_order = reversed(list(fwd_search_order))
  358. cur_child_uri = nsc['fcres'][uuid]
  359. for cparent_uuid in rev_search_order:
  360. cparent_uri = nsc['fcres'][cparent_uuid]
  361. # @FIXME A bit ugly. Maybe we should use a Pairtree class.
  362. if self._rdf_store_cls(cparent_uri).ask_rsrc_exists():
  363. return cparent_uri
  364. else:
  365. self._create_pairtree(cparent_uri, cur_child_uri)
  366. cur_child_uri = cparent_uri
  367. return None
  368. #def _find_first_ancestor(self):
  369. # '''
  370. # Find by logic and triplestore queries the first existing resource by
  371. # traversing a path hierarchy upwards.
  372. # @return rdflib.term.URIRef
  373. # '''
  374. # path_components = self.uuid.split('/')
  375. # if len(path_components) < 2:
  376. # return None
  377. # # Build search list, e.g. for a/b/c/d/e would be a/b/c/d, a/b/c, a/b, a
  378. # search_order = accumulate(
  379. # reversed(search_order)[1:],
  380. # func=lambda x,y : x + '/' + y
  381. # )
  382. # for cmp in search_order:
  383. # if self.gs.ask_rsrc_exists(ns['fcres'].cmp):
  384. # return urn
  385. # else:
  386. # self._create_pairtree_node(cmp)
  387. # return None
  388. def _create_pairtree(self, uri, child_uri):
  389. '''
  390. Create a pairtree node with a containment statement.
  391. This is the default fcrepo4 behavior and probably not the best one, but
  392. we are following it here.
  393. If a resource such as `fcres:a/b/c` is created, and neither fcres:a or
  394. fcres:a/b exists, we have to create pairtree nodes in order to maintain
  395. the containment chain.
  396. This way, both fcres:a and fcres:a/b become thus containers of
  397. fcres:a/b/c, which may be confusing.
  398. '''
  399. g = Graph()
  400. g.add((uri, RDF.type, nsc['fedora'].Pairtree))
  401. g.add((uri, RDF.type, nsc['ldp'].Container))
  402. g.add((uri, RDF.type, nsc['ldp'].BasicContainer))
  403. g.add((uri, RDF.type, nsc['ldp'].RDFSource))
  404. g.add((uri, nsc['ldp'].contains, child_uri))
  405. if '/' not in str(uri):
  406. g.add((nsc['fcsystem'].root, nsc['ldp'].contains, uri))
  407. self.gs.create_rsrc(g)
  408. #def _splice_in(self, parent):
  409. # '''
  410. # Insert the new resource between a container and its child.
  411. # If a resource is inserted between two resources that already have a
  412. # containment relationship, e.g. inserting `<a/b>` where
  413. # `<a> ldp:contains <a/b/c>` exists, the existing containment
  414. # relationship must be broken in order to insert the resource in between.
  415. # NOTE: This method only removes the containment relationship between the
  416. # old parent (`<a>` in the example above) and old child (`<a/b/c>`) and
  417. # sets a new one between the new parent and child (`<a/b>` and
  418. # `<a/b/c>`). The relationship between `<a>` and `<a/b>` is set
  419. # separately.
  420. # @param rdflib.resource.Resource parent The parent resource. This
  421. # includes the root node.
  422. # '''
  423. # # For some reason, initBindings (which adds a VALUES statement in the
  424. # # query) does not work **just for `?new`**. `BIND` is necessary along
  425. # # with a format() function.
  426. # q = '''
  427. # SELECT ?child {{
  428. # ?p ldp:contains ?child .
  429. # FILTER ( ?child != <{}> ) .
  430. # FILTER STRSTARTS(str(?child), "{}") .
  431. # }}
  432. # LIMIT 1
  433. # '''.format(self.urn)
  434. # qres = self.rsrc.graph.query(q, initBindings={'p' : parent.identifier})
  435. # if not qres:
  436. # return
  437. # child_urn = qres.next()[0]
  438. # parent.remove(nsc['ldp'].contains, child_urn)
  439. # self.src.add(nsc['ldp'].contains, child_urn)
  440. #def find_lost_children(self):
  441. # '''
  442. # If the parent was created after its children and has to find them!
  443. # '''
  444. # q = '''
  445. # SELECT ?child {
  446. class LdpRs(Ldpr):
  447. '''LDP-RS (LDP RDF source).
  448. Definition: https://www.w3.org/TR/ldp/#ldprs
  449. '''
  450. base_types = {
  451. nsc['ldp'].RDFSource
  452. }
  453. @transactional
  454. @must_exist
  455. def patch(self, data):
  456. '''
  457. https://www.w3.org/TR/ldp/#ldpr-HTTP_PATCH
  458. '''
  459. ts = Literal(arrow.utcnow(), datatype=XSD.dateTime)
  460. self.gs.patch_rsrc(self.urn, data, ts)
  461. self.gs.ds.add((self.urn, nsc['fedora'].lastUpdated, ts))
  462. self.gs.ds.add((self.urn, nsc['fedora'].lastUpdatedBy,
  463. Literal('BypassAdmin')))
  464. class LdpNr(LdpRs):
  465. '''LDP-NR (Non-RDF Source).
  466. Definition: https://www.w3.org/TR/ldp/#ldpnr
  467. '''
  468. pass
  469. class Ldpc(LdpRs):
  470. '''LDPC (LDP Container).'''
  471. def __init__(self, uuid):
  472. super().__init__(uuid)
  473. self.base_types.update({
  474. nsc['ldp'].Container,
  475. })
  476. class LdpBc(Ldpc):
  477. '''LDP-BC (LDP Basic Container).'''
  478. def __init__(self, uuid):
  479. super().__init__(uuid)
  480. self.base_types.update({
  481. nsc['ldp'].BasicContainer,
  482. })
  483. class LdpDc(Ldpc):
  484. '''LDP-DC (LDP Direct Container).'''
  485. def __init__(self, uuid):
  486. super().__init__(uuid)
  487. self.base_types.update({
  488. nsc['ldp'].DirectContainer,
  489. })
  490. class LdpIc(Ldpc):
  491. '''LDP-IC (LDP Indirect Container).'''
  492. def __init__(self, uuid):
  493. super().__init__(uuid)
  494. self.base_types.update({
  495. nsc['ldp'].IndirectContainer,
  496. })