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