Browse Source

GET version and version metadata; shared method for content negotiation.

Stefano Cossu 7 years ago
parent
commit
3ece9aae6d

+ 34 - 25
doc/examples/store_layouts/graph_per_aspect.trig

@@ -8,6 +8,7 @@ PREFIX fcg: <info:fcsystem/graph/>
 PREFIX foaf: <http://xmlns.com/foaf/0.1/>
 PREFIX foaf: <http://xmlns.com/foaf/0.1/>
 PREFIX ldp: <http://www.w3.org/ns/ldp#>
 PREFIX ldp: <http://www.w3.org/ns/ldp#>
 PREFIX ns: <http://example.edu/lakesuperior/ns#>
 PREFIX ns: <http://example.edu/lakesuperior/ns#>
+PREFIX premis: <http://www.loc.gov/premis/rdf/v1#>
 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
 PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
 
 
 # Admin data graphs.
 # Admin data graphs.
@@ -30,26 +31,26 @@ PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
     fcrepo:created "2017-11-23"^^xsd:date ;
     fcrepo:created "2017-11-23"^^xsd:date ;
     fcrepo:lastModified "2017-11-27"^^xsd:date ;
     fcrepo:lastModified "2017-11-27"^^xsd:date ;
     fcrepo:hasVersion
     fcrepo:hasVersion
-      <info:fcres/a/b/c;v1> , <info:fcres/a/b/c;v2> , <info:fcres/a/b/c;v3> ;
+      <info:fcres/a/b/c/fcr:versions/v1> , <info:fcres/a/b/c;v2> , <info:fcres/a/b/c;v3> ;
     .
     .
 }
 }
 
 
-<info:fcsystem/graph/admin/a/b/c;v1> {
-  <info:fcres/a/b/c;v1> a fcrepo:Version ;
+<info:fcsystem/graph/admin/a/b/c/fcr:versions/v1> {
+  <info:fcres/a/b/c/fcr:versions/v1> a fcrepo:Version ;
     fcrepo:created "2017-11-23"^^xsd:date ;
     fcrepo:created "2017-11-23"^^xsd:date ;
     fcrepo:lastModified "2017-11-23"^^xsd:date ;
     fcrepo:lastModified "2017-11-23"^^xsd:date ;
   .
   .
 }
 }
 
 
-<info:fcsystem/graph/admin/a/b/c;v2> {
-  <info:fcres/a/b/c;v2> a fcrepo:Version ;
+<info:fcsystem/graph/admin/a/b/c/fcr:versions/v2> {
+  <info:fcres/a/b/c/fcr:versions/v2> a fcrepo:Version ;
     fcrepo:created "2017-11-23"^^xsd:date ;
     fcrepo:created "2017-11-23"^^xsd:date ;
     fcrepo:lastModified "2017-11-24"^^xsd:date ;
     fcrepo:lastModified "2017-11-24"^^xsd:date ;
   .
   .
 }
 }
 
 
-<info:fcsystem/graph/admin/a/b/c;v3> {
-  <info:fcres/a/b/c;v3> a fcrepo:Version ;
+<info:fcsystem/graph/admin/a/b/c/fcr:versions/v3> {
+  <info:fcres/a/b/c/fcr:versions/v3> a fcrepo:Version ;
     fcrepo:created "2017-11-23"^^xsd:date ;
     fcrepo:created "2017-11-23"^^xsd:date ;
     fcrepo:lastModified "2017-11-25"^^xsd:date ;
     fcrepo:lastModified "2017-11-25"^^xsd:date ;
   .
   .
@@ -107,23 +108,23 @@ PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
 }
 }
 
 
 # Previous states (versions) of a resource.
 # Previous states (versions) of a resource.
-<info:fcsystem/graph/userdata/_main/a/b/c;v1> {
-  <info:fcres/a/b/c;v1> a ns:Book ;
+<info:fcsystem/graph/userdata/_main/a/b/c/fcr:versions/v1> {
+  <info:fcres/a/b/c/fcr:versions/v1> a ns:Book ;
     fcrepo:hasParent <info:fcres/> ;
     fcrepo:hasParent <info:fcres/> ;
     dc:title "Moby Dick" ;
     dc:title "Moby Dick" ;
     .
     .
 }
 }
 
 
-<info:fcsystem/graph/userdata/_main/a/b/c;v2> {
-  <info:fcres/a/b/c;v2> a ns:Book ;
+<info:fcsystem/graph/userdata/_main/a/b/c/fcr:versions/v2> {
+  <info:fcres/a/b/c/fcr:versions/v2> a ns:Book ;
     fcrepo:hasParent <info:fcres/> ;
     fcrepo:hasParent <info:fcres/> ;
     dc:title "Moby Dick" ;
     dc:title "Moby Dick" ;
     dc:creator "Herman Melvil" ;
     dc:creator "Herman Melvil" ;
     .
     .
 }
 }
 
 
-<info:fcsystem/graph/userdata/_main/a/b/c;v3> {
-  <info:fcres/a/b/c;v3> a ns:Book ;
+<info:fcsystem/graph/userdata/_main/a/b/c/fcr:versions/v3> {
+  <info:fcres/a/b/c/fcr:versions/v3> a ns:Book ;
     fcrepo:hasParent <info:fcres/> ;
     fcrepo:hasParent <info:fcres/> ;
     dc:title "Moby Dick" ;
     dc:title "Moby Dick" ;
     dc:creator "Herman Melville" ;
     dc:creator "Herman Melville" ;
@@ -147,29 +148,37 @@ PREFIX xsd: <http://www.w3.org/2001/XMLSchema#>
 # Historic version metadata. This is kept separate to optimize current resource
 # Historic version metadata. This is kept separate to optimize current resource
 # lookups.
 # lookups.
 <info:fcsystem/graph/historic>  {
 <info:fcsystem/graph/historic>  {
-  <info:fcsystem/graph/admin/a/b/c;v1>
-    foaf:primaryTopic <info:fcres/a/b/c;v1> ;
+  <info:fcsystem/graph/admin/a/b/c/fcr:versions/v1>
+    foaf:primaryTopic <info:fcres/a/b/c/fcr:versions/v1> ;
     fcrepo:created "2017-11-24"^^xsd:date ;
     fcrepo:created "2017-11-24"^^xsd:date ;
+    fcrepo:hasVersionLabel "v1" ;
   .
   .
-  <info:fcsystem/graph/admin/a/b/c;v2>
-    foaf:primaryTopic <info:fcres/a/b/c;v2> ;
+  <info:fcsystem/graph/admin/a/b/c/fcr:versions/v2>
+    foaf:primaryTopic <info:fcres/a/b/c/fcr:versions/v2> ;
     fcrepo:created "2017-11-25"^^xsd:date ;
     fcrepo:created "2017-11-25"^^xsd:date ;
+    fcrepo:hasVersionLabel "v2" ;
   .
   .
-  <info:fcsystem/graph/admin/a/b/c;v3>
-    foaf:primaryTopic <info:fcres/a/b/c;v3> ;
+  <info:fcsystem/graph/admin/a/b/c/fcr:versions/v3>
+    foaf:primaryTopic <info:fcres/a/b/c/fcr:versions/v3> ;
     fcrepo:created "2017-11-26"^^xsd:date ;
     fcrepo:created "2017-11-26"^^xsd:date ;
+    fcrepo:hasVersionLabel "v3" ;
   .
   .
 
 
-  <info:fcsystem/graph/userdata/_main/a/b/c;v1>
-    foaf:primaryTopic <info:fcres/a/b/c;v1> ;
+  <info:fcsystem/graph/userdata/_main/a/b/c/fcr:versions/v1>
+    foaf:primaryTopic <info:fcres/a/b/c/fcr:versions/v1> ;
     fcrepo:created "2017-11-24"^^xsd:date ;
     fcrepo:created "2017-11-24"^^xsd:date ;
+    fcrepo:hasVersionLabel "v1" ;
+    # Provenance data can also be added.
+    premis:actor <http://ex.org/user/1325> ;
   .
   .
-  <info:fcsystem/graph/userdata/_main/a/b/c;v2>
-    foaf:primaryTopic <info:fcres/a/b/c;v2> ;
+  <info:fcsystem/graph/userdata/_main/a/b/c/fcr:versions/v2>
+    foaf:primaryTopic <info:fcres/a/b/c/fcr:versions/v2> ;
     fcrepo:created "2017-11-25"^^xsd:date ;
     fcrepo:created "2017-11-25"^^xsd:date ;
+    fcrepo:hasVersionLabel "v2" ;
   .
   .
-  <info:fcsystem/graph/userdata/_main/a/b/c;v3>
-    foaf:primaryTopic <info:fcres/a/b/c;v3> ;
+  <info:fcsystem/graph/userdata/_main/a/b/c/fcr:versions/v3>
+    foaf:primaryTopic <info:fcres/a/b/c/fcr:versions/v3> ;
     fcrepo:created "2017-11-26"^^xsd:date ;
     fcrepo:created "2017-11-26"^^xsd:date ;
+    fcrepo:hasVersionLabel "v3" ;
   .
   .
 }
 }

+ 19 - 12
lakesuperior/endpoints/ldp.py

@@ -131,16 +131,8 @@ def get_resource(uid, force_rdf=False):
         if isinstance(rsrc, LdpRs) \
         if isinstance(rsrc, LdpRs) \
                 or is_accept_hdr_rdf_parsable() \
                 or is_accept_hdr_rdf_parsable() \
                 or force_rdf:
                 or force_rdf:
-            resp = rsrc.get()
-            if request.accept_mimetypes.best == 'text/html':
-                rsrc = resp.resource(request.path)
-                return render_template(
-                        'resource.html', rsrc=rsrc, nsm=nsm,
-                        blacklist = vw_blacklist)
-            else:
-                for p in vw_blacklist:
-                    resp.remove((None, p, None))
-                return (resp.serialize(format='turtle'), out_headers)
+            rsp = rsrc.get()
+            return negotiate_content(rsp, out_headers)
         else:
         else:
             logger.info('Streaming out binary content.')
             logger.info('Streaming out binary content.')
             rsp = make_response(send_file(rsrc.local_path, as_attachment=True,
             rsp = make_response(send_file(rsrc.local_path, as_attachment=True,
@@ -213,7 +205,7 @@ def get_version_info(uid):
     except TombstoneError as e:
     except TombstoneError as e:
         return _tombstone_response(e, uid)
         return _tombstone_response(e, uid)
     else:
     else:
-        return rsp.serialize(format='turtle'), 200
+        return negotiate_content(rsp)
 
 
 
 
 @ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['GET'])
 @ldp.route('/<path:uid>/fcr:versions/<ver_uid>', methods=['GET'])
@@ -233,7 +225,7 @@ def get_version(uid, ver_uid):
     except TombstoneError as e:
     except TombstoneError as e:
         return _tombstone_response(e, uid)
         return _tombstone_response(e, uid)
     else:
     else:
-        return rsp.serialize(format='turtle'), 200
+        return negotiate_content(rsp)
 
 
 
 
 @ldp.route('/<path:uid>/fcr:versions', methods=['POST'])
 @ldp.route('/<path:uid>/fcr:versions', methods=['POST'])
@@ -427,6 +419,21 @@ def tombstone(uid):
         return '', 404
         return '', 404
 
 
 
 
+def negotiate_content(rsp, headers=None):
+    '''
+    Return HTML or serialized RDF depending on accept headers.
+    '''
+    if request.accept_mimetypes.best == 'text/html':
+        rsrc = rsp.resource(request.path)
+        return render_template(
+                'resource.html', rsrc=rsrc, nsm=nsm,
+                blacklist = vw_blacklist)
+    else:
+        for p in vw_blacklist:
+            rsp.remove((None, p, None))
+        return (rsp.serialize(format='turtle'), headers)
+
+
 def uuid_for_post(parent_uid=None, slug=None):
 def uuid_for_post(parent_uid=None, slug=None):
     '''
     '''
     Validate conditions to perform a POST and return an LDP resource
     Validate conditions to perform a POST and return an LDP resource

+ 38 - 44
lakesuperior/model/ldpr.py

@@ -20,6 +20,8 @@ from lakesuperior.dictionaries.srv_mgd_terms import  srv_mgd_subjects, \
         srv_mgd_predicates, srv_mgd_types
         srv_mgd_predicates, srv_mgd_types
 from lakesuperior.exceptions import *
 from lakesuperior.exceptions import *
 from lakesuperior.model.ldp_factory import LdpFactory
 from lakesuperior.model.ldp_factory import LdpFactory
+from lakesuperior.store_layouts.ldp_rs.rsrc_centric_layout import (
+        VERS_CONT_LABEL)
 
 
 
 
 ROOT_UID = ''
 ROOT_UID = ''
@@ -104,8 +106,6 @@ class Ldpr(metaclass=ABCMeta):
     RES_DELETED = '_delete_'
     RES_DELETED = '_delete_'
     RES_UPDATED = '_update_'
     RES_UPDATED = '_update_'
 
 
-    RES_VER_CONT_LABEL = 'fcr:versions'
-
     base_types = {
     base_types = {
         nsc['fcrepo'].Resource,
         nsc['fcrepo'].Resource,
         nsc['ldp'].Resource,
         nsc['ldp'].Resource,
@@ -294,7 +294,7 @@ class Ldpr(metaclass=ABCMeta):
         '''
         '''
         if not hasattr(self, '_version_info'):
         if not hasattr(self, '_version_info'):
             try:
             try:
-                self._version_info = self.rdfly.get_version_info(self.urn)
+                self._version_info = self.rdfly.get_version_info(self.uid)
             except ResourceNotExistsError as e:
             except ResourceNotExistsError as e:
                 self._version_info = Graph()
                 self._version_info = Graph()
 
 
@@ -395,10 +395,32 @@ class Ldpr(metaclass=ABCMeta):
         This gets the RDF metadata. The binary retrieval is handled directly
         This gets the RDF metadata. The binary retrieval is handled directly
         by the route.
         by the route.
         '''
         '''
-        global_gr = g.tbox.globalize_graph(self.out_graph)
-        global_gr.namespace_manager = nsm
+        gr = g.tbox.globalize_graph(self.out_graph)
+        gr.namespace_manager = nsm
+
+        return gr
+
+
+    def get_version_info(self):
+        '''
+        Get the `fcr:versions` graph.
+        '''
+        gr = g.tbox.globalize_graph(self.version_info)
+        gr.namespace_manager = nsm
 
 
-        return global_gr
+        return gr
+
+
+    def get_version(self, ver_uid):
+        '''
+        Get a version by label.
+        '''
+        ver_gr = self.rdfly.get_metadata(self.uid, ver_uid).graph
+
+        gr = g.tbox.globalize_graph(ver_gr)
+        gr.namespace_manager = nsm
+
+        return gr
 
 
 
 
     @atomic
     @atomic
@@ -475,7 +497,7 @@ class Ldpr(metaclass=ABCMeta):
         LIMIT 1
         LIMIT 1
         ''')
         ''')
         ver_uid = str(ver_rsp.bindings[0]['uid'])
         ver_uid = str(ver_rsp.bindings[0]['uid'])
-        ver_trp = set(self.rdfly.get_version(self.urn, ver_uid))
+        ver_trp = set(self.rdfly.get_metadata(self.urn, ver_uid))
 
 
         laz_gr = Graph()
         laz_gr = Graph()
         for t in ver_trp:
         for t in ver_trp:
@@ -509,22 +531,6 @@ class Ldpr(metaclass=ABCMeta):
         return self._purge_rsrc(inbound)
         return self._purge_rsrc(inbound)
 
 
 
 
-    def get_version_info(self):
-        '''
-        Get the `fcr:versions` graph.
-        '''
-        return g.tbox.globalize_graph(self.version_info)
-
-
-    def get_version(self, ver_uid):
-        '''
-        Get a version by label.
-        '''
-        ver_gr = self.rdfly.get_version(self.urn, ver_uid)
-
-        return g.tbox.globalize_graph(ver_gr)
-
-
     @atomic
     @atomic
     def create_version(self, ver_uid):
     def create_version(self, ver_uid):
         '''
         '''
@@ -558,7 +564,7 @@ class Ldpr(metaclass=ABCMeta):
         if backup:
         if backup:
             self.create_version(uuid4())
             self.create_version(uuid4())
 
 
-        ver_gr = self.rdfly.get_version(self.urn, ver_uid)
+        ver_gr = self.rdfly.get_metadata(self.urn, ver_uid)
         revert_gr = Graph()
         revert_gr = Graph()
         for t in ver_gr:
         for t in ver_gr:
             if t[1] not in srv_mgd_predicates and not(
             if t[1] not in srv_mgd_predicates and not(
@@ -682,8 +688,8 @@ class Ldpr(metaclass=ABCMeta):
         Perform version creation and return the internal URN.
         Perform version creation and return the internal URN.
         '''
         '''
         # Create version resource from copying the current state.
         # Create version resource from copying the current state.
-        ver_add_gr = Graph()
-        vers_uid = '{}/{}'.format(self.uid, self.RES_VER_CONT_LABEL)
+        ver_add_gr = set()
+        vers_uid = '{}/{}'.format(self.uid, VERS_CONT_LABEL)
         ver_uid = '{}/{}'.format(vers_uid, ver_uid)
         ver_uid = '{}/{}'.format(vers_uid, ver_uid)
         ver_uri = nsc['fcres'][ver_uid]
         ver_uri = nsc['fcres'][ver_uid]
         ver_add_gr.add((ver_uri, RDF.type, nsc['fcrepo'].Version))
         ver_add_gr.add((ver_uri, RDF.type, nsc['fcrepo'].Version))
@@ -707,25 +713,13 @@ class Ldpr(metaclass=ABCMeta):
                         g.tbox.replace_term_domain(t[0], self.urn, ver_uri),
                         g.tbox.replace_term_domain(t[0], self.urn, ver_uri),
                         t[1], t[2]))
                         t[1], t[2]))
 
 
-        self.rdfly.modify_rsrc(
-                self.uid, add_trp=ver_add_gr, types={nsc['fcrepo'].Version})
-
-        # Add version metadata.
-        add_gr = set()
-        add_gr.add((
-        elf.urn, nsc['fcrepo'].hasVersion, ver_uri))
-        add_gr.add(
-           (ver_uri, nsc['fcrepo'].created, g.timestamp_term))
-        add_gr.add(
-                (ver_uri, nsc['fcrepo'].hasVersionLabel, Literal(ver_uid)))
-
-        self.rdfly.modify_rsrc(self.uid, add_trp=add_gr)
-
-        # Update resource.
-        rsrc_add_gr = Graph()
-        rsrc_add_gr.add((
-            self.urn, nsc['fcrepo'].hasVersions, nsc['fcres'][vers_uid]))
+        self.rdfly.modify_rsrc(ver_uid, add_trp=ver_add_gr)
 
 
+        # Update resource admin data.
+        rsrc_add_gr = {
+            (self.urn, nsc['fcrepo'].hasVersion, ver_uri),
+            (self.urn, nsc['fcrepo'].hasVersions, nsc['fcres'][vers_uid]),
+        }
         self._modify_rsrc(self.RES_UPDATED, add_trp=rsrc_add_gr, notify=False)
         self._modify_rsrc(self.RES_UPDATED, add_trp=rsrc_add_gr, notify=False)
 
 
         return nsc['fcres'][ver_uid]
         return nsc['fcres'][ver_uid]

+ 64 - 66
lakesuperior/store_layouts/ldp_rs/rsrc_centric_layout.py

@@ -6,12 +6,12 @@ from urllib.parse import quote
 
 
 import requests
 import requests
 
 
-from flask import current_app
+from flask import g
 from rdflib import Graph
 from rdflib import Graph
 from rdflib.namespace import RDF
 from rdflib.namespace import RDF
 from rdflib.query import ResultException
 from rdflib.query import ResultException
 from rdflib.resource import Resource
 from rdflib.resource import Resource
-from rdflib.term import URIRef
+from rdflib.term import URIRef, Literal
 
 
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_collection as nsc
 from lakesuperior.dictionaries.namespaces import ns_mgr as nsm
 from lakesuperior.dictionaries.namespaces import ns_mgr as nsm
@@ -22,6 +22,7 @@ from lakesuperior.exceptions import (InvalidResourceError, InvalidTripleError,
 
 
 META_GR_URI = nsc['fcsystem']['meta']
 META_GR_URI = nsc['fcsystem']['meta']
 HIST_GR_URI = nsc['fcsystem']['historic']
 HIST_GR_URI = nsc['fcsystem']['historic']
+VERS_CONT_LABEL = 'fcr:versions'
 
 
 
 
 class RsrcCentricLayout:
 class RsrcCentricLayout:
@@ -56,6 +57,7 @@ class RsrcCentricLayout:
     '''
     '''
 
 
     _logger = logging.getLogger(__name__)
     _logger = logging.getLogger(__name__)
+    _graph_uids = ('fcadmin', 'fcmain', 'fcstruct')
 
 
     # @TODO Move to a config file?
     # @TODO Move to a config file?
     attr_map = {
     attr_map = {
@@ -66,6 +68,7 @@ class RsrcCentricLayout:
                 nsc['fcrepo'].created,
                 nsc['fcrepo'].created,
                 nsc['fcrepo'].createdBy,
                 nsc['fcrepo'].createdBy,
                 nsc['fcrepo'].hasParent,
                 nsc['fcrepo'].hasParent,
+                nsc['fcrepo'].hasVersion,
                 nsc['fcrepo'].lastModified,
                 nsc['fcrepo'].lastModified,
                 nsc['fcrepo'].lastModifiedBy,
                 nsc['fcrepo'].lastModifiedBy,
                 # The following 3 are set by the user but still in this group
                 # The following 3 are set by the user but still in this group
@@ -168,7 +171,7 @@ class RsrcCentricLayout:
         See base_rdf_layout.extract_imr.
         See base_rdf_layout.extract_imr.
         '''
         '''
         if incl_children:
         if incl_children:
-            incl_child_qry = 'FROM {}'.format(self._struct_uri(uid).n3())
+            incl_child_qry = 'FROM {}'.format(nsc['fcstruct'][uid].n3())
             if embed_children:
             if embed_children:
                 pass # Not implemented. May never be.
                 pass # Not implemented. May never be.
         else:
         else:
@@ -181,8 +184,8 @@ class RsrcCentricLayout:
         {chld}
         {chld}
         WHERE {{ ?s ?p ?o . }}
         WHERE {{ ?s ?p ?o . }}
         '''.format(
         '''.format(
-                ag=self._admin_uri(uid).n3(),
-                sg=self._main_uri(uid).n3(),
+                ag=nsc['fcadmin'][uid].n3(),
+                sg=nsc['fcmain'][uid].n3(),
                 chld=incl_child_qry,
                 chld=incl_child_qry,
             )
             )
         try:
         try:
@@ -228,7 +231,7 @@ class RsrcCentricLayout:
         '''
         '''
         See base_rdf_layout.ask_rsrc_exists.
         See base_rdf_layout.ask_rsrc_exists.
         '''
         '''
-        meta_gr = self.ds.graph(self._admin_uri(uid))
+        meta_gr = self.ds.graph(nsc['fcadmin'][uid])
         return bool(
         return bool(
                 meta_gr[nsc['fcres'][uid] : RDF.type : nsc['fcrepo'].Resource])
                 meta_gr[nsc['fcres'][uid] : RDF.type : nsc['fcrepo'].Resource])
 
 
@@ -238,11 +241,36 @@ class RsrcCentricLayout:
         This is an optimized query to get everything the application needs to
         This is an optimized query to get everything the application needs to
         insert new contents, and nothing more.
         insert new contents, and nothing more.
         '''
         '''
-        gr = self.ds.graph(self._admin_uri(uid, ver_uid)) | Graph()
+        if ver_uid:
+            uid = self.snapshot_uid(uid, ver_uid)
+        gr = self.ds.graph(nsc['fcadmin'][uid]) | Graph()
 
 
         return Resource(gr, nsc['fcres'][uid])
         return Resource(gr, nsc['fcres'][uid])
 
 
 
 
+    def get_version_info(self, uid):
+        '''
+        Get all metadata about a resource's versions.
+        '''
+        # @NOTE This pretty much bends the ontology—it replaces the graph URI
+        # with the subject URI. But the concepts of data and metadata in Fedora
+        # are quite fluid anyways...
+        qry = '''
+        CONSTRUCT {?v ?p ?o .} {
+          GRAPH ?ag {
+            ?s fcrepo:hasVersion ?v .
+          }
+          GRAPH fcsystem:historic {
+            ?vm foaf:primaryTopic ?v .
+            ?vm  ?p ?o .
+            FILTER (?o != ?v)
+          }
+        }'''
+
+        return self.ds.query(qry, initBindings={'ag': nsc['fcadmin'][uid],
+            's': nsc['fcres'][uid]}).graph
+
+
     def get_inbound_rel(self, uri):
     def get_inbound_rel(self, uri):
         '''
         '''
         Query inbound relationships for a subject.
         Query inbound relationships for a subject.
@@ -270,27 +298,6 @@ class RsrcCentricLayout:
             return qres.graph
             return qres.graph
 
 
 
 
-    def create_snapshot(self, uid, ver_uid):
-        '''
-        Create a version snapshot.
-        '''
-        state_gr = self.ds.graph(self._main_uri(uid))
-        state_ver_gr = self.ds.graph(self._main_uri(uid, ver_uid))
-        meta_gr = self.ds.graph(self._admin_uri(uid))
-        meta_ver_gr = self.ds.graph(self._admin_uri(uid, ver_uid))
-
-
-
-
-    def get_version(self, uid, ver_uid):
-        '''
-        See base_rdf_layout.get_version.
-        '''
-        # @TODO
-        gr = self.ds.graph(self._main_uri(uid, ver_uid))
-        return Resource(gr | Graph(), nsc['fcres'][uid])
-
-
     def purge_rsrc(self, uid, inbound=True, backup_uid=None):
     def purge_rsrc(self, uid, inbound=True, backup_uid=None):
         '''
         '''
         Completely delete a resource and (optionally) its references.
         Completely delete a resource and (optionally) its references.
@@ -331,7 +338,6 @@ class RsrcCentricLayout:
             mg=META_GR_URI.n3(),
             mg=META_GR_URI.n3(),
             hg=HIST_GR_URI.n3())
             hg=HIST_GR_URI.n3())
 
 
-        import pdb; pdb.set_trace()
         if inbound:
         if inbound:
             # Gather ALL subjects in the user graph. There may be fragments.
             # Gather ALL subjects in the user graph. There may be fragments.
             #subj_gen = self.ds.graph(self._main_uri(uid)).subjects()
             #subj_gen = self.ds.graph(self._main_uri(uid)).subjects()
@@ -352,21 +358,26 @@ class RsrcCentricLayout:
         self.ds.update(qry)
         self.ds.update(qry)
 
 
 
 
-    def create_or_replace_rsrc(self, uid, trp, backup_uid=None):
+    def create_or_replace_rsrc(self, uid, trp):
         '''
         '''
         Create a new resource or replace an existing one.
         Create a new resource or replace an existing one.
         '''
         '''
-        self.delete_rsrc_data(uid, backup_uid)
+        self.delete_rsrc_data(uid)
 
 
         return self.modify_rsrc(uid, add_trp=trp)
         return self.modify_rsrc(uid, add_trp=trp)
 
 
 
 
     def modify_rsrc(self, uid, remove_trp=set(), add_trp=set()):
     def modify_rsrc(self, uid, remove_trp=set(), add_trp=set()):
         '''
         '''
-        See base_rdf_layout.update_rsrc.
+        Modify triples about a subject.
+
+        This method adds and removes triple sets from specific graphs,
+        indicated by the term rotuer. It also adds metadata about the changed
+        graphs.
         '''
         '''
         remove_routes = defaultdict(set)
         remove_routes = defaultdict(set)
         add_routes = defaultdict(set)
         add_routes = defaultdict(set)
+        historic = VERS_CONT_LABEL in uid
 
 
         # Create add and remove sets for each graph.
         # Create add and remove sets for each graph.
         for t in remove_trp:
         for t in remove_trp:
@@ -376,6 +387,8 @@ class RsrcCentricLayout:
             target_gr_uri = self._map_graph_uri(t, uid)
             target_gr_uri = self._map_graph_uri(t, uid)
             add_routes[target_gr_uri].add(t)
             add_routes[target_gr_uri].add(t)
 
 
+        # Decide if metadata go into historic or current graph.
+        meta_uri = HIST_GR_URI if historic else META_GR_URI
         # Remove and add triple sets from each graph.
         # Remove and add triple sets from each graph.
         for gr_uri, trp in remove_routes.items():
         for gr_uri, trp in remove_routes.items():
             gr = self.ds.graph(gr_uri)
             gr = self.ds.graph(gr_uri)
@@ -383,50 +396,35 @@ class RsrcCentricLayout:
         for gr_uri, trp in add_routes.items():
         for gr_uri, trp in add_routes.items():
             gr = self.ds.graph(gr_uri)
             gr = self.ds.graph(gr_uri)
             gr += trp
             gr += trp
-            self.ds.graph(META_GR_URI).add((
+            # Add metadata.
+            self.ds.graph(meta_uri).set((
                 gr_uri, nsc['foaf'].primaryTopic, nsc['fcres'][uid]))
                 gr_uri, nsc['foaf'].primaryTopic, nsc['fcres'][uid]))
+            self.ds.graph(meta_uri).set((
+                gr_uri, nsc['fcrepo'].created, g.timestamp_term))
+            if historic:
+                # @FIXME Ugly reverse engineering.
+                ver_uid = uid.split(VERS_CONT_LABEL)[1].lstrip('/')
+                self.ds.graph(meta_uri).set((
+                    gr_uri, nsc['fcrepo'].hasVersionLabel, Literal(ver_uid)))
+            # @TODO More provenance metadata can be added here.
 
 
 
 
-    def delete_rsrc_data(self, uid, backup_uid=None):
-        if backup_uid:
-            self.create_snapshot(uid, backup_uid)
-        ag_uri.n3(), mg=mg_uri.n3(), sg=sg_uri.n3())
-
-        for guid in ('fcadmin', 'fcmain', 'fcstruct'):
-            self.ds.remove_graph(self.ds.graph(nsc[guid][uid])
-
-
-    ## PROTECTED MEMBERS ##
-
-    def _main_uri(self, uid, ver_uid=None):
-        '''
-        Convert a UID into a request URL to the graph store.
-        '''
-        if ver_uid:
-            uid += ';' + ver_uid
-
-        return nsc['fcmain'][uid]
+    def delete_rsrc_data(self, uid):
+        for guid in self._graph_uids:
+            self.ds.remove_graph(self.ds.graph(nsc[guid][uid]))
 
 
 
 
-    def _admin_uri(self, uid, ver_uid=None):
+    def snapshot_uid(self, uid, ver_uid):
         '''
         '''
-        Convert a UID into a request URL to the graph store.
+        Create a versioned UID string from a main UID and a versio n UID.
         '''
         '''
-        if ver_uid:
-            uid += ';' + ver_uid
+        if VERS_CONT_LABEL in uid:
+            raise ValueError('Resource \'{}\' is already a version.')
 
 
-        return nsc['fcadmin'][uid]
+        return '{}/{}/{}'.format(uid, VERS_CONT_LABEL, ver_uid)
 
 
 
 
-    def _struct_uri(self, uid, ver_uid=None):
-        '''
-        Convert a UID into a request URL to the graph store.
-        '''
-        if ver_uid:
-            uid += ';' + ver_uid
-
-        return nsc['fcstruct'][uid]
-
+    ## PROTECTED MEMBERS ##
 
 
     def _map_graph_uri(self, t, uid):
     def _map_graph_uri(self, t, uid):
         '''
         '''