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;
019
020import static java.net.URI.create;
021import static java.nio.charset.StandardCharsets.UTF_8;
022import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION;
023import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION_FILENAME;
024import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION_MODIFICATION_DATE;
025import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION_SIZE;
026import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_TYPE;
027import static org.fcrepo.client.FedoraHeaderConstants.LINK;
028import static org.fcrepo.client.FedoraHeaderConstants.LOCATION;
029import static org.fcrepo.client.LinkHeaderConstants.TYPE_REL;
030import static org.fcrepo.client.LinkHeaderConstants.DESCRIBEDBY_REL;
031import static org.fcrepo.client.FedoraTypes.MEMENTO_ORIGINAL_TYPE;
032import static org.junit.Assert.assertEquals;
033import static org.junit.Assert.assertFalse;
034import static org.junit.Assert.assertSame;
035import static org.junit.Assert.assertTrue;
036import static org.junit.Assert.fail;
037import static org.mockito.ArgumentMatchers.any;
038import static org.mockito.Mockito.doThrow;
039import static org.mockito.Mockito.mock;
040import static org.mockito.Mockito.times;
041import static org.mockito.Mockito.verify;
042import static org.mockito.Mockito.when;
043
044import java.io.ByteArrayInputStream;
045import java.io.IOException;
046import java.io.InputStream;
047import java.net.URI;
048import java.util.Arrays;
049import java.util.HashMap;
050import java.util.List;
051import java.util.Map;
052
053import org.apache.commons.io.IOUtils;
054import org.apache.commons.io.output.NullOutputStream;
055import org.junit.Test;
056import org.junit.runner.RunWith;
057import org.mockito.junit.MockitoJUnitRunner;
058
059import com.google.common.io.ByteStreams;
060
061/**
062 * @author ajs6f
063 */
064@RunWith(MockitoJUnitRunner.class)
065public class FcrepoResponseTest {
066
067    @Test
068    public void testResponse() throws IOException {
069        final URI uri = create("http://localhost/path/a/b");
070        final int status = 200;
071        final Map<String, List<String>> headers = new HashMap<>();
072        final String contentType = "text/plain";
073        headers.put(CONTENT_TYPE, Arrays.asList(contentType));
074        final String location = "http://localhost/path/a/b/c";
075        headers.put(LOCATION, Arrays.asList(location));
076        final String body = "Text response";
077        final InputStream bodyStream = new ByteArrayInputStream(body.getBytes(UTF_8));
078        final FcrepoResponse response = new FcrepoResponse(uri, status, headers, bodyStream);
079
080        assertEquals(response.getUrl(), uri);
081        assertEquals(response.getStatusCode(), status);
082        assertEquals(response.getContentType(), contentType);
083        assertEquals(response.getLocation(), create(location));
084        assertEquals(IOUtils.toString(response.getBody(), UTF_8), body);
085
086        response.setUrl(create("http://example.org/path/a/b"));
087        assertEquals(response.getUrl(), create("http://example.org/path/a/b"));
088
089        response.setStatusCode(301);
090        assertEquals(response.getStatusCode(), 301);
091
092        response.setContentType("application/n-triples");
093        assertEquals(response.getContentType(), "application/n-triples");
094
095        response.setLocation(create("http://example.org/path/a/b/c"));
096        assertEquals(response.getLocation(), create("http://example.org/path/a/b/c"));
097
098        response.setBody(new ByteArrayInputStream(
099                "<http://example.org/book/3> <dc:title> \"Title\" .".getBytes(UTF_8)));
100        assertEquals(IOUtils.toString(response.getBody(), UTF_8),
101                "<http://example.org/book/3> <dc:title> \"Title\" .");
102        response.close();
103    }
104
105    /**
106     * Demonstrates that response objects are <em>not</em> {@code close()}ed by default, that the state of
107     * {@link FcrepoResponse#closed} is set appropriately when {@link FcrepoResponse#close()} is invoked under normal
108     * (i.e. no exception thrown during {@code close()}) conditions, and that {@link InputStream#close()} is not
109     * invoked repeatedly after the {@code FcrepoResponse} has been {@code close()}ed.
110     *
111     * @throws IOException if something exceptional happens
112     */
113    @Test
114    public void testClosableReleasesResources() throws IOException {
115        final InputStream mockBody = mock(InputStream.class);
116        final Map<String, List<String>> headers = new HashMap<>();
117        final String contentType = "text/plain";
118        headers.put(CONTENT_TYPE, Arrays.asList(contentType));
119        final String location = "http://localhost/bar";
120        headers.put(LOCATION, Arrays.asList(location));
121        final FcrepoResponse underTest = new FcrepoResponse(
122                URI.create("http://localhost/foo"), 201, headers, mockBody);
123
124        assertFalse("FcrepoResponse objects should not be closed until close() is invoked.", underTest.isClosed());
125
126        underTest.close();
127        assertTrue(underTest.isClosed());
128        verify(mockBody, times(1)).close();
129
130        underTest.close();
131        assertTrue(underTest.isClosed());
132        verify(mockBody, times(1)).close();
133    }
134
135    /**
136     * Demonstrates that if an {@code IOException} is thrown by {@link FcrepoResponse#close()}, <em>and</em> an
137     * exception is thrown inside of a client's {@code try} block, the {@code IOException} from the {@code close()}
138     * method is properly appended as a suppressed exception.
139     *
140     * @throws IOException if something exceptional happens
141     */
142    @Test
143    public void testClosableSuppressedExceptions() throws IOException {
144        final InputStream mockBody = mock(InputStream.class);
145        final IOException notSuppressed = new IOException("Not suppressed.");
146        final IOException suppressed = new IOException("Suppressed");
147        doThrow(suppressed).when(mockBody).close();
148
149        final Map<String, List<String>> headers = new HashMap<>();
150        final String contentType = "text/plain";
151        headers.put(CONTENT_TYPE, Arrays.asList(contentType));
152        final String location = "http://localhost/bar";
153        headers.put(LOCATION, Arrays.asList(location));
154
155        try (FcrepoResponse underTest = new FcrepoResponse(URI.create("http://localhost/foo"), 201,
156                headers, mockBody)) {
157            assertFalse(underTest.isClosed());
158
159            throw notSuppressed;
160
161        } catch (final Exception e) {
162            assertSame(notSuppressed, e);
163            assertTrue(e.getSuppressed() != null && e.getSuppressed().length == 1);
164            assertSame(suppressed, e.getSuppressed()[0]);
165        }
166
167        verify(mockBody).close();
168    }
169
170    /**
171     * Demonstrates a successful idiomatic usage with try-with-resources
172     *
173     * @throws FcrepoOperationFailedException if something exceptional happens
174     */
175    @Test
176    public void testIdiomaticInvokation() throws FcrepoOperationFailedException {
177        final String content = "Hello World!";
178        final ByteArrayInputStream entityBody = new ByteArrayInputStream(content.getBytes());
179        final FcrepoClient client = mock(FcrepoClient.class);
180        final GetBuilder getBuilder = mock(GetBuilder.class);
181
182        when(client.get(any(URI.class))).thenReturn(getBuilder);
183        when(getBuilder.perform()).thenReturn(new FcrepoResponse(null, 200, null, entityBody));
184
185        try (FcrepoResponse res = client.get(URI.create("foo")).perform()) {
186            assertEquals(content, IOUtils.toString(res.getBody(), "UTF-8"));
187        } catch (final IOException e) {
188            fail("Unexpected exception: " + e);
189        }
190    }
191
192    /**
193     * Demonstrates idiomatic exception handling with try-with-resources
194     *
195     * @throws Exception if something exceptional happens
196     */
197    @Test
198    public void testIdiomaticInvokationThrowsException() throws Exception {
199        final InputStream mockBody = mock(InputStream.class);
200        final IOException ioe = new IOException("Mocked IOE");
201        when(mockBody.read(any(byte[].class))).thenThrow(ioe);
202
203        final FcrepoClient client = mock(FcrepoClient.class);
204        final GetBuilder getBuilder = mock(GetBuilder.class);
205
206        when(client.get(any(URI.class))).thenReturn(getBuilder);
207        when(getBuilder.perform()).thenReturn(new FcrepoResponse(null, 200, null, mockBody));
208
209        try (FcrepoResponse res = client.get(URI.create("foo")).perform()) {
210            ByteStreams.copy(res.getBody(), NullOutputStream.NULL_OUTPUT_STREAM);
211            fail("Expected an IOException to be thrown.");
212        } catch (final IOException e) {
213            assertSame(ioe, e);
214        }
215
216        verify(mockBody).close();
217    }
218
219    @Test
220    public void testLocationFromDescribedBy() throws Exception {
221        final Map<String, List<String>> headers = new HashMap<>();
222        final String contentType = "text/plain";
223        headers.put(CONTENT_TYPE, Arrays.asList(contentType));
224        final String describedBy = "http://localhost/bar/file/fcr:metadata";
225        headers.put(LINK, Arrays.asList(
226                "<http://www.w3.org/ns/ldp#Resource>;rel=\"type\"",
227                "<" + describedBy + ">; rel=\"describedby\""));
228
229        try (FcrepoResponse response = new FcrepoResponse(URI.create("http://localhost/foo"), 201,
230                headers, null)) {
231            assertEquals(create(describedBy), response.getLocation());
232        }
233    }
234
235    @Test
236    public void testLocationOverDescribedBy() throws Exception {
237        final Map<String, List<String>> headers = new HashMap<>();
238        final String contentType = "text/plain";
239        headers.put(CONTENT_TYPE, Arrays.asList(contentType));
240        final String location = "http://localhost/bar/file";
241        headers.put(LOCATION, Arrays.asList(location));
242        final String describedBy = "http://localhost/bar/file/fcr:metadata";
243        headers.put(LINK, Arrays.asList(
244                "<http://www.w3.org/ns/ldp#Resource>;rel=\"type\"",
245                "<" + describedBy + ">; rel=\"describedby\""));
246
247        try (FcrepoResponse response = new FcrepoResponse(URI.create("http://localhost/foo"), 201,
248                headers, null)) {
249            assertEquals(create(location), response.getLocation());
250            assertEquals(describedBy, response.getLinkHeaders(DESCRIBEDBY_REL).get(0).toString());
251        }
252    }
253
254    @Test
255    public void testContentDisposition() throws Exception {
256        final Map<String, List<String>> headers = new HashMap<>();
257        final String filename = "file.txt";
258        final String createDate = "Fri, 10 Jun 2016 14:52:46 GMT";
259        final String modDate = "Fri, 10 Jun 2016 18:52:46 GMT";
260        final long size = 5320;
261
262        headers.put(CONTENT_DISPOSITION, Arrays.asList("attachment; filename=\"" + filename + "\";" +
263                " creation-date=\"" + createDate + "\";" +
264                " modification-date=\"" + modDate + "\";" +
265                " size=" + size));
266
267        try (FcrepoResponse response = new FcrepoResponse(URI.create("http://localhost/foo"), 201,
268                headers, null)) {
269            final Map<String, String> disp = response.getContentDisposition();
270            assertEquals(disp.get(CONTENT_DISPOSITION_FILENAME), filename);
271            assertEquals(disp.get(CONTENT_DISPOSITION_MODIFICATION_DATE), modDate);
272            assertEquals(Long.parseLong(disp.get(CONTENT_DISPOSITION_SIZE)), size);
273        }
274    }
275
276    @Test
277    public void testHasType() throws Exception {
278        final Map<String, List<String>> headers = new HashMap<>();
279        final FcrepoLink typeLink = FcrepoLink.fromUri(MEMENTO_ORIGINAL_TYPE).rel(TYPE_REL).build();
280        headers.put(LINK, Arrays.asList(typeLink.toString()));
281
282        final URI uri = URI.create("http://localhost/foo");
283        try (final FcrepoResponse response = new FcrepoResponse(uri, 201, headers, new ByteArrayInputStream(""
284                .getBytes()))) {
285            assertTrue(response.hasType(MEMENTO_ORIGINAL_TYPE));
286            assertTrue(response.hasType(URI.create(MEMENTO_ORIGINAL_TYPE)));
287
288            assertFalse(response.hasType("http://example.com/some_type"));
289            assertFalse(response.hasType(URI.create("http://example.com/some_type")));
290        }
291    }
292}