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