Coverage report: /development/source/library/org/datagraph/spocq-shard/src/spocq-server/extensions/ldp.lisp
| Kind | Covered | All | % |
| expression | 87 | 1076 | 8.1 |
| branch | 6 | 58 | 10.3 |
Key
Not instrumented
Conditionalized out
Executed
Not executed
Both branches taken
One branch taken
Neither branch taken
1
;;; -*- Mode: lisp; Syntax: ansi-common-lisp; Base: 10; Package: org.datagraph.spocq.server.implementation; -*-
2
;;; Copyright 2019 [james anderson](mailto:james.anderson@setf.de) All Rights Reserved
4
(in-package :org.datagraph.spocq.server.implementation)
6
(:documentation "linked data platform protocol"
8
"This file implements the http binding for a linked data platform service.
10
It provides GET, PATCH, PUT, POST and DELETE operators for resources and containers.
11
(+ means covered in http-api-tests, - means implemented, but not testsd)
13
GET : the request uri designates either a simple resource - LPD-NR or LDP-RS, or an LDP-C.
14
+ LDP-NR : retrieved from the file system or redirected to the remote location
15
+ LDP-RS : content is retrieved from the respective graph
16
+ LDP-C : resources, the response depends on the request return preference
17
either just the structural and meta-data or that plus the member content
18
- with optional slicing.
20
PUT : request uri is the resource itself; operation distinguishes resource type
21
- LDP-NR : is delegated to the file system and the request content is stored opaquely
22
- LDP-RS : the content replaces the respective graph.
23
any membership assertions in a parent LDP-BC and -DC assertions are stable,
24
but an LDP-IC reference could change.
25
- LDP-C : the content replaces that of the respective graph, as per LPDP-RS,
26
but there are limitations.
27
any changes are retricted to meta-data and membership assertions are immutable.
29
POST : the relation between the document content and the request uri depends on the resource type
30
+ LDP-NR : is stored opaquely in content-adressable local files or remote locations
31
- LDP-RS : the request content is merged into the respective graph to augment the resources' rdf content.
32
as it is additive, no reference can change.
33
+ LDP-C : the request resource is the container and content is the contained element.
34
the member identifier is either supplied in the location header (which can be relative) as a slug
35
this serves as the base for parsing the request content and as the graph which holds the member resource content
37
DELETE : the request uri is unambiguosly an LDP-C or LDP-RS
38
- LDP-NR : delete opaquely as per mapping to the file system; delete any LDP-BC and -DB assertions, -IC are not possible
39
- LDP-RS : delete the resource itself; delete containment, direct membership or indirect membership assertions
40
+ LDP-C : delete the resource itself.
41
member deletion follows LDP-BC, LDP-DC and LDP-IC links based on configuration.
43
PATCH : permit SPARQL only, are delegated to the query processor, and constrained to apply to the target
48
the LDP-NR determination involves the request content type.
49
any RDF document type is handled as LDP-RS while non RDF is LDP-NR
51
The LDP working group explicitly limited their recommendation on auth issues to use cases and scenarios.
52
This means there are no specific requirements as to authorization granularity.
53
This implementation places controls at the repository level.
54
The authentication transpires as normal for HTTP via token, password or location.
55
The target repository is defined per host and initial authorization proceeds with respect to access controls
57
In addition, the respective resources permit also WAC specifications, whihc then apply tot he rarget resource
64
4.2.1.3 : etags for non-rdf / non-sparql resources
65
LDP-NR resources are described by a metadata resource, which is represented as a graph.
66
the revision information for changes to this graph serves as the etag
68
4.2.1.4 : need to provide the header, eg: rel=type, http://www.w3.org/ns/ldp#Resource link header
69
this is interpreted in requests and gernated in responses.
71
4.2.1.5 : upon creation the respective new uri must become the base uri during request processing.
72
this is accomplished in the graph-store-response method specific to /ldp resources in order
73
for it to be available in the decode methods. if none was present in the request, the generated
74
value is added to the request headers. not that there is no standard request header for an
77
4.4.1.2 : are type triples materialized?
78
if no is present in the request content, they are added in order to be available when retrieving
79
the resource from the store.
81
5.2.3.10 : allow clients to suggest the URI for a resource created through POST
82
iff no base iri is present, then iff a Location is present, it is use to set the base iri.
83
otherwise if a Slug header is present, it is used to generate the location - initially as the base iri.
84
this coupld be simplified to work with just the location and slug
88
is there a way to change some incidental data about a container, for example
90
- the membership term for a direct container
91
or does one accomplish this with graph store requests? with sparql patch requests?
93
does a response comprise exactly one resource, particularly a container, or more than one?
94
5.1 example 14, what does a typed response for a direct container include also an indirect container.
97
the question re its values was asked explicitly (https://lists.w3.org/Archives/Public/ietf-http-wg/2012OctDec/0122.html)
98
with one alternative suggested, that the base can come from the location header of the post
99
(http://www.w3.org/Protocols/HTTP/1.1/rfc2616bis/issues/#i80), but there was some scepticism and no conclusion.
100
given the recommendation examples, the base issue seems to have been decided to be 'the created resource'
103
is any posted content with a subject other than <> added to the container or appended to the newly created mamber?
105
is deletion transitive?
106
5.2.5 says nothing about deleting containers. is the consequence transitive?
107
5.4.3 also says nothing about deleting the container, just the containment link as a consequence of deleting the contained resource.
108
in (http://www.w3.org/TR/ldp-primer/) the best practice note about the bug tracker indicates the to delete the product resource would delete also its bug reports, but
109
the example deletes a bug only. no example deletes the product.
110
- GET is transitive, according to prefer.result=representation
111
- DELETE is unspecified
113
how does one move a resource from one container to another?
114
by using patch on the two containers or by resorting to something outside of ldp in order to get transactional integrity?
115
there is a mention in item 10 of the techincal issues in the charter, but the word appears in no other ldp document.
116
there is not use in the mailing list archive.
118
how are collations specified: sparql's order by does not include collation and does not reference
119
the three.argument version of fn:compare, but rather
120
'The collation for fn:compare is defined by XPath and identified by http://www.w3.org/2005/xpath-functions/collation/codepoint.
121
This collation allows for string comparison based on code point values.
122
Codepoint string equivalence can be tested with RDF term equivalence.'
124
if a POST request includes both a header uri specification (slug or location) and the content includes
125
relative subject uri, what is the precedence?
128
;;; (load "/development/source/library/org/datagraph/spocq/src/server/ldp.lisp")
130
(defparameter *ldp-repository-id* "openrdf-sesame/ldp")
131
(defparameter *class.ldp-http-request* 'ldp-http-request)
132
(defparameter *class.ldp-http-response* 'ldp-http-response)
134
;;; removed |/:account/:repository/:graph/:element*|
136
(defclass |ldp|:|Resource| (ldp:resource)
138
(:metaclass ldp:resource-class))
140
(defclass |ldp|:|RDFSource| (|ldp|:|Resource| ldp:rdf-source )
142
(:metaclass ldp:resource-class))
144
(defclass |ldp|:|NonRDFSource| (|ldp|:|Resource| ldp:non-rdf-source )
146
(:metaclass ldp:resource-class))
148
(defclass |ldp|:|Container| (|ldp|:|RDFSource| ldp:container)
150
(:metaclass ldp:resource-class))
152
(defclass |ldp|:|BasicContainer| (|ldp|:|Container| ldp:basic-container)
154
(:metaclass ldp:resource-class))
156
(defclass |ldp|:|DirectContainer| (|ldp|:|Container| ldp:direct-container)
158
(:metaclass ldp:resource-class))
160
(defclass |ldp|:|IndirectContainer| (|ldp|:|Container| ldp:indirect-container)
162
(:metaclass ldp:resource-class))
164
(defclass ldp-http-request (tbnl:tbnl-request) ())
165
(defclass ldp-http-response (tbnl:tbnl-response) ())
168
(defgeneric request-type-link-header-type (request)
169
(:documentation "Extract the class declaration from a request type link")
170
(:method ((request http:request))
171
(request-type-link-header-type (http:request-header request :link)))
172
(:method ((header string))
173
(let* ((links (split-string header #\,))
174
(scanner (load-time-value
175
(cl-ppcre:create-scanner `(:sequence :start-anchor
178
(:greedy-repetition 0 nil
179
(:inverted-char-class #\< #\> #\" #\{ #\}
180
:whitespace-char-class
181
(:range ,(code-char #x00) ,(code-char #x20)))))
183
(:greedy-repetition 0 nil :whitespace-char-class)
185
(:greedy-repetition 0 nil :whitespace-char-class)
186
"rel=" (:greedy-repetition 0 1 #\") "type" (:greedy-repetition 0 1 #\")))))
187
(types (loop for link in links
188
collect (multiple-value-bind (match strings)
189
(cl-ppcre:scan-to-strings scanner link)
191
(let ((type (dydra:intern-iri (aref strings 0))))
192
(when (and (symbolp type) (subtypep type 'ldp:resource))
194
(http:bad-request "Invalid type link: ~s" link))))))
196
(flet ((minimum-type (t1 t2)
197
(if (subtypep t1 t2) t1 t2)))
198
(reduce #'minimum-type types))
200
(:method ((type null))
203
(http:bad-request "Invalid type link: ~s" type)))
204
;;; (request-type-link-header-type "<http://www.w3.org/ns/ldp#NonRDFSource>; rel=\"type\"")
205
;;; (request-type-link-header-type "<http://www.w3.org/ns/ldp#NonRDFSource>; rel=type")
207
(defgeneric compute-response-type-link-header (resource)
208
(:method ((type symbol))
209
(format nil "<~a>; rel=type" (dydra:iri-lexical-form type)))
210
(:method ((resource null))
211
"<http://www.w3.org/ns/ldp#Resource>; rel=type")
212
(:method ((resource ldp:container))
213
(compute-response-type-link-header (type-of resource)))
214
(:method ((resource ldp:resource))
215
"<http://www.w3.org/ns/ldp#Resource>; rel=type"
216
;;(describe resource)
217
;;"<http://www.w3.org/ns/ldp#BasicContainer>; rel=type"
219
(:method ((resource t))
223
(defmethod http:make-resource ((class ldp:resource-class) (request http:request) &rest args
225
(identifier (dydra:intern-iri (concatenate 'string "http://"
226
(http:request-header request :host)
229
(declare (dynamic-extent args))
230
;; extract header hists and parameters
231
(let* ((link-class (request-type-link-header-type request)))
232
;; iff a class is provided, use it. otherwise the generic class will suffice until
233
;; the content is read
234
;;!! must distinguish between put class and post class
235
(cond ((equal "/" path) ; the root is hardwired to ba a basic container
236
(setf class (find-class |ldp|:|BasicContainer|)))
238
(setf class link-class)))
239
(dydra:log-debug "make-resource: class: ~s path: ~s identifier: ~s" (class-name class) path identifier)
240
(apply #'dydra:ensure-instance class :request request :identifier identifier args)))
241
;;; (clrhash (class-cache (find-class '|ldp|:|BasicContainer|)))
244
(defparameter *default-agent* nil)
245
;;; (defparameter *default-agent* (make-instance 'dydra:user :name "jhacker" :id (intern-iri "http://dydra.com/users/jhacker") :location "127.0.0.1"))
247
;;; this would come into play iff authorization constraints are to be applied to individual resources.
248
;;; in order to do this, an auth prologue would have to be added to linked-data-platform-response
249
;;; and/or its method combination would need to be changed.
250
(defmethod http:authorize-request ((resource ldp:resource) (request http:request))
252
(let* ((host-name (http:request-header request :host))
253
(repository-id (cond ((host-repository-id host-name :if-does-not-exist nil))
255
(dydra:log-warn "No repository defined for host: ~s" host-name)
256
(http:bad-request "No repository defined for host: ~s" host-name))))
257
(repository (dydra:repository repository-id))
258
(agent (http:request-agent request)))
260
(setf agent *default-agent*)
261
(setf (http:request-agent request) agent))
263
;; (setf (http:request-agent request) (spocq.i::system-agent))
266
;; control for authorized access to the repository itself is left to transaction creation
267
(setf (dydra:resource-store-repository-id resource) repository-id)
268
;; control for existence
269
(if (dydra:repository-exists-p repository)
270
;; control for disabled repositories
271
(if (dydra:repository-enabled-p repository-id)
272
;; control for access to the resource
273
(dydra:access-authorized-p repository agent (request-access-mode request))
274
(http:bad-request "The repository has been disabled: ~s." repository-id))
275
(http:not-found "Repository not found: ~a." repository)))))
277
(defmethod dydra:task-operation-access-mode ((task dydra:ldp-task) (op symbol))
278
(request-access-mode op))
280
(eval-when (:compile-toplevel :load-toplevel :execute)
281
(defclass ldp-resource-function (http:resource-function)
282
((http.i::default-accept-header
283
:initform "text/turtle"
285
(:metaclass c2mop:funcallable-standard-class)
286
(:documentation "A specialized resourc function for linked data platform
287
which sets the default response media type")))
290
;;; header argument handling
292
(let ((whitespace '(:greedy-repetition 0 nil :whitespace-char-class)))
293
(setf (cl-ppcre:parse-tree-synonym 'whitespace) whitespace))
295
(defparameter *prefer-header-scanner*
296
(cl-ppcre:create-scanner '(:sequence :start-anchor
297
"Prefer:" whitespace "return" whitespace #\= whitespace "representation"
298
(:register (:greedy-repetition 0 nil :everything))
300
:case-insensitive-mode t))
301
(defparameter *prefer-parameter-scanner*
302
(cl-ppcre:create-scanner '(:sequence :start-anchor whitespace #\; whitespace
303
(:register spocq.i::pn_prefix)
304
(:sequence whitespace #\= whitespace
305
(:register (:greedy-repetition 0 nil (:inverted-char-class #\;)))))))
306
;;; (cl-ppcre:scan-to-strings *prefer-header-scanner* "Prefer: return=representation; max-triple-count=\"500\"" :start 0)
309
(defun unquote-header-argument (string)
310
"RFC7240 permits both quoted and unquoted tokens"
311
(let ((length (length string)))
313
(if (eql (char string 0) #\")
314
(if (eql (char string (1- length)) #\")
315
(subseq string 1 (1- length))
316
(http:bad-request " invalid argument value: ~s" string))
320
(defun parse-integer-header-argument (string)
321
(handler-case (parse-integer (unquote-header-argument string))
322
(error (c) (http:bad-request (format nil "Invalid integer argument ~s: ~a" string c)))))
324
(defun parse-iri-list-header-argument (string)
325
(loop for iri in (de.setf.utility:split-string (unquote-header-argument string) #\space)
326
collect (spocq.i::intern-iri iri)))
328
(defgeneric parse-header-argument-value (initarg string)
329
(:method ((initarg (eql :include)) (value string))
330
(parse-iri-list-header-argument value))
331
(:method ((initarg (eql :omit)) (value string))
332
(parse-iri-list-header-argument value))
333
(:method ((initarg (eql :max-triple-count)) (value string))
334
(parse-integer-header-argument value))
335
(:method ((initarg (eql :max-member-count)) (value string))
336
(parse-integer-header-argument value))
337
(:method ((initarg (eql :max-kbyte-count)) (value string))
338
(parse-integer-header-argument value)))
340
(defparameter *prefer-parameters*
341
'(:return :include :omit :max-triple-count :max-member-count :max-kbyte-count))
343
(defun compute-prefer-header-bindings (header &optional (parameter-keywords *prefer-parameters*))
344
(multiple-value-bind (match strings) (cl-ppcre:scan-to-strings *prefer-header-scanner* header)
346
(loop with parameters = (aref strings 0)
348
with end = (length parameters)
350
for (match key-value) = (multiple-value-list (cl-ppcre:scan-to-strings *prefer-parameter-scanner* parameters :start start))
352
append (let* ((key (aref key-value 0))
353
(initarg-key (or (find key parameter-keywords :test #'string-equal)
354
(http:bad-request "Invalid prefer parameter: ~s" header)))
355
(value (aref key-value 1))
356
(initarg-value (parse-header-argument-value initarg-key value)))
357
(incf start (length match))
358
(list initarg-key initarg-value))
359
else do (http:bad-request " invalid prefer parmeter: ~s" header)))))
361
(defgeneric compute-effective-member-type (request-content-type field-type link-header-type)
362
(:documentation "For a given request, use any header type and type asserted in the content field
363
together with the content media type to derive and effective type or reject those given.")
364
(:method ((request-content-type mime:rdf) (field-type null) (header-type null))
366
(:method ((request-content-type mime:rdf) (field-type null) (header-type symbol))
368
(|ldp|:|Resource| |ldp|:|RDFSource|)
370
(:method ((request-content-type mime:mime-type) (field-type null) (header-type null))
371
|ldp|:|NonRDFSource|)
372
(:method ((request-content-type t) (field-type t) (header-type t))
374
(:method ((request-content-type t) (field-type null) (header-type t))
376
(:method ((request-content-type null) (field-type null) (header-type null))
380
(defgeneric compute-resource-external-identifier (resource &optional member-identifier)
381
(:method ((resource ldp:resource) &optional member-identifier)
382
(let* ((repository (spocq.i::instance-repository resource))
383
(repository-identifier (spocq.e:repository-identifier repository))
384
(instance-identifier (spocq.e:instance-identifier resource)))
385
(if (and (null member-identifier)
386
;; module http/https scheme ?!
387
(spocq.e:iri-equal repository-identifier instance-identifier))
388
repository-identifier
389
(intern-iri (format nil "~/format-csv-iri-namestring//ldp?resource=~/format-csv-iri-namestring/"
390
repository-identifier
391
(or member-identifier instance-identifier)))))))
393
;;; (compute-response-type-link-header "Prefer: return=representation; max-triple-count=\"500\"")
395
(defparameter *ldp-include* nil)
396
(defparameter *ldp-omit* nil)
397
(defparameter *ldp-page* nil)
398
(defparameter *ldp-max-triple-count* nil)
399
(defparameter *ldp-max-member-count* nil)
400
(defparameter *ldp-max-kbyte-count* nil)
402
(defmethod http:request-argument-list ((request ldp-http-request))
403
"add to the base arguemnt those which are extracted from the ldb representations header."
404
(let ((base-arguments (call-next-method))
405
(ldp-arguments (loop for header in (http:request-headers request)
406
for binding = (compute-prefer-header-bindings header)
409
(spocq.i::plist-merge (append ldp-arguments
411
:include *ldp-include*
413
:max-triple-count *ldp-max-triple-count*
414
:max-member-count *ldp-max-member-count*
415
:max-kbyte-count *ldp-max-kbyte-count*
418
(defmethod graph-store-options :before ((resource |/:account/:repository/ldp|) request response request-type response-type)
419
(setf (http:response-header response :accept-post) "text/turtle, application/n-triples")
420
(setf (http:response-header response :accept-patch) "application/sparql-update")
423
(defmethod graph-store-head :before ((resource |/:account/:repository/ldp|) request response request-type response-type
424
&key (context (resource-graph resource)) (target context))
426
(http:bad-request "A resource must be specified."))
427
(let ((ldp-resource (ldp:ensure-resource :identifier target
428
:repository (resource-repository resource))))
429
(pushnew (compute-response-type-link-header ldp-resource) (http:response-header response :link) :test #'equal))
430
(setf (http:response-header response :accept-post) "text/turtle, application/n-triples")
431
(setf (http:response-header response :accept-patch) "application/sparql-update")
434
;;; dispatch linked data platform access according to combination of content media type and length
435
(defmethod graph-store-response :delete ((resource |/:account/:repository/ldp|) request response (request-type t) (response-type t))
436
(indirect-linked-data-platform-response :delete resource request response request response-type nil))
438
(defmethod graph-store-response :get ((resource |/:account/:repository/ldp|) request response (request-type t) (response-type t))
439
"Respond with graph content. Allow an optional context"
440
(indirect-linked-data-platform-response :get resource request response request-type response-type nil))
442
(defmethod graph-store-response :post ((resource |/:account/:repository/ldp|) request response (request-type null) (response-type t))
443
(http:bad-request "A content-type must be provided: ~a~@[~a~]" resource (resource-graph resource)))
445
(defmethod graph-store-response :post ((resource |/:account/:repository/ldp|) request response (request-type t) (response-type t))
446
;; persuant to https://www.w3.org/TR/ldp/#ldpr-resource, adjust the base iri to reflect any slug
447
(let* ((location (http:request-header request "Location")))
448
(log-graph-store-service-response resource request location)
449
;; introduce a default base uri for triple content -normally, there is no header present
450
(let ((member-type (or (request-type-link-header-type request)
451
(typecase request-type
452
(mime:rdf '|ldp|:|RDFSource|)
453
(t '|ldp|:|NonRDFSource|)))))
454
(unless (http:request-header request :base-iri)
455
;; manufacture a base-iri for decoding.
456
;; slug takes precedence, then location, in order to interpret
457
;; correctly the header for remote non-rdf resources
458
(let ((location (http:request-header request :location))
459
(slug (http:request-header request :slug)))
461
(let* ((target (or (resource-graph resource) (resource-identifier resource)))
462
(base-iri (compute-member-iri target :slug slug :container-p (when member-type (subtypep member-type 'ldp:container)))))
463
(setf (http:request-base-iri request) (iri-lexical-form base-iri))))
465
(setf (http:request-base-iri request) location))))))
466
(typecase request-type
468
(labels ((post-content (pathname effective-request-type)
469
(unwind-protect (indirect-linked-data-platform-response :post resource request response effective-request-type response-type pathname)
470
(conditional-delete-file pathname)))
471
(process-location (stream)
472
(setf (http:request-header request :location) nil)
473
(let ((request-content-stream (http:request-content-stream request)))
474
(unwind-protect (progn (setf (http:request-content-stream request) stream)
475
(multiple-value-call #'post-content (call-next-method)))
476
(setf (http:request-content-stream request) request-content-stream)))))
478
(hunchentoot::call-with-open-request-stream #'process-location location :accept request-type)
479
(multiple-value-call #'post-content (call-next-method)))))
482
(indirect-linked-data-platform-response :post resource request response request-type response-type (intern-iri location))
483
;; otherwise save in account-specific location and pass that
484
(flet ((post-saved-content (pathname effective-request-type)
485
(let* ((hash-pathname (spocq.i::file-hash-pathname pathname))
486
(hash-pathname-namestring (namestring hash-pathname)))
487
(unless (every #'(lambda (c) (or (alphanumericp c) (eql c #\/))) hash-pathname-namestring)
488
;; constrain just in case
489
(http:bad-request "Invalid location: ~s" hash-pathname-namestring))
491
(make-pathname :directory (append '(:relative "data") (rest (pathname-directory hash-pathname)))
492
:defaults hash-pathname))
493
(let* ((repository-pathname (repository-pathname (resource-repository resource)))
494
(persistent-pathname (merge-pathnames hash-pathname repository-pathname))
495
(file-url (spocq.i::intern-file-url hash-pathname))
497
(cond ((probe-file persistent-pathname)
498
(conditional-delete-file pathname))
500
(ensure-directories-exist persistent-pathname)
501
(rename-file pathname persistent-pathname)))
503
(multiple-value-prog1 (indirect-linked-data-platform-response :post resource request response effective-request-type response-type file-url)
505
(unless succeeded (conditional-delete-file persistent-pathname)))))))
506
(multiple-value-call #'post-saved-content (call-next-method))))))))
510
(defgeneric indirect-linked-data-platform-response (method resource request response request-type response-type request-source &key target)
511
(:method :before (method resource request response request-type response-type request-source &rest args)
512
(declare (ignore args))
513
(dydra:log-debug "indirect-linked-data-platform-response: ~s ~s ~s (~s ~s)"
514
dydra:*task-id* resource
515
(http:request-headers request)
518
(when (typep resource spocq.i::*linked-data-platform.describe-type*)
520
(when (typep request spocq.i::*linked-data-platform.describe-type*)
522
(when (typep response spocq.i::*linked-data-platform.describe-type*)
523
(describe response)))
525
;; these should be handled by the specializations, above
526
(:method ((method (eql :head)) (resource repository-resource) request response request-type response-type request-source &key target)
527
(declare (ignore target))
528
(log-warn "indirect-linked-data-platform-response: head: ~s" resource)
531
(:method ((method (eql :options)) (resource repository-resource) request response request-type response-type request-source &key target)
532
(declare (ignore target))
533
(log-warn "indirect-linked-data-platform-response: options: ~s" resource)
536
(:method ((method t) (resource |/:account/:repository/ldp|) request response request-type response-type request-source
537
&key (target (or (resource-graph resource) (resource-identifier resource))))
538
(let ((ldp-resource (ldp:ensure-resource :identifier target
539
:repository (resource-repository resource))))
540
(linked-data-platform-response method ldp-resource request response request-type response-type request-source)))
542
(:method ((method t) (resource |/:account/:repository/:graph|) request response request-type response-type request-source
543
&key (target (or (resource-graph resource) (resource-identifier resource))))
544
(let ((ldp-resource (ldp:ensure-resource :identifier target
545
:repository (resource-repository resource))))
546
(linked-data-platform-response method ldp-resource request response request-type response-type request-source)))
548
(:method ((method t) (resource |/:account/:repository/:graph/:element*|) request response request-type response-type request-source
549
&key (target (or (resource-graph resource) (resource-identifier resource))))
550
;; in this case, it is the resource itself
551
(let ((ldp-resource (ldp:ensure-resource :identifier target
552
:repository (resource-repository resource))))
553
(linked-data-platform-response method ldp-resource request response request-type response-type request-source))))
556
(defgeneric linked-data-platform-response (method resource request response request-type response-type request-source)
557
(:documentation "Given the target resource, the request object and the response object,
558
perform the operation on the target. the operation is that of the original request.")
560
(:method :around ((method t) (resource ldp:resource) request response request-type response-type request-source)
561
"Wrap all requests with logic - post authorization, to
562
- determine the target container,
563
- open a transaction (type respective the request method),
564
- bind the request to the repository and then,
565
- continue with the operation implementation"
567
(let* ((repository-id (dydra:repository-id (ldp::resource-repository resource)))
568
(dydra:*task-id* (or (request-id request) (dydra:make-task-id)))
569
(argument-list (http:request-argument-list request))
570
(parsed-argument-list (parse-http-configuration argument-list))
571
(dydra:*base-iri* (dydra:instance-identifier resource)))
572
(setf (http:response-allow response) '(:DELETE :HEAD :OPTIONS :GET :PATCH :POST :PUT))
573
(setf (http:response-header response :accept-post) "text/turtle, application/n-triples")
574
(setf (http:response-header response :accept-patch) "application/sparql-update")
575
;; always add the abstract resurce
576
(push "<http://www.w3.org/ns/ldp#Resource>; rel=type" (http:response-header response :link))
578
(with-http-configuration (list* :task-id dydra:*task-id* parsed-argument-list)
579
(let ((task (dydra:make-ldp-task
581
:agent (http:request-agent request)
582
:dynamic-bindings dydra:*dynamic-bindings*
583
:repository-id repository-id
584
:response-content-type response-type
585
:revision-id dydra:*revision-id*
586
:task-id dydra:*task-id*
587
:user-id dydra:*user-id*))
588
(disposition (case (request-access-mode method)
589
(|acl|:|Write| :commit)
590
(|acl|:|Read| :continue))))
592
(dydra:with-task-environment (:task task :normal-disposition disposition)
593
(dydra:log-debug "with task transaction ~s ~s" *task* *transaction*) ; should include the task id, the repository and the pertinent revision
594
(let ((dydra:*query* dydra:*task*))
595
;; make sure instance is current wrt the store
596
(dydra:synchronize-resource resource)
597
;; set provisionally, but allow post to replace this value
598
(let ((revision-id (dydra:instance-store-revision resource))
599
(if-match (http:request-header request :if-match))
600
(etag-scanner (load-time-value (cl-ppcre:create-scanner '(:sequence #\" (:register (:greedy-repetition 1 nil (:inverted-char-class #\"))) #\")))))
601
(dydra:log-debug "contingent if-match constraint: revision-id: ~a if-match: ~a" revision-id if-match)
603
(let ((etag-value (first (coerce (nth-value 1 (cl-ppcre:scan-to-strings etag-scanner if-match)) 'list))))
605
(http:bad-request "Invalid If-Match value: ~a" if-match))
606
(unless (equal etag-value revision-id)
607
(http:precondition-failed "Expected ETAG ~a != ~a" revision-id etag-value))))
608
;; add the etag for read methods only
609
;; (setf (http:response-header response :etag) (write-to-string revision-id))
611
(multiple-value-prog1 (call-next-method)
612
(dydra:log-debug "ldp: ~s ~s ~s"
613
dydra:*task-id* resource
614
(http:response-headers response)))))
615
(spocq.e:task-authorization-error (c)
616
(http:unauthorized (format nil "~a" c))))))))
618
(:method ((method (eql :delete)) (resource ldp:resource) request response request-type response-type request-source)
619
(let* ((start-time (get-universal-time))
620
(repository (spocq.i::instance-repository resource))
621
(object-identifier (compute-resource-external-identifier resource))
622
(task-uuid (spocq.i::intern-uuid dydra:*task-id*))
623
(client-request-id (request-client-request-id request)))
624
(ldp:resource-delete resource)
625
(setf (http:response-etag response) (dydra:resolve-repository-revision-id repository))
626
(setf (http:response-header response "Client-Request-ID") client-request-id)
627
(setf (http:response-header response "Request-ID") dydra:*task-id*)
628
(spocq.i::make-list-solution-field
629
:dimensions spocq.i::*construct-dimensions*
630
:solutions (spocq.i::filter-solution-field `((,task-uuid |as|:|id| ,task-uuid)
631
(,task-uuid |rdf|:|type| |as|:|Delete|)
632
(,task-uuid |rdf|:|type| |mthd|:|DELETE|)
633
(,task-uuid |prov|:|startedAtTime| ,(spocq.i::universal-time-date-time start-time))
634
(,task-uuid |prov|:|endedAtTime| ,(spocq.i::universal-time-date-time (get-universal-time)))
635
(,task-uuid |as|:|actor| ,(spocq.i::agent-identifier (http:request-agent request)))
636
(,task-uuid |as|:|object| ,object-identifier)
637
(,task-uuid |as|:|inReplyTo| ,client-request-id))
638
(complement #'null)))))
640
(:method ((method (eql :get)) (resource ldp:rdf-source) request response request-type response-type request-source)
641
"Return a representation of a resource which comprises rdf, constrained by page argument.
642
For containers, recognize also include/omit and member limits.
643
handle paging depending on presence of max count and/or page"
644
(let* ((revision-identifier (http:request-if-match request))
645
(external-identifier nil))
646
(labels ((external-identifier ()
647
(or external-identifier (setf external-identifier (compute-resource-external-identifier resource))))
648
(page-location-url (page)
649
(format nil "~a~:[?~;&~]page=~a" (external-identifier) (find #\? external-identifier) page)))
650
(multiple-value-bind (solutions page-count)
651
(ldp:resource-get resource
653
:include *ldp-include*
655
:max-member-count *ldp-max-member-count*
656
:max-triple-count *ldp-max-triple-count*
657
:revision revision-identifier)
659
(push (page-location-url 1) (http:response-header response :link))
660
(when (> *ldp-page* 1)
661
(push (page-location-url (1- *ldp-page*)) (http:response-header response :link)))
662
(when (< *ldp-page* page-count)
663
(push (page-location-url (1+ *ldp-page*)) (http:response-header response :link)))
664
(push (page-location-url page-count) (http:response-header response :link)))
665
(setf (http:response-header response :location) (dydra:iri-lexical-form (dydra:instance-identifier resource)))
666
(pushnew (compute-response-type-link-header resource) (http:response-header response :link) :test #'equal)
667
;; use as etag the revision id for the current revision
668
(setf (http:response-header response :etag) (or revision-identifier
669
(write-to-string (dydra:repository-revision-id *repository*))))
670
(dydra:make-list-solution-field
671
:dimensions dydra:*construct-dimensions*
672
:solutions solutions)))))
674
(:method ((method (eql :get)) (resource ldp:non-rdf-source) request response request-type response-type request-source)
675
"Return a representation of an opaque non-rdf resource.
676
If it is local, perform any content negotiation inline and return nil.
677
If it is remote redirect."
678
(let* ((revision-identifier (http:request-if-match request))
679
(media-type (ldp::resource-media-type resource))
680
(location (ldp::resource-location resource)))
683
(spocq:file-url ;; content-negotiate and emit. type will have defaulted to turtle
684
(let ((pathname (let* ((repository (spocq.i::instance-repository resource))
685
(repository-pathname (repository-pathname repository))
686
(url-pathname (spocq.i::file-url-pathname location)))
687
(case (first (pathname-directory url-pathname))
688
(:absolute (http:bad-request "Invalid resource location: ~s" location))
690
(let* ((namestring (namestring url-pathname)))
691
(unless (every #'(lambda (c) (or (alphanumericp c) (eql c #\/))) namestring)
692
;; constrain just in case
693
(http:bad-request "Invalid location: ~s" namestring)))
694
(merge-pathnames url-pathname repository-pathname))))
695
(setf (http:response-header response :etag) (or revision-identifier
696
(write-to-string (dydra:repository-revision-id *repository*))))
697
(when (and (null response-type) media-type)
698
(setf (http:response-header response :content-type) (mime:mime-type-namestring media-type)))
699
(cond ((probe-file pathname)
700
(setf (http:response-header response :last-modified) (hunchentoot:rfc-1123-date (or (file-write-date pathname)
701
(get-universal-time))))
702
(http:copy-stream pathname (http:response-content-stream response)))
704
(http:not-found "Non-RDF source not found: ~s: ~s"
705
(instance-identifier resource)
707
(spocq:http-url ;; redirect
708
(setf (http:response-header response :location) (iri-lexical-form location))
709
(setf (http:response-content-length response) 0)
710
(http:see-other :location (iri-lexical-form location)))
712
(http:bad-request "Resource href location is not supported: ~s: ~s"
713
(instance-identifier resource)
716
(http:not-found "Non-RDF source not found: ~s"
717
(instance-identifier resource))))
720
(:method ((method (eql :patch)) (resource ldp:resource) request response (request-type mime:application/sparql-update) response-type request-source)
721
(let ((base-iri (dydra:instance-identifier resource)))
722
(ldp:resource-patch resource (parse-sparql (http:request-body request) :base-iri base-iri))))
724
(:method ((method (eql :post)) (resource ldp:container) request response (request-type mime:rdf) response-type request-source)
725
"posting rdf content to a container:
726
the resource class determines how to decode the request content
727
any link type specified means create a member from the decoded request content."
728
(let* ((start-time (get-universal-time))
729
(repository (spocq.i::instance-repository resource))
730
(content-field (dydra:parse-nquads request-source))
731
(member-field-type (ldp:resource-type content-field))
732
(member-link-type (request-type-link-header-type request))
733
(member-type (compute-effective-member-type request-type member-field-type member-link-type))
734
;; base iri should have been patched prior to parsing the request content
735
(member-slug (http:request-header request :slug))
736
(member (if (and member-slug member-type)
737
;; must be invoked theoruhg a path (eg graph-store-response) which established a base iri
738
(let* ((member-identifier (intern-iri (or (http:request-header request :base-iri)
739
(http:bad-request "a request base iri is required.")))))
740
(make-instance member-type
741
:triples (if (null member-field-type)
742
(cons `(,member-identifier |rdf|:|type| ,member-type) content-field)
744
:identifier member-identifier
745
:repository (spocq.i::instance-repository resource)))
746
(make-instance '|ldp|:|RDFSource|
747
:triples content-field
748
:repository (spocq.i::instance-repository resource))))
749
(location-identifier (ldp:resource-post resource member))
750
(task-uuid (spocq.i::intern-uuid dydra:*task-id*))
751
(client-request-id (request-client-request-id request)))
752
(setf (http:response-header response :location) (dydra:iri-lexical-form location-identifier))
753
(setf (http:response-etag response) (dydra:resolve-repository-revision-id repository))
754
(setf (http:response-header response "Client-Request-ID") client-request-id)
755
(setf (http:response-header response "Request-ID") dydra:*task-id*)
756
;; once the post happens, the resource will have been aligned with the type
757
;; the response should reflect the end class
759
(pushnew (compute-response-type-link-header member-type) (http:response-header response :link) :test #'equal))
760
;; the etag is not provided for modifications,, but if it were, it would
761
;; need to reflect the transaction id, rather its reference revision
763
(let* ((object-identifier (compute-resource-external-identifier resource (instance-identifier member))))
764
(spocq.i::make-list-solution-field
765
:dimensions spocq.i::*construct-dimensions*
766
:solutions (spocq.i::filter-solution-field `((,task-uuid |as|:|id| ,task-uuid)
767
(,task-uuid |rdf|:|type| |as|:|Update|)
768
(,task-uuid |rdf|:|type| |mthd|:|POST|)
769
(,task-uuid <http://purl.org/dc/terms/title> ,member-slug)
770
(,task-uuid |prov|:|startedAtTime| ,(spocq.i::universal-time-date-time start-time))
771
(,task-uuid |prov|:|endedAtTime| ,(spocq.i::universal-time-date-time (get-universal-time)))
772
(,task-uuid |as|:|actor| ,(spocq.i::agent-identifier (http:request-agent request)))
773
(,task-uuid |as|:|object| ,object-identifier)
774
(,task-uuid |as|:|inReplyTo| ,client-request-id))
775
(complement #'null))))))
777
(:method ((method (eql :post)) (resource ldp:container) request response (request-type mime:mime-type) response-type (request-source spocq:iri))
778
"posting non-rdf content to a container:
779
construct a non-rdf-resource to cache the location and metadata and post that.
780
see https://ci.mines-stetienne.fr/rdfp/ for media type predicate"
781
(let* ((start-time (get-universal-time))
782
(repository (spocq.i::instance-repository resource))
783
(member-link-type (request-type-link-header-type request))
784
(member-type (compute-effective-member-type request-type nil member-link-type))
785
(member-identifier (intern-iri (or (http:request-header request :base-iri)
786
(http:bad-request "a request base iri is required."))))
787
(request-type-url (or (spocq.i::mime-type-url request-type :if-does-not-exist nil)
788
(http:unsupported-media-type "LDP: post media type not supported: ~a" (type-of request-type))))
789
(content-field `((,member-identifier |rdf|:|type| ,member-type)
790
(,member-identifier <https://w3id.org/rdfp/mediaType> ,request-type-url)
791
(,member-identifier |dcat|:|downloadURL| ,request-source)))
792
;; base iri should have been patched prior to parsing the request content
793
(member-slug (http:request-header request :slug))
794
(member (make-instance member-type
795
:triples content-field
796
:identifier member-identifier
797
:repository (spocq.i::instance-repository resource)))
798
(location-identifier (ldp:resource-post resource member))
799
(task-uuid (spocq.i::intern-uuid dydra:*task-id*))
800
(client-request-id (request-client-request-id request)))
801
(setf (http:response-header response :location) (dydra:iri-lexical-form location-identifier))
802
(setf (http:response-etag response) (dydra:resolve-repository-revision-id repository))
803
(setf (http:response-header response "Client-Request-ID") client-request-id)
804
(setf (http:response-header response "Request-ID") dydra:*task-id*)
805
;; once the post happens, the resource will have been aligned with the type
806
;; the response should reflect the end class
807
(pushnew (compute-response-type-link-header member-type) (http:response-header response :link) :test #'equal)
808
;; the etag is not provided for modifications,, but if it were, it would
809
;; need to reflect the transaction id, rather its reference revision
811
(let* ((object-identifier (compute-resource-external-identifier resource member-identifier)))
812
(spocq.i::make-list-solution-field
813
:dimensions spocq.i::*construct-dimensions*
814
:solutions (spocq.i::filter-solution-field `((,task-uuid |as|:|id| ,task-uuid)
815
(,task-uuid |rdf|:|type| |as|:|Update|)
816
(,task-uuid |rdf|:|type| |mthd|:|POST|)
817
(,task-uuid <http://purl.org/dc/terms/title> ,member-slug)
818
(,task-uuid |prov|:|startedAtTime| ,(spocq.i::universal-time-date-time start-time))
819
(,task-uuid |prov|:|endedAtTime| ,(spocq.i::universal-time-date-time (get-universal-time)))
820
(,task-uuid |as|:|actor| ,(spocq.i::agent-identifier (http:request-agent request)))
821
(,task-uuid |as|:|object| ,object-identifier)
822
(,task-uuid |as|:|inReplyTo| ,client-request-id))
823
(complement #'null))))))
825
(:method ((method (eql :put)) (resource ldp:resource) request response (request-type t) response-type request-source)
826
"Replace the 'identified resource'.
827
First clear its graph, then insert any triples and any quads in the given graph.
828
Quads in another graph leas to a BAD-REQUEST error."
829
(handler-case (ldp:resource-put resource (dydra:parse-nquads request-source))
830
(spocq.e:constraint-violation (condition)
831
(push "<http://www.w3.org/ns/ldp#contains>; rel=\"http://www.w3.org/ns/ldp#constrainedBy\""
832
(http:response-header response :link))
833
(apply #'http:conflict (condition-format-control condition) (condition-format-arguments condition)))
834
(spocq.e:request-error (condition)
835
(apply #'http:bad-request (condition-format-control condition) (condition-format-arguments condition))))
836
(setf (http:response-header response :location) (dydra:iri-lexical-form (dydra:instance-identifier resource)))
837
(pushnew (compute-response-type-link-header resource) (http:response-header response :link) :test #'equal)
838
;; the etag is not provided for modifications,, but if it were, it would
839
;; need to reflect the transaction id, rather its reference revision
845
(load-time-validate (compute-prefer-header-bindings)
846
(mapcar #'compute-prefer-header-bindings
847
'("Prefer: return=representation; max-triple-count=\"500\""
848
"Prefer: return=representation; max-member-count=\"10\""
849
"Prefer: return=representation; max-member-count=\"10\"; max-kbyte-count=\"100\""
850
"Prefer: return=representation; omit=\"http://www.w3.org/ns/ldp#PreferMembership http://www.w3.org/ns/ldp#PreferContainment\""
851
"Prefer: return=representation; include=\"http://www.w3.org/ns/ldp#PreferMinimalContainer\""
853
'((:MAX-TRIPLE-COUNT 500) (:MAX-MEMBER-COUNT 10)
854
(:MAX-MEMBER-COUNT 10 :MAX-KBYTE-COUNT 100)
855
(:OMIT (|http://www.w3.org/ns/ldp#|:|PreferMembership| |http://www.w3.org/ns/ldp#|:|PreferContainment|))
856
(:INCLUDE (|http://www.w3.org/ns/ldp#|:|PreferMinimalContainer|))))