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