001package org.w3.ldp.testsuite.test;
002
003import com.github.jsonldjava.core.JsonLdError;
004import com.github.jsonldjava.core.JsonLdProcessor;
005import com.github.jsonldjava.utils.JsonUtils;
006import com.hp.hpl.jena.rdf.model.Model;
007import com.hp.hpl.jena.rdf.model.Property;
008import com.hp.hpl.jena.rdf.model.Resource;
009import com.hp.hpl.jena.rdf.model.StmtIterator;
010import com.hp.hpl.jena.vocabulary.DCTerms;
011import com.hp.hpl.jena.vocabulary.RDF;
012import com.jayway.restassured.response.Response;
013import org.apache.http.HttpStatus;
014import org.apache.marmotta.commons.vocabulary.LDP;
015import org.jboss.resteasy.spi.Failure;
016import org.testng.annotations.Optional;
017import org.testng.annotations.Parameters;
018import org.testng.annotations.Test;
019import org.w3.ldp.testsuite.LdpTestSuite;
020import org.w3.ldp.testsuite.annotations.SpecTest;
021import org.w3.ldp.testsuite.annotations.SpecTest.METHOD;
022import org.w3.ldp.testsuite.annotations.SpecTest.STATUS;
023import org.w3.ldp.testsuite.exception.SkipException;
024import org.w3.ldp.testsuite.exception.SkipNotTestableException;
025import org.w3.ldp.testsuite.http.HttpMethod;
026import org.w3.ldp.testsuite.mapper.RdfObjectMapper;
027import org.w3.ldp.testsuite.matcher.HeaderMatchers;
028
029import java.io.IOException;
030import java.net.URI;
031import java.net.URISyntaxException;
032
033import static org.hamcrest.MatcherAssert.assertThat;
034import static org.hamcrest.Matchers.not;
035import static org.hamcrest.text.IsEmptyString.isEmptyOrNullString;
036import static org.testng.Assert.*;
037import static org.w3.ldp.testsuite.http.HttpHeaders.*;
038import static org.w3.ldp.testsuite.http.MediaTypes.TEXT_TURTLE;
039import static org.w3.ldp.testsuite.matcher.HeaderMatchers.isValidEntityTag;
040import static org.w3.ldp.testsuite.matcher.HttpStatus4xxRangeMatcher.is4xxRange;
041import static org.w3.ldp.testsuite.matcher.HttpStatusSuccessMatcher.isSuccessful;
042
043/**
044 * Tests all RDF source LDP resources, including containers and member resources.
045 */
046public abstract class RdfSourceTest extends CommonResourceTest {
047
048        private static final String MSG_NO_READ_ONLY_PROPERTY = "Skipping test because we have no read-only properties to PUT."
049                        + " Server-managed properties are specified using the \"read-only-prop\" command-line parameter.";
050        private static final String MSG_PUT_RESTRICTIONS = "Skipping test because there are restrictions on PUT content for this resource. "
051                        + "The requirement needs to be tested manually.";
052
053        private static final String UNKNOWN_PROPERTY = "http://example.com/ns#comment";
054
055        /** A relative URI to use for testing. */
056        protected static final String RELATIVE_URI = "relatedResource";
057
058        @Parameters("auth")
059        public RdfSourceTest(@Optional String auth) throws IOException {
060                super(auth);
061        }
062
063        @Test(
064                        groups = {MUST},
065                        description = "LDP servers MUST assign the default base-URI "
066                                        + "for [RFC3987] relative-URI resolution to be the HTTP "
067                                        + "Request-URI when the resource already exists, and to "
068                                        + "the URI of the created resource when the request results "
069                                        + "in the creation of a new resource.")
070        @Parameters("relativeUri")
071        @SpecTest(
072                        specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-gen-defbaseuri",
073                        testMethod = METHOD.AUTOMATED,
074                        approval = STATUS.WG_APPROVED)
075        public void testRelativeUriResolutionPut(@Optional String relativeUri) {
076                skipIfMethodNotAllowed(HttpMethod.PUT);
077
078                if (restrictionsOnTestResourceContent()) {
079                        throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(),
080                                        MSG_PUT_RESTRICTIONS, skipLog);
081                }
082
083                if (relativeUri == null) {
084                        relativeUri = RELATIVE_URI;
085                }
086
087                String resourceUri = getResourceUri();
088                Response response = buildBaseRequestSpecification()
089                                .header(ACCEPT, TEXT_TURTLE)
090                        .expect()
091                                .statusCode(isSuccessful())
092                                .header(ETAG, isValidEntityTag())
093                        .when()
094                                .get(resourceUri);
095
096                String eTag = response.getHeader(ETAG);
097                Model model = response.as(Model.class, new RdfObjectMapper(resourceUri));
098
099                // Add a statement with a relative URI.
100                getPrimaryTopic(model, resourceUri).addProperty(DCTerms.relation, model.getResource(relativeUri));
101
102                // Put the resource back using relative URIs.
103                Response put = buildBaseRequestSpecification()
104                                .contentType(TEXT_TURTLE).header(IF_MATCH, eTag)
105                                .body(model, new RdfObjectMapper("")) // keep URIs relative
106                                .when().put(resourceUri);
107                if (!isSuccessful().matches(put.getStatusCode())) {
108                        throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(),
109                                        "Cannot verify relative URI resolution because the PUT request failed. Skipping test.",
110                                        skipLog);
111                }
112
113                // Get the resource again to verify its content.
114                model = buildBaseRequestSpecification()
115                                .header(ACCEPT, TEXT_TURTLE)
116                        .expect()
117                                .statusCode(isSuccessful())
118                        .when()
119                                .get(resourceUri).as(Model.class, new RdfObjectMapper(resourceUri));
120
121                // Verify the change.
122                String relationAbsoluteUri = resolveIfRelative(resourceUri, relativeUri);
123                assertTrue(
124                                model.contains(
125                                                getPrimaryTopic(model, resourceUri),
126                                                DCTerms.relation,
127                                                model.getResource(relationAbsoluteUri)
128                                ),
129                                "Response does not have expected triple: <" + resourceUri + "> dcterms:relation <" + relationAbsoluteUri + ">."
130                );
131        }
132
133        @Test(
134                        groups = {MUST},
135                        description = "If a HTTP PUT is accepted on an existing resource, "
136                                        + "LDP servers MUST replace the entire persistent state of "
137                                        + "the identified resource with the entity representation "
138                                        + "in the body of the request.")
139        @SpecTest(
140                        specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-put-replaceall",
141                        testMethod = METHOD.AUTOMATED,
142                        approval = STATUS.WG_APPROVED)
143        public void testPutReplacesResource() {
144                putReplaceResource(true);
145        }
146
147        @Test(
148                        groups = {MUST},
149                        description = "LDP servers SHOULD allow clients to update resources "
150                                        + "without requiring detailed knowledge of server-specific "
151                                        + "constraints. This is a consequence of the requirement to "
152                                        + "enable simple creation and modification of LDPRs.")
153        @SpecTest(
154                        specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-put-simpleupdate",
155                        testMethod = METHOD.AUTOMATED,
156                        approval = STATUS.WG_APPROVED)
157        public void testPutSimpleUpdate() {
158                putReplaceResource(false);
159        }
160
161        @Override
162        @Test(
163                        groups = {MUST},
164                        description = "LDP servers MUST provide an RDF representation "
165                                        + "for LDP-RSs. The HTTP Request-URI of the LDP-RS is "
166                                        + "typically the subject of most triples in the response.")
167        @SpecTest(
168                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-gen-rdf",
169                        testMethod = METHOD.AUTOMATED,
170                        approval = STATUS.WG_APPROVED)
171        public void testGetResource() {
172                // Make sure we can get the resource itself and the response is
173                // valid RDF. Turtle is a required media type, so this request
174                // should succeed for all LDP-RS.
175                buildBaseRequestSpecification()
176                                .header(ACCEPT, TEXT_TURTLE)
177                                .expect().statusCode(HttpStatus.SC_OK).contentType(HeaderMatchers.isTurtleCompatibleContentType())
178                                .when().get(getResourceUri()).as(Model.class, new RdfObjectMapper(getResourceUri()));
179        }
180
181        @Test(
182                        groups = {SHOULD},
183                        description = "LDP-RSs representations SHOULD have at least one "
184                                        + "rdf:type set explicitly. This makes the representations "
185                                        + "much more useful to client applications that don’t "
186                                        + "support inferencing.")
187        @SpecTest(
188                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-gen-atleast1rdftype",
189                        testMethod = METHOD.AUTOMATED,
190                        approval = STATUS.WG_APPROVED)
191        public void testContainsRdfType() {
192                Model containerModel = getAsModel(getResourceUri());
193                Resource r = getPrimaryTopic(containerModel, getResourceUri());
194                assertTrue(r.hasProperty(RDF.type), "LDP-RS representation has no explicit rdf:type");
195        }
196
197        @Test(
198                        groups = {MAY},
199                        description = "The representation of a LDP-RS MAY have an rdf:type "
200                                        + "of ldp:RDFSource for Linked Data Platform RDF Source.")
201        @SpecTest(
202                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-rdftype",
203                        testMethod = METHOD.AUTOMATED,
204                        approval = STATUS.WG_APPROVED)
205        public void testTypeRdfSource() {
206                Model containerModel = getAsModel(getResourceUri());
207                Resource r = containerModel.getResource(getResourceUri());
208                assertTrue(
209                                r.hasProperty(
210                                                RDF.type,
211                                                containerModel.createResource(LDP.RDFSource.stringValue())
212                                ),
213                                "LDP-RS representation does not have rdf:type ldp:RDFSource");
214        }
215
216        @Test(
217                        groups = {SHOULD, MANUAL},
218                        description = "LDP-RSs SHOULD reuse existing vocabularies instead of "
219                                        + "creating their own duplicate vocabulary terms. In addition "
220                                        + "to this general rule, some specific cases are covered by "
221                                        + "other conformance rules.")
222        @SpecTest(
223                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-gen-reusevocab",
224                        testMethod = METHOD.MANUAL,
225                        approval = STATUS.WG_APPROVED,
226                        steps = {"Given a URL for a RDF Source, perform a GET using an RDF content type",
227                                "Inspect the content to ensure standard terms are used.  For example, if things like "
228                                + "ex:label or ex:title are used, instead of DCTERMS or RDFS, then the test "
229                                + "should fail."})
230        public void testReUseVocabularies() {
231                throw new SkipNotTestableException(Thread.currentThread().getStackTrace()[1].getMethodName(), skipLog);
232        }
233
234        @Test(
235                        groups = {SHOULD, MANUAL},
236                        description = "LDP-RSs predicates SHOULD use standard vocabularies such "
237                                        + "as Dublin Core [DC-TERMS], RDF [rdf11-concepts] and RDF "
238                                        + "Schema [rdf-schema], whenever possible.")
239        @SpecTest(
240                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-gen-reusevocabsuchas",
241                        testMethod = METHOD.MANUAL,
242                        approval = STATUS.WG_APPROVED,
243                        steps = {"Given a URL for a RDF Source, perform a GET using an RDF content type",
244                                        "Inspect the content to ensure standard terms are used.  For example, if things like "
245                                        + "ex:label or ex:title are used, instead of DCTERMS or RDFS, then the test "
246                                        + "should fail."})
247        public void testUseStandardVocabularies() throws URISyntaxException {
248                // TODO: Consider ideas for testUseStandardVocabularies (see comment)
249                /* Possible ideas:
250                   fetch resource, look for known vocabulary term
251                   URIs.  Also can look at total number of terms and have a threshold
252                   of differences, say 100 terms and < 5 standard predicates would be odd.
253                   Also could look for similar/like short predicate short names or even
254                   look for owl:sameAs.
255                */
256                throw new SkipNotTestableException(Thread.currentThread().getStackTrace()[1].getMethodName(), skipLog);
257        }
258
259        @Test(
260                        groups = {MUST, MANUAL},
261                        description = "LDP servers MUST NOT require LDP clients to implement inferencing "
262                                        + "in order to recognize the subset of content defined by LDP. Other "
263                                        + "specifications built on top of LDP may require clients to implement "
264                                        + "inferencing [rdf11-concepts]. The practical implication is that all "
265                                        + "content defined by LDP must be explicitly represented, unless noted "
266                                        + "otherwise within this document.")
267        @SpecTest(
268                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-gen-noinferencing",
269                        testMethod = METHOD.MANUAL,
270                        approval = STATUS.WG_APPROVED,
271                        steps = {"Perform a GET on a URL for the reource requesting an RDF content type",
272                                        "Inspect the results (both headers and content) for missing terms.  Additionally "
273                                        + "could run an inferencing tool and compare results, seeing if needed information"
274                                        + "should have been explicitly listed by the server."})
275        public void testRestrictClientInference() {
276                throw new SkipNotTestableException(Thread.currentThread().getStackTrace()[1].getMethodName(), skipLog);
277        }
278
279        @Test(
280                        groups = {MUST},
281                        description = "LDP servers must respond with a Turtle representation of the "
282                                        + "requested LDP-RS when the request includes an Accept header specifying "
283                                        + "text/turtle, unless HTTP content negotiation requires a different outcome [turtle].")
284        @SpecTest(
285                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-get-turtle",
286                        testMethod = METHOD.AUTOMATED,
287                        approval = STATUS.WG_APPROVED)
288        public void testGetResourceAcceptTurtle() {
289                // Accept: text/turtle
290                buildBaseRequestSpecification().header(ACCEPT, TEXT_TURTLE)
291                                .expect().statusCode(isSuccessful()).contentType(HeaderMatchers.isTurtleCompatibleContentType())
292                                .when().get(getResourceUri()).as(Model.class, new RdfObjectMapper(getResourceUri()));
293
294                // More complicated Accept header
295                buildBaseRequestSpecification().header(ACCEPT, "text/turtle;q=0.9,application/json;q=0.8")
296                                .expect().statusCode(isSuccessful()).contentType(HeaderMatchers.isTurtleCompatibleContentType())
297                                .when().get(getResourceUri()).as(Model.class, new RdfObjectMapper(getResourceUri()));
298        }
299        
300        @Test(
301                        groups = {SHOULD},
302                        description = "LDP servers should respond with a text/turtle representation of the "
303                                        + "requested LDP-RS whenever the Accept request header is absent [turtle].")
304        @SpecTest(
305                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-get-conneg",
306                        testMethod = METHOD.AUTOMATED,
307                        approval = STATUS.WG_APPROVED)
308        public void testGetResourceAsTurtleNoAccept() {
309                // No Accept header
310                buildBaseRequestSpecification()
311                                .expect().statusCode(isSuccessful()).contentType(HeaderMatchers.isTurtleCompatibleContentType())
312                                .when().get(getResourceUri()).as(Model.class, new RdfObjectMapper(getResourceUri()));
313        }
314
315        @Test(
316                        groups = {MUST},
317                        description = " LDP servers must respond with a application/ld+json "
318                                        + "representation of the requested LDP-RS when the request "
319                                        + "includes an Accept header, unless content negotiation or "
320                                        + "Turtle support requires a different outcome [JSON-LD].")
321        @SpecTest(
322                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-get-jsonld",
323                        testMethod = METHOD.AUTOMATED,
324                        approval = STATUS.WG_APPROVED)
325        public void testJsonLdRepresentation() throws IOException, JsonLdError {
326                Response response = buildBaseRequestSpecification()
327                                .header(ACCEPT, "application/ld+json, application/json;q=0.5")
328                                .expect()
329                                .statusCode(isSuccessful())
330                                .contentType(HeaderMatchers.isJsonLdCompatibleContentType())
331                                .when()
332                                .get(getResourceUri());
333
334                // Make sure it parses as JSON-LD.
335                Object json = JsonUtils.fromInputStream(response.asInputStream());
336                JsonLdProcessor.toRDF(json); // throws JsonLdError if not valid
337        }
338
339        @Test(
340                        groups = {MUST},
341                        description = "LDP servers MUST publish any constraints on LDP clients’ "
342                                        + "ability to create or update LDPRs, by adding a Link header "
343                                        + "with rel='http://www.w3.org/ns/ldp#constrainedBy' [RFC5988] "
344                                        + "to all responses to requests which fail due to violation of "
345                                        + "those constraints.")
346        @Parameters({ "readOnlyProp" })
347        @SpecTest(
348                        specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-gen-pubclireqs",
349                        testMethod = METHOD.AUTOMATED,
350                        approval = STATUS.WG_APPROVED,
351                        comment = "Covers only part of the specification requirement. "
352                                        + "testPublishConstraintsUnknownProp covers the rest.")
353        public void testPublishConstraintsReadOnlyProp(@Optional String readOnlyProp) {
354                skipIfMethodNotAllowed(HttpMethod.PUT);
355
356                if (readOnlyProp == null) {
357                        throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(),
358                                        MSG_NO_READ_ONLY_PROPERTY, skipLog);
359                }
360
361                expectPut4xxConstrainedBy(readOnlyProp);
362        }
363
364        @Test(
365                        groups = {MUST},
366                        description = "LDP servers MUST publish any constraints on LDP clients’ "
367                                        + "ability to create or update LDPRs, by adding a Link header "
368                                        + "with rel='http://www.w3.org/ns/ldp#constrainedBy' [RFC5988] "
369                                        + "to all responses to requests which fail due to violation of "
370                                        + "those constraints.")
371        @SpecTest(
372                        specRefUri = LdpTestSuite.SPEC_URI + "#ldpr-gen-pubclireqs",
373                        testMethod = METHOD.AUTOMATED,
374                        approval = STATUS.WG_APPROVED,
375                        comment = "Covers only part of the specification requirement. "
376                                        + "testPublishConstraintsReadOnlyProp covers the rest.")
377        public void testPublishConstraintsUnknownProp() {
378                skipIfMethodNotAllowed(HttpMethod.PUT);
379                expectPut4xxConstrainedBy(UNKNOWN_PROPERTY);
380        }
381
382
383        @Test(
384                        groups = {MUST},
385                        description = "If an otherwise valid HTTP PUT request is received that "
386                                        + "attempts to change properties the server does not allow "
387                                        + "clients to modify, LDP servers MUST respond with a 4xx range "
388                                        + "status code (typically 409 Conflict)")
389        @Parameters("readOnlyProp")
390        @SpecTest(
391                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-put-servermanagedprops",
392                        testMethod = METHOD.AUTOMATED,
393                        approval = STATUS.WG_APPROVED,
394                        comment = "Covers only part of the specification requirement. "
395                                        + "test4xxErrorHasResponseBody covers the rest.")
396        public void testPutReadOnlyProperties4xxStatus(@Optional String readOnlyProp) {
397                skipIfMethodNotAllowed(HttpMethod.PUT);
398
399                if (readOnlyProp == null) {
400                        throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(),
401                                        MSG_NO_READ_ONLY_PROPERTY, skipLog);
402                }
403
404                expectPut4xxStatus(readOnlyProp);
405        }
406
407        @Test(
408                        groups = {SHOULD},
409                        description = "LDP servers SHOULD provide a corresponding response body containing "
410                                        + "information about which properties could not be persisted. The "
411                                        + "format of the 4xx response body is not constrained by LDP.")
412        @Parameters("readOnlyProp")
413        @SpecTest(
414                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-put-servermanagedprops",
415                        testMethod = METHOD.AUTOMATED,
416                        approval = STATUS.WG_APPROVED,
417                        comment = "Covers only part of the specification requirement. "
418                                        + "testPutReadOnlyProperties4xxStatus covers the rest.")
419        public void test4xxErrorHasResponseBody(@Optional String readOnlyProp) {
420                skipIfMethodNotAllowed(HttpMethod.PUT);
421
422                if (readOnlyProp == null) {
423                        throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(),
424                                        MSG_NO_READ_ONLY_PROPERTY, skipLog);
425                }
426
427                expectPut4xxResponseBody(readOnlyProp);
428        }
429
430        @Test(
431                        groups = {MUST},
432                        description = "If an otherwise valid HTTP PUT request is received that "
433                                        + "contains properties the server chooses not to persist, "
434                                        + "e.g. unknown content, LDP servers MUST respond with an "
435                                        + "appropriate 4xx range status code [HTTP11].")
436        @SpecTest(
437                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-put-failed",
438                        testMethod = METHOD.AUTOMATED,
439                        approval = STATUS.WG_APPROVED,
440                        comment = "Covers only part of the specification requirement. "
441                                        + "testResponsePropertiesNotPersisted covers the rest.")
442        public void testPutPropertiesNotPersisted() {
443                skipIfMethodNotAllowed(HttpMethod.PUT);
444                expectPut4xxStatus(UNKNOWN_PROPERTY);
445        }
446
447        @Test(
448                        groups = {SHOULD},
449                        description = "LDP servers SHOULD provide a corresponding response body containing "
450                                        + "information about which properties could not be persisted. The "
451                                        + "format of the 4xx response body is not constrained by LDP. LDP "
452                                        + "servers expose these application-specific constraints as described "
453                                        + "in section 4.2.1 General.")
454        @SpecTest(
455                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-put-failed",
456                        testMethod = METHOD.AUTOMATED,
457                        approval = STATUS.WG_APPROVED,
458                        comment = "Covers only part of the specification requirement. "
459                                        + "testPutPropertiesNotPersisted covers the rest.")
460        public void testResponsePropertiesNotPersisted() {
461                skipIfMethodNotAllowed(HttpMethod.PUT);
462                expectPut4xxResponseBody(UNKNOWN_PROPERTY);
463        }
464
465        @Test(
466                        groups = {MUST},
467                        description = "Each LDP RDF Source MUST also be a conforming LDP "
468                                        + "Resource as defined in section 4.2 Resource, along "
469                                        + "with the restrictions in this section.")
470        @SpecTest(
471                        specRefUri = LdpTestSuite.SPEC_URI + "#ldprs-are-ldpr",
472                        testMethod = METHOD.INDIRECT,
473                        approval = STATUS.WG_APPROVED,
474                        coveredByTests = {CommonResourceTest.class},
475                        coveredByGroups = {MUST})
476        public void testConformsRdfSourceLdpResource() {
477                throw new org.testng.SkipException("Covered indirectly by the MUST tests defined in CommonResourceTest class");
478        }
479
480        protected void modifyProperty(Model m, String resourceUri, String property) {
481                Resource r = getPrimaryTopic(m, resourceUri);
482                Property p = m.createProperty(property);
483                r.removeAll(p);
484                // Don't sweat the value or datatype since we expect the PUT to fail anyway.
485                r.addProperty(p, "modified");
486        }
487
488        protected void expectPut4xxConstrainedBy(String invalidProp) {
489                Response putResponse = expectPut4xxStatus(invalidProp);
490                final String uri = getResourceUri();
491                String constrainedBy = getFirstLinkForRelation(uri, LINK_REL_CONSTRAINEDBY, uri, putResponse);
492                assertNotNull(constrainedBy, "Response did not contain a Link header with rel=\"http://www.w3.org/ns/ldp#constrainedBy\"");
493
494                try {
495                        final URI linkUri = new URI(constrainedBy);
496
497                        if (linkUri.getScheme().startsWith("http")) {
498                                // Make sure we can GET the constrainedBy link.
499                                buildBaseRequestSpecification()
500                                                .expect()
501                                                        .statusCode(isSuccessful())
502                                                .when()
503                                                        .get(constrainedBy);
504                        }
505                } catch (URISyntaxException e) {
506                        throw new IllegalArgumentException(e);
507                }
508        }
509
510        protected Response expectPut4xxStatus(String invalidProp) {
511                // Get the resource.
512                String resourceUri = getResourceUri();
513                Response getResponse = buildBaseRequestSpecification()
514                                .header(ACCEPT, TEXT_TURTLE)
515                        .expect()
516                                .statusCode(isSuccessful())
517                                .header(ETAG, isValidEntityTag())
518                        .when()
519                                .get(resourceUri);
520
521                String eTag = getResponse.getHeader(ETAG);
522                Model m = getResponse.as(Model.class, new RdfObjectMapper(resourceUri));
523                modifyProperty(m, resourceUri, invalidProp);
524
525                Response putResponse = buildBaseRequestSpecification()
526                                .contentType(TEXT_TURTLE)
527                                .header(IF_MATCH, eTag)
528                                .body(m, new RdfObjectMapper(resourceUri))
529                        .when()
530                                .put(resourceUri);
531                if (isSuccessful().matches(putResponse.getStatusCode())) {
532                        throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(),
533                                        "Skipping test because PUT request was successful.", skipLog);
534                }
535
536                assertThat(putResponse.statusCode(), is4xxRange());
537
538                return putResponse;
539        }
540
541        protected void expectPut4xxResponseBody(String invalidProp) {
542                Response putResponse = expectPut4xxStatus(invalidProp);
543                assertThat(putResponse.body().asString(), not(isEmptyOrNullString()));
544        }
545
546        /**
547         * Are there any restrictions on the content of the resource being tested
548         * (i.e., the resource returned by {@link #getResourceUri()}). Should be
549         * true for LDP containers since PUT can't directly modify containment
550         * triples.
551         *
552         * <p>
553         * This method is used for {@link #testPutReplacesResource()} and
554         * {@link #testRelativeUriResolutionPut(String)}.
555         * </p>
556         *
557         * @return true if there are restrictions on what triples are allowed; false
558         *         otherwise
559
560         * @see #restrictionsOnPostContent()
561         */
562        protected boolean restrictionsOnTestResourceContent() {
563                // Should be the same as POST content unless this is a container.
564                // (Overridden by subclasses as necessary.)
565                return restrictionsOnPostContent();
566        }
567
568        protected void putReplaceResource(boolean continueOnError) {
569                skipIfMethodNotAllowed(HttpMethod.PUT);
570
571                if (restrictionsOnTestResourceContent()) {
572                        throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(),
573                                        MSG_PUT_RESTRICTIONS, skipLog);
574                }
575
576                String resourceUri = getResourceUri();
577                Response response = buildBaseRequestSpecification()
578                                                .header(ACCEPT, TEXT_TURTLE)
579                                .expect()
580                                                .statusCode(isSuccessful())
581                                                .header(ETAG, isValidEntityTag())
582                                .when()
583                                                .get(resourceUri);
584
585                String eTag = response.getHeader(ETAG);
586                Model originalModel = response.as(Model.class, new RdfObjectMapper(resourceUri));
587                Resource resource = getPrimaryTopic(originalModel, resourceUri);
588
589                assertNotNull(resource, "Expected to location resource in response for "+resourceUri);
590
591                // Update the model with updated title
592                resource.removeAll(DCTerms.title);
593                // Make sure the title is unique
594                final String UPDATED_TITLE = "This resources content has been replaced (" + System.currentTimeMillis() + ")";
595                originalModel.add(resource, DCTerms.title, UPDATED_TITLE);
596
597                response = buildBaseRequestSpecification()
598                                        .contentType(TEXT_TURTLE)
599                                        .header(IF_MATCH, eTag)
600                                        .body(originalModel, new RdfObjectMapper(resourceUri)) // relative URI
601                                .when()
602                                        .put(resourceUri);
603
604                if (!isSuccessful().matches(response.getStatusCode())) {
605                        if (continueOnError) {
606                                throw new SkipException(Thread.currentThread().getStackTrace()[1].getMethodName(),
607                                                "Skipping test because the PUT failed. The server may have restrictions on its content.",
608                                                skipLog);
609                        } else {
610                                throw new Failure("Unable to do simple update on resource, received code: "+response.getStatusLine());
611                        }
612                }
613
614                // Get the resource again to see what's there.
615                response = buildBaseRequestSpecification()
616                                .header(ACCEPT, TEXT_TURTLE)
617                        .expect()
618                                .statusCode(isSuccessful())
619                                .header(ETAG, isValidEntityTag())
620                        .when()
621                                .get(resourceUri);
622                eTag = response.getHeader(ETAG);
623                Model updatedModel = response.as(Model.class, new RdfObjectMapper(resourceUri));
624
625                // Make sure it's the only title (we removed all before PUTting)
626                Resource updatedResource = getPrimaryTopic(updatedModel, resourceUri);
627                StmtIterator titleProps = updatedResource.listProperties(DCTerms.title);
628                int titlePropSize = titleProps.toSet().size();
629                assertEquals(titlePropSize, 1, "Updated resource should only contain one dcterms:title changes but instead found "+titlePropSize+" changes");
630        }
631}