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.util.Collections.emptyList;
021import static java.util.stream.Collectors.toList;
022import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION;
023import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_TYPE;
024import static org.fcrepo.client.FedoraHeaderConstants.DESCRIBED_BY;
025import static org.fcrepo.client.FedoraHeaderConstants.LINK;
026import static org.fcrepo.client.FedoraHeaderConstants.LOCATION;
027
028import java.io.Closeable;
029import java.io.IOException;
030import java.io.InputStream;
031import java.net.URI;
032import java.util.HashMap;
033import java.util.List;
034import java.util.Map;
035import org.apache.http.HeaderElement;
036import org.apache.http.NameValuePair;
037import org.apache.http.message.BasicHeader;
038
039/**
040 * Represents a response from a fedora repository using a {@link FcrepoClient}.
041 * <p>
042 * This class implements {@link Closeable}. Suggested usage is to create the {@code FcrepoResponse} within a
043 * try-with-resources block, insuring that any resources held by the response are freed automatically.
044 * </p>
045 * <pre>
046 * FcrepoClient client = ...;
047 * try (FcrepoResponse res = client.get(...)) {
048 *     // do something with the response
049 * } catch (FcrepoOperationFailedException|IOException e) {
050 *     // handle any exceptions
051 * }
052 * </pre> Closed responses have no obligation to provide access to released resources.
053 *
054 * @author Aaron Coburn
055 * @since October 20, 2014
056 */
057public class FcrepoResponse implements Closeable {
058
059    private URI url;
060
061    private int statusCode;
062
063    private URI location;
064
065    private Map<String, List<String>> headers;
066
067    private Map<String, String> contentDisposition;
068
069    private InputStream body;
070
071    private String contentType;
072
073    private boolean closed = false;
074
075    /**
076     * Create a FcrepoResponse object from the http response
077     *
078     * @param url the requested URL
079     * @param statusCode the HTTP status code
080     * @param headers a map of all response header names and values
081     * @param body the response body stream
082     */
083    public FcrepoResponse(final URI url, final int statusCode, final Map<String, List<String>> headers,
084            final InputStream body) {
085        this.setUrl(url);
086        this.setStatusCode(statusCode);
087        this.setHeaders(headers);
088        this.setBody(body);
089    }
090
091    /**
092     * {@inheritDoc}
093     * <p>
094     * Implementation note: Invoking this method will close the underlying {@code InputStream} containing the entity
095     * body of the HTTP response.
096     * </p>
097     *
098     * @throws IOException if there is an error closing the underlying HTTP response stream.
099     */
100    @Override
101    public void close() throws IOException {
102        if (!this.closed && this.body != null) {
103            try {
104                this.body.close();
105            } finally {
106                this.closed = true;
107            }
108        }
109    }
110
111    /**
112     * Whether or not the resources have been freed from this response. There should be no expectation that a closed
113     * response provides access to the {@link #getBody() entity body}.
114     *
115     * @return {@code true} if resources have been freed, otherwise {@code false}
116     */
117    public boolean isClosed() {
118        return closed;
119    }
120
121    /**
122     * url getter
123     *
124     * @return the requested URL
125     */
126    public URI getUrl() {
127        return url;
128    }
129
130    /**
131     * url setter
132     *
133     * @param url the requested URL
134     */
135    public void setUrl(final URI url) {
136        this.url = url;
137    }
138
139    /**
140     * statusCode getter
141     *
142     * @return the HTTP status code
143     */
144    public int getStatusCode() {
145        return statusCode;
146    }
147
148    /**
149     * statusCode setter
150     *
151     * @param statusCode the HTTP status code
152     */
153    public void setStatusCode(final int statusCode) {
154        this.statusCode = statusCode;
155    }
156
157    /**
158     * body getter
159     *
160     * @return the response body as a stream
161     */
162    public InputStream getBody() {
163        return body;
164    }
165
166    /**
167     * body setter
168     *
169     * @param body the contents of the response body
170     */
171    public void setBody(final InputStream body) {
172        this.body = body;
173    }
174
175    /**
176     * headers getter
177     *
178     * @return headers from the response
179     */
180    public Map<String, List<String>> getHeaders() {
181        return this.headers;
182    }
183
184    /**
185     * Get all values for the specified header
186     *
187     * @param name name of the header to retrieve
188     * @return All values of the specified header
189     */
190    public List<String> getHeaderValues(final String name) {
191        return headers == null ? emptyList() : headers.getOrDefault(name, emptyList());
192    }
193
194    /**
195     * Get the first value for the specified header
196     *
197     * @param name name of the header to retrieve
198     * @return First value of the header, or null if not present
199     */
200    public String getHeaderValue(final String name) {
201        final List<String> values = getHeaderValues(name);
202        if (values == null || values.size() == 0) {
203            return null;
204        }
205
206        return values.get(0);
207    }
208
209    /**
210     * headers setter
211     *
212     * @param headers headers from the response
213     */
214    public void setHeaders(final Map<String, List<String>> headers) {
215        this.headers = headers;
216    }
217
218    /**
219     * Retrieve link header values matching the given relationship
220     *
221     * @param relationship the relationship of links to return
222     * @return list of link header URIs matching the given relationship
223     */
224    public List<URI> getLinkHeaders(final String relationship) {
225        return getHeaderValues(LINK).stream().map(FcrepoLink::new).filter(link -> link.getRel().equals(relationship))
226                .map(FcrepoLink::getUri).collect(toList());
227    }
228
229    /**
230     * location getter
231     *
232     * @return the location of a related resource
233     */
234    public URI getLocation() {
235        if (location == null && headers != null) {
236            // Retrieve the value from the location header if available
237            final String value = getHeaderValue(LOCATION);
238            if (value != null) {
239                location = URI.create(getHeaderValue(LOCATION));
240            }
241            // Fall back to retrieving from the described by link
242            if (location == null) {
243                final List<URI> links = getLinkHeaders(DESCRIBED_BY);
244                if (links != null && links.size() == 1) {
245                    location = links.get(0);
246                }
247            }
248        }
249
250        return location;
251    }
252
253    /**
254     * location setter
255     *
256     * @param location the value of a related resource
257     */
258    public void setLocation(final URI location) {
259        this.location = location;
260    }
261
262    /**
263     * contentType getter
264     *
265     * @return the mime-type of response
266     */
267    public String getContentType() {
268        if (contentType == null && headers != null) {
269            contentType = getHeaderValue(CONTENT_TYPE);
270        }
271        return contentType;
272    }
273
274    /**
275     * contentType setter
276     *
277     * @param contentType the mime-type of the response
278     */
279    public void setContentType(final String contentType) {
280        this.contentType = contentType;
281    }
282
283    /**
284     * Get a map of parameters from the Content-Disposition header if present
285     *
286     * @return map of Content-Disposition parameters or null
287     */
288    public Map<String, String> getContentDisposition() {
289        if (contentDisposition == null && headers.containsKey(CONTENT_DISPOSITION)) {
290            final List<String> values = headers.get(CONTENT_DISPOSITION);
291            if (values.isEmpty()) {
292                return null;
293            }
294
295            contentDisposition = new HashMap<>();
296            final String value = values.get(0);
297            final BasicHeader header = new BasicHeader(CONTENT_DISPOSITION, value);
298            for (final HeaderElement headEl : header.getElements()) {
299                for (final NameValuePair pair : headEl.getParameters()) {
300                    contentDisposition.put(pair.getName(), pair.getValue());
301                }
302            }
303        }
304        return contentDisposition;
305    }
306}