001/*
002 * Licensed to DuraSpace under one or more contributor license agreements.
003 * See the NOTICE file distributed with this work for additional information
004 * regarding copyright ownership.
005 *
006 * DuraSpace licenses this file to you under the Apache License,
007 * Version 2.0 (the "License"); you may not use this file except in
008 * compliance with the License.  You may obtain a copy of the License at
009 *
010 *     http://www.apache.org/licenses/LICENSE-2.0
011 *
012 * Unless required by applicable law or agreed to in writing, software
013 * distributed under the License is distributed on an "AS IS" BASIS,
014 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
015 * See the License for the specific language governing permissions and
016 * limitations under the License.
017 */
018package org.fcrepo.client.integration;
019
020import static javax.ws.rs.core.Response.Status.BAD_REQUEST;
021import static javax.ws.rs.core.Response.Status.CONFLICT;
022import static javax.ws.rs.core.Response.Status.CREATED;
023import static javax.ws.rs.core.Response.Status.GONE;
024import static javax.ws.rs.core.Response.Status.NOT_FOUND;
025import static javax.ws.rs.core.Response.Status.NOT_MODIFIED;
026import static javax.ws.rs.core.Response.Status.NO_CONTENT;
027import static javax.ws.rs.core.Response.Status.OK;
028import static javax.ws.rs.core.Response.Status.PARTIAL_CONTENT;
029import static javax.ws.rs.core.Response.Status.PRECONDITION_FAILED;
030import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION_FILENAME;
031import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_TYPE;
032import static org.fcrepo.client.FedoraHeaderConstants.ETAG;
033import static org.fcrepo.client.FedoraHeaderConstants.LAST_MODIFIED;
034import static org.fcrepo.client.TestUtils.TEXT_TURTLE;
035import static org.fcrepo.client.TestUtils.sparqlUpdate;
036import static org.junit.Assert.assertEquals;
037import static org.junit.Assert.assertNotEquals;
038import static org.junit.Assert.assertNotNull;
039import static org.junit.Assert.assertNull;
040import static org.junit.Assert.assertTrue;
041
042import java.io.ByteArrayInputStream;
043import java.io.InputStream;
044import java.net.URI;
045import java.util.Calendar;
046import java.util.Date;
047import java.util.Map;
048
049import javax.ws.rs.core.EntityTag;
050
051import org.apache.commons.io.IOUtils;
052import org.apache.http.client.utils.DateUtils;
053import org.fcrepo.client.FcrepoClient;
054import org.fcrepo.client.FcrepoOperationFailedException;
055import org.fcrepo.client.FcrepoResponse;
056import org.jgroups.util.UUID;
057import org.junit.Before;
058import org.junit.Test;
059import org.junit.runner.RunWith;
060import org.springframework.test.context.ContextConfiguration;
061import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
062
063/**
064 * @author bbpennel
065 */
066@RunWith(SpringJUnit4ClassRunner.class)
067@ContextConfiguration("/spring-test/test-container.xml")
068public class FcrepoClientIT extends AbstractResourceIT {
069
070    protected URI url;
071
072    final private String UNMODIFIED_DATE = "Mon, 1 Jan 2001 00:00:00 GMT";
073
074    public FcrepoClientIT() throws Exception {
075        super();
076
077        client = FcrepoClient.client()
078                .credentials("fedoraAdmin", "password")
079                .authScope("localhost")
080                .build();
081    }
082
083    @Before
084    public void before() {
085        url = URI.create(serverAddress + UUID.randomUUID().toString());
086    }
087
088    @Test
089    public void testPost() throws Exception {
090        final FcrepoResponse response = client.post(new URI(serverAddress))
091                .perform();
092
093        assertEquals(CREATED.getStatusCode(), response.getStatusCode());
094    }
095
096    @Test
097    public void testPostBinary() throws Exception {
098        final String slug = "hello1";
099        final String filename = "hello.txt";
100        final String mimetype = "text/plain";
101        final String bodyContent = "Hello world";
102        final FcrepoResponse response = client.post(new URI(serverAddress))
103                .body(new ByteArrayInputStream(bodyContent.getBytes()), mimetype)
104                .filename(filename)
105                .slug(slug)
106                .perform();
107
108        final String content = IOUtils.toString(response.getBody(), "UTF-8");
109        final int status = response.getStatusCode();
110
111        assertEquals("Didn't get a CREATED response! Got content:\n" + content,
112                CREATED.getStatusCode(), status);
113        assertEquals("Location did not match slug", serverAddress + slug, response.getLocation().toString());
114
115        assertNotNull("Didn't find linked description!", response.getLinkHeaders("describedby").get(0));
116
117        final FcrepoResponse getResponse = client.get(response.getLocation()).perform();
118        final Map<String, String> contentDisp = getResponse.getContentDisposition();
119        assertEquals(filename, contentDisp.get(CONTENT_DISPOSITION_FILENAME));
120
121        assertEquals(mimetype, getResponse.getContentType());
122
123        final String getContent = IOUtils.toString(getResponse.getBody(), "UTF-8");
124        assertEquals(bodyContent, getContent);
125    }
126
127    @Test
128    public void testPostDigestMismatch() throws Exception {
129        final String bodyContent = "Hello world";
130        final String invalidDigest = "adc83b19e793491b1c6ea0fd8b46cd9f32e592fc";
131
132        final FcrepoResponse response = client.post(new URI(serverAddress))
133                .body(new ByteArrayInputStream(bodyContent.getBytes()), "text/plain")
134                .digestSha1(invalidDigest)
135                .perform();
136
137        assertEquals("Invalid checksum was not rejected", CONFLICT.getStatusCode(), response.getStatusCode());
138    }
139
140    @Test
141    public void testPostDigestMultipleChecksums() throws Exception {
142        final String bodyContent = "Hello world";
143
144        final FcrepoResponse response = client.post(new URI(serverAddress))
145                .body(new ByteArrayInputStream(bodyContent.getBytes()), "text/plain")
146                .digestMd5("3e25960a79dbc69b674cd4ec67a72c62")
147                .digestSha1("7b502c3a1f48c8609ae212cdfb639dee39673f5e")
148                .digestSha256("64ec88ca00b268e5ba1a35678a1b5316d212f4f366b2477232534a8aeca37f3c")
149                .perform();
150
151        assertEquals("Checksums rejected", CREATED.getStatusCode(), response.getStatusCode());
152    }
153
154    @Test
155    public void testPostDigestMultipleChecksumsOneMismatch() throws Exception {
156        final String bodyContent = "Hello world";
157
158        final FcrepoResponse response = client.post(new URI(serverAddress))
159                .body(new ByteArrayInputStream(bodyContent.getBytes()), "text/plain")
160                .digestMd5("3e25960a79dbc69b674cd4ec67a72c62")
161                .digestSha1("7b502c3a1f48c8609ae212cdfb639dee39673f5e")
162                // Incorrect sha256
163                .digestSha256("123488ca00b268e5ba1a35678a1b5316d212f4f366b2477232534a8aeca37f3c")
164                .perform();
165
166        assertEquals("Invalid checksum was not rejected", CONFLICT.getStatusCode(), response.getStatusCode());
167    }
168
169    @Test
170    public void testPut() throws Exception {
171        final FcrepoResponse response = create();
172
173        assertEquals(CREATED.getStatusCode(), response.getStatusCode());
174        assertEquals(url, response.getLocation());
175    }
176
177    @Test
178    public void testPutEtag() throws Exception {
179        // Create object
180        final FcrepoResponse response = create();
181
182        // Get the etag of the nearly created object
183        final EntityTag etag = EntityTag.valueOf(response.getHeaderValue(ETAG));
184
185        // Retrieve the body of the resource so we can modify it
186        String body = getTurtle(url);
187        body += "\n<> dc:title \"some-title\"";
188
189        // Check that etag is making it through and being rejected
190        final FcrepoResponse updateResp = client.put(url)
191                .body(new ByteArrayInputStream(body.getBytes()), TEXT_TURTLE)
192                .ifMatch("\"bad-etag\"")
193                .perform();
194
195        assertEquals(PRECONDITION_FAILED.getStatusCode(), updateResp.getStatusCode());
196
197        // Verify that etag is retrieved and resubmitted correctly
198        final FcrepoResponse validResp = client.put(url)
199                .body(new ByteArrayInputStream(body.getBytes()), TEXT_TURTLE)
200                .ifMatch("\"" + etag.getValue() + "\"")
201                .perform();
202        assertEquals(NO_CONTENT.getStatusCode(), validResp.getStatusCode());
203    }
204
205    @Test
206    public void testPutUnmodifiedSince() throws Exception {
207        // Create object
208        final FcrepoResponse response = create();
209
210        // Retrieve the body of the resource so we can modify it
211        String body = getTurtle(url);
212        body += "\n<> dc:title \"some-title\"";
213
214        // Update the body the first time, which should succeed
215        final String originalModified = response.getHeaderValue(LAST_MODIFIED);
216        final FcrepoResponse matchResponse = client.put(url)
217                .body(new ByteArrayInputStream(body.getBytes()), TEXT_TURTLE)
218                .ifUnmodifiedSince(originalModified)
219                .perform();
220
221        assertEquals(NO_CONTENT.getStatusCode(), matchResponse.getStatusCode());
222
223        // Update the triples a second time with old timestamp
224        final FcrepoResponse mismatchResponse = client.put(url)
225                .body(new ByteArrayInputStream(body.getBytes()), TEXT_TURTLE)
226                .ifUnmodifiedSince(UNMODIFIED_DATE)
227                .perform();
228
229        assertEquals(PRECONDITION_FAILED.getStatusCode(), mismatchResponse.getStatusCode());
230    }
231
232    @Test
233    public void testPutLenient() throws Exception {
234        // Create object
235        create();
236        final String body = "<> <http://purl.org/dc/elements/1.1/title> \"some-title\"";
237
238        // try to update without lenient header
239        final FcrepoResponse strictResponse = client.put(url)
240                .body(new ByteArrayInputStream(body.getBytes()), TEXT_TURTLE)
241                .perform();
242        assertEquals(CONFLICT.getStatusCode(), strictResponse.getStatusCode());
243
244        // try again with lenient header
245        final FcrepoResponse lenientResponse = client.put(url)
246                .body(new ByteArrayInputStream(body.getBytes()), TEXT_TURTLE)
247                .preferLenient()
248                .perform();
249        assertEquals(NO_CONTENT.getStatusCode(), lenientResponse.getStatusCode());
250    }
251
252    @Test
253    public void testPatch() throws Exception {
254        // Create object
255        create();
256
257        final InputStream body = new ByteArrayInputStream(sparqlUpdate.getBytes());
258
259        // Update triples with sparql update
260        final FcrepoResponse response = client.patch(url)
261                .body(body)
262                .perform();
263
264        assertEquals(NO_CONTENT.getStatusCode(), response.getStatusCode());
265    }
266
267    @Test
268    public void testPatchEtagUpdated() throws Exception {
269        // Create object
270        final FcrepoResponse createResp = create();
271        final EntityTag createdEtag = EntityTag.valueOf(createResp.getHeaderValue(ETAG));
272
273        final InputStream body = new ByteArrayInputStream(sparqlUpdate.getBytes());
274
275        // Update triples with sparql update
276        final FcrepoResponse response = client.patch(url)
277                .body(body)
278                .ifMatch("\"" + createdEtag.getValue() + "\"")
279                .perform();
280
281        final EntityTag updateEtag = EntityTag.valueOf(response.getHeaderValue(ETAG));
282
283        assertEquals(NO_CONTENT.getStatusCode(), response.getStatusCode());
284        assertNotEquals("Etag did not change after patch", createdEtag, updateEtag);
285    }
286
287    @Test
288    public void testPatchNoBody() throws Exception {
289        create();
290
291        final FcrepoResponse response = client.patch(url)
292                .perform();
293
294        assertEquals(BAD_REQUEST.getStatusCode(), response.getStatusCode());
295    }
296
297    @Test
298    public void testDelete() throws Exception {
299        create();
300
301        final FcrepoResponse response = client.delete(url).perform();
302
303        assertEquals(NO_CONTENT.getStatusCode(), response.getStatusCode());
304
305        assertEquals(GONE.getStatusCode(), client.get(url).perform().getStatusCode());
306    }
307
308    @Test
309    public void testGet() throws Exception {
310        create();
311        final FcrepoResponse response = client.get(url).perform();
312
313        assertEquals(OK.getStatusCode(), response.getStatusCode());
314    }
315
316    @Test
317    public void testGetNotFound() throws Exception {
318        final FcrepoResponse response = client.get(url).perform();
319
320        assertEquals(NOT_FOUND.getStatusCode(), response.getStatusCode());
321    }
322
323    @Test
324    public void testGetUnmodified() throws Exception {
325        // Check that get returns a 304 if the item hasn't changed according to last-modified
326        final FcrepoResponse response = create();
327
328        // Get tomorrows date to provide as the modified-since date
329        final String lastModified = response.getHeaderValue(LAST_MODIFIED);
330        final Date modDate = DateUtils.parseDate(lastModified);
331        final Calendar cal = Calendar.getInstance();
332        cal.setTime(modDate);
333        cal.add(Calendar.DATE, 1);
334
335        final FcrepoResponse modResp = client.get(url)
336                .ifModifiedSince(DateUtils.formatDate(cal.getTime()))
337                .perform();
338
339        assertEquals(NOT_MODIFIED.getStatusCode(), modResp.getStatusCode());
340        assertNull("Response body should not be returned when unmodified", modResp.getBody());
341    }
342
343    @Test
344    public void testGetModified() throws Exception {
345        // Check that get returns a 200 if the item has changed according to last-modified
346        final FcrepoResponse response = create();
347
348        // Get yesterdays date to provide as the modified-since date
349        final String lastModified = response.getHeaderValue(LAST_MODIFIED);
350        final Date modDate = DateUtils.parseDate(lastModified);
351        final Calendar cal = Calendar.getInstance();
352        cal.setTime(modDate);
353        cal.add(Calendar.DATE, -1);
354
355        final FcrepoResponse modResp = client.get(url)
356                .ifModifiedSince(DateUtils.formatDate(cal.getTime()))
357                .perform();
358
359        assertEquals(OK.getStatusCode(), modResp.getStatusCode());
360        assertNotNull("GET response body should be normal when modified", modResp.getBody());
361    }
362
363    @Test
364    public void testGetAccept() throws Exception {
365        // Check that get returns a 304 if the item hasn't changed according to last-modified/etag
366        create();
367
368        final FcrepoResponse response = client.get(url)
369                .accept("application/n-triples")
370                .perform();
371
372        assertEquals("application/n-triples", response.getHeaderValue(CONTENT_TYPE));
373        assertEquals(OK.getStatusCode(), response.getStatusCode());
374    }
375
376    @Test
377    public void testGetPrefer() throws Exception {
378        // Check that get returns a 304 if the item hasn't changed according to last-modified/etag
379        create();
380
381        final FcrepoResponse response = client.get(url)
382                .preferMinimal()
383                .perform();
384
385        assertEquals(OK.getStatusCode(), response.getStatusCode());
386        assertEquals("return=minimal", response.getHeaderValue("Preference-Applied"));
387    }
388
389    @Test
390    public void testGetRange() throws Exception {
391        // Creating a binary for retrieval
392        final String mimetype = "text/plain";
393        final String bodyContent = "Hello world";
394        final FcrepoResponse response = client.post(new URI(serverAddress))
395                .body(new ByteArrayInputStream(bodyContent.getBytes()), mimetype)
396                .perform();
397
398        final URI url = response.getLocation();
399
400        // Get the content of the object after the first 6 bytes
401        final FcrepoResponse rangeResp = client.get(url)
402                .range(6L, null)
403                .perform();
404
405        final String content = IOUtils.toString(rangeResp.getBody(), "UTF-8");
406        assertEquals("Body did not contain correct range of original content", "world", content);
407        assertEquals(PARTIAL_CONTENT.getStatusCode(), rangeResp.getStatusCode());
408    }
409
410    @Test
411    public void testGetDisableRedirects() throws Exception {
412        // Creating a binary with external content for retrieval
413        final String mimetype = "message/external-body; access-type=URL; URL=\"http://www.example.com/file\"";
414        final FcrepoResponse response = client.post(new URI(serverAddress))
415                .body(new ByteArrayInputStream(new byte[]{}), mimetype)
416                .perform();
417
418        final URI url = response.getLocation();
419
420        // Make sure the response is the redirect itself, not the URL being redirected to
421        final FcrepoResponse getResponse = client.get(url).disableRedirects().perform();
422        assertEquals(307, getResponse.getStatusCode());
423        assertEquals(url, getResponse.getUrl());
424        assertEquals(URI.create("http://www.example.com/file"), getResponse.getLocation());
425    }
426
427    @Test
428    public void testHead() throws Exception {
429        final FcrepoResponse response = create();
430        final FcrepoResponse headResp = client.head(url).perform();
431
432        assertEquals(OK.getStatusCode(), headResp.getStatusCode());
433        assertEquals(response.getHeaderValue(ETAG), headResp.getHeaderValue(ETAG));
434        assertNotNull(headResp.getHeaderValue("Allow"));
435    }
436
437    @Test
438    public void testOptions() throws Exception {
439        create();
440        final FcrepoResponse headResp = client.options(url).perform();
441
442        assertEquals(OK.getStatusCode(), headResp.getStatusCode());
443        assertNotNull(headResp.getHeaderValue("Allow"));
444        assertNotNull(headResp.getHeaderValue("Accept-Post"));
445        assertNotNull(headResp.getHeaderValue("Accept-Patch"));
446    }
447
448    @Test
449    public void testMove() throws Exception {
450        create();
451
452        final URI destUrl = new URI(url.toString() + "_dest");
453        final FcrepoResponse moveResp = client.move(url, destUrl).perform();
454        assertEquals(CREATED.getStatusCode(), moveResp.getStatusCode());
455
456        assertEquals("Object still at original url",
457                GONE.getStatusCode(), client.get(url).perform().getStatusCode());
458
459        assertEquals("Object not at expected new url",
460                OK.getStatusCode(), client.get(destUrl).perform().getStatusCode());
461    }
462
463    @Test
464    public void testCopy() throws Exception {
465        create();
466
467        // Add something identifiable to the record
468        final InputStream body = new ByteArrayInputStream(sparqlUpdate.getBytes());
469        client.patch(url).body(body).perform();
470
471        final URI destUrl = new URI(url.toString() + "_dest");
472        final FcrepoResponse copyResp = client.copy(url, destUrl).perform();
473        assertEquals(CREATED.getStatusCode(), copyResp.getStatusCode());
474
475        final FcrepoResponse originalResp = client.get(url).perform();
476        final String originalContent = IOUtils.toString(originalResp.getBody(), "UTF-8");
477        assertTrue(originalContent.contains("Foo"));
478
479        final FcrepoResponse destResp = client.get(destUrl).perform();
480        final String destContent = IOUtils.toString(destResp.getBody(), "UTF-8");
481        assertTrue(destContent.contains("Foo"));
482    }
483
484    private FcrepoResponse create() throws FcrepoOperationFailedException {
485        return client.put(url).perform();
486    }
487
488    private String getTurtle(final URI url) throws Exception {
489        final FcrepoResponse getResponse = client.get(url)
490                .accept("text/turtle")
491                .perform();
492        return IOUtils.toString(getResponse.getBody(), "UTF-8");
493    }
494}