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