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 org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION;
020import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_TYPE;
021import static org.fcrepo.client.FedoraHeaderConstants.DESCRIBED_BY;
022import static org.fcrepo.client.FedoraHeaderConstants.LINK;
023import static org.fcrepo.client.FedoraHeaderConstants.LOCATION;
024
025import java.io.Closeable;
026import java.io.IOException;
027import java.io.InputStream;
028import java.net.URI;
029import java.util.ArrayList;
030import java.util.HashMap;
031import java.util.List;
032import java.util.Map;
033
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, or null if not present
188     */
189    public List<String> getHeaderValues(final String name) {
190        if (headers == null) {
191            return null;
192        }
193
194        return headers.get(name);
195    }
196
197    /**
198     * Get the first value for the specified header
199     * 
200     * @param name name of the header to retrieve
201     * @return First value of the header, or null if not present
202     */
203    public String getHeaderValue(final String name) {
204        final List<String> values = getHeaderValues(name);
205        if (values == null || values.size() == 0) {
206            return null;
207        }
208
209        return values.get(0);
210    }
211
212    /**
213     * headers setter
214     * 
215     * @param headers headers from the response
216     */
217    public void setHeaders(final Map<String, List<String>> headers) {
218        this.headers = headers;
219    }
220
221    /**
222     * Retrieve link header values matching the given relationship
223     * 
224     * @param relationship the relationship of links to return
225     * @return list of link header URIs matching the given relationship
226     */
227    public List<URI> getLinkHeaders(final String relationship) {
228        final List<URI> uris = new ArrayList<URI>();
229        final List<String> linkStrings = getHeaderValues(LINK);
230        if (linkStrings == null) {
231            return null;
232        }
233
234        for (String linkString : linkStrings) {
235            final FcrepoLink link = new FcrepoLink(linkString);
236            if (link.getRel().equals(relationship)) {
237                uris.add(link.getUri());
238            }
239        }
240
241        return uris;
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(DESCRIBED_BY);
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}