from rdflib import BNode, Graph, RDF, URIRef
from oldman.exception import OMDifferentHashlessIRIError, OMForbiddenSkolemizedIRIError, OMClassInstanceError
from oldman.resource import Resource, is_blank_node
_JSON_TYPES = ["application/json", "json"]
_JSON_LD_TYPES = ["application/ld+json", "json-ld"]
[docs]class CRUDController(object):
"""A :class:`~oldman.rest.crud.CRUDController` object helps you to manipulate
your :class:`~oldman.resource.Resource` objects in a RESTful-like manner.
Please note that REST/HTTP only manipulates hash-less IRIs.
A hash IRI is the combination of a hash-less IRI (fragment-less IRI) and a fragment.
Multiple hashed IRIs may have the same hash-less IRI and only differ by
their fragment values.
This is a concern for each type of HTTP operation.
This class is generic and does not support the Collection pattern
(there is no append method).
:param manager: :class:`~oldman.management.manager.ResourceManager` object.
Possible improvements:
- Add a PATCH method.
"""
def __init__(self, manager):
self._manager = manager
[docs] def get(self, hashless_iri, content_type="text/turtle"):
"""Gets the main :class:`~oldman.resource.Resource` object having its hash-less IRI.
When multiple :class:`~oldman.resource.Resource` objects have this hash-less IRI,
one of them has to be selected.
If one has no fragment value, it is selected.
Otherwise, this selection is currently arbitrary.
TODO: stop selecting the resources and returns the list.
Raises an :class:`~oldman.exception.ObjectNotFoundError` exception if no resource is found.
:param hashless_iri: hash-less of the resource.
:param content_type: Content type of its representation.
:return: The selected :class:`~oldman.resource.Resource` object.
"""
resource = self._manager.get(hashless_iri=hashless_iri)
if content_type in _JSON_TYPES:
return resource.to_json()
elif content_type in _JSON_LD_TYPES:
return resource.to_jsonld()
# Try as a RDF mime-type (may not be supported)
else:
return resource.to_rdf(content_type)
[docs] def delete(self, hashless_iri):
"""Deletes every :class:`~oldman.resource.Resource` object having this hash-less IRI.
:param hashless_iri: Hash-less IRI.
"""
for resource in self._manager.filter(hashless_iri=hashless_iri):
if resource is not None:
resource.delete()
[docs] def update(self, hashless_iri, document_content, content_type, allow_new_type=False, allow_type_removal=False):
"""Updates every :class:`~oldman.resource.Resource` object having this hash-less IRI.
Raises an :class:`~oldman.exception.OMDifferentBaseIRIError` exception
if tries to create of modify non-blank :class:`~oldman.resource.Resource` objects
that have a different hash-less IRI.
This restriction is motivated by security concerns.
Accepts JSON, JSON-LD and RDF formats supported by RDFlib.
:param hashless_iri: Document IRI.
:param document_content: Payload.
:param content_type: Content type of the payload.
:param allow_new_type: If `True`, new types can be added. Defaults to `False`. See
:func:`oldman.resource.Resource.full_update` for explanations about the
security concerns.
:param allow_type_removal: If `True`, new types can be removed. Same security concerns than above.
Defaults to `False`.
"""
graph = Graph()
#TODO: manage parsing exceptions
if content_type in _JSON_TYPES:
resource = self._manager.get(hashless_iri=hashless_iri)
graph.parse(data=document_content, format="json-ld", publicID=hashless_iri,
context=resource.context)
#RDF graph
else:
graph.parse(data=document_content, format=content_type, publicID=hashless_iri)
self._update_graph(hashless_iri, graph, allow_new_type, allow_type_removal)
def _update_graph(self, hashless_iri, graph, allow_new_type, allow_type_removal):
subjects = set(graph.subjects())
# Non-skolemized blank nodes
bnode_subjects = filter(lambda x: isinstance(x, BNode), subjects)
other_subjects = subjects.difference(bnode_subjects)
#Blank nodes (may obtain a regular IRI)
resources = self._create_blank_nodes(hashless_iri, graph, bnode_subjects, allow_new_type, allow_type_removal)
#Objects with an existing IRI
resources += self._create_regular_resources(hashless_iri, graph, other_subjects, allow_new_type,
allow_type_removal)
#Check validity before saving
#May raise a LDEditError
for r in resources:
r.check_validity()
#TODO: improve it as a transaction (really necessary?)
for r in resources:
r.save()
#Delete omitted resources
all_resource_iris = {r.id for r in self._manager.filter(hashless_iri=hashless_iri)}
resource_iris_to_remove = all_resource_iris.difference({r.id for r in resources})
for iri in resource_iris_to_remove:
# Cheap because already in the resource cache
r = self._manager.get(id=iri)
if r is not None:
r.delete()
def _create_blank_nodes(self, hashless_iri, graph, bnode_subjects, allow_new_type, allow_type_removal):
resources = []
# Only former b-nodes
dependent_resources = []
for bnode in bnode_subjects:
types = {unicode(t) for t in graph.objects(bnode, RDF.type)}
resource = self._manager.new(hashless_iri=hashless_iri, types=types)
_alter_bnode_triples(graph, bnode, URIRef(resource.id))
resource.full_update_from_graph(graph, save=False, allow_new_type=allow_new_type,
allow_type_removal=allow_type_removal)
resources.append(resource)
deps = {o for _, p, o in graph.triples((bnode, None, None))
if isinstance(o, BNode)}
if len(deps) > 0:
dependent_resources.append(resource)
if (not resource.is_blank_node()) and resource.hashless_iri != hashless_iri:
raise OMDifferentHashlessIRIError(u"%s is not the hash-less IRI of %s" % (hashless_iri, resource.id))
#When some Bnodes are interconnected
for resource in dependent_resources:
# Update again
resource.full_update_from_graph(graph, save=False)
return resources
def _create_regular_resources(self, hashless_iri, graph, other_subjects, allow_new_type, allow_type_removal):
resources = []
for iri in [unicode(s) for s in other_subjects]:
if is_blank_node(iri):
raise OMForbiddenSkolemizedIRIError(u"Skolemized IRI like %s are not allowed when updating a resource."
% iri)
elif iri.split("#")[0] != hashless_iri:
raise OMDifferentHashlessIRIError(u"%s is not the hash-less IRI of %s" % (hashless_iri, iri))
try:
resource = self._manager.get(id=iri)
resource.full_update_from_graph(graph, save=False, allow_new_type=allow_new_type,
allow_type_removal=allow_type_removal)
except OMClassInstanceError:
# New object
resource = Resource.load_from_graph(self._manager, iri, graph, is_new=True)
resources.append(resource)
return resources
def _alter_bnode_triples(graph, bnode, new_iri_ref):
subject_triples = list(graph.triples((bnode, None, None)))
for _, p, o in subject_triples:
graph.remove((bnode, p, o))
graph.add((new_iri_ref, p, o))
object_triples = list(graph.triples((None, None, bnode)))
for s, p, _ in object_triples:
graph.remove((s, p, bnode))
graph.add((s, p, new_iri_ref))