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