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