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.LINK;
025import static org.fcrepo.client.FedoraHeaderConstants.LOCATION;
026import static org.fcrepo.client.LinkHeaderConstants.DESCRIBEDBY_REL;
027import static org.fcrepo.client.LinkHeaderConstants.TYPE_REL;
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     * Return true if the response represents a resource with the given type
231     *
232     * @param typeString String containing the URI of the type
233     * @return true if the type is present.
234     */
235    public boolean hasType(final String typeString) {
236        return hasType(URI.create(typeString));
237    }
238
239    /**
240     * Return true if the response represents a resource with the given type
241     *
242     * @param typeUri URI of the type
243     * @return true if the type is present.
244     */
245    public boolean hasType(final URI typeUri) {
246        return getHeaderValues(LINK).stream()
247                .map(FcrepoLink::new)
248                .anyMatch(l -> l.getRel().equals(TYPE_REL) && l.getUri().equals(typeUri));
249    }
250
251    /**
252     * location getter
253     *
254     * @return the location of a related resource
255     */
256    public URI getLocation() {
257        if (location == null && headers != null) {
258            // Retrieve the value from the location header if available
259            final String value = getHeaderValue(LOCATION);
260            if (value != null) {
261                location = URI.create(getHeaderValue(LOCATION));
262            }
263            // Fall back to retrieving from the described by link
264            if (location == null) {
265                final List<URI> links = getLinkHeaders(DESCRIBEDBY_REL);
266                if (links != null && links.size() == 1) {
267                    location = links.get(0);
268                }
269            }
270        }
271
272        return location;
273    }
274
275    /**
276     * location setter
277     *
278     * @param location the value of a related resource
279     */
280    public void setLocation(final URI location) {
281        this.location = location;
282    }
283
284    /**
285     * contentType getter
286     *
287     * @return the mime-type of response
288     */
289    public String getContentType() {
290        if (contentType == null && headers != null) {
291            contentType = getHeaderValue(CONTENT_TYPE);
292        }
293        return contentType;
294    }
295
296    /**
297     * contentType setter
298     *
299     * @param contentType the mime-type of the response
300     */
301    public void setContentType(final String contentType) {
302        this.contentType = contentType;
303    }
304
305    /**
306     * Get a map of parameters from the Content-Disposition header if present
307     *
308     * @return map of Content-Disposition parameters or null
309     */
310    public Map<String, String> getContentDisposition() {
311        if (contentDisposition == null && headers.containsKey(CONTENT_DISPOSITION)) {
312            final List<String> values = headers.get(CONTENT_DISPOSITION);
313            if (values.isEmpty()) {
314                return null;
315            }
316
317            contentDisposition = new HashMap<>();
318            final String value = values.get(0);
319            final BasicHeader header = new BasicHeader(CONTENT_DISPOSITION, value);
320            for (final HeaderElement headEl : header.getElements()) {
321                for (final NameValuePair pair : headEl.getParameters()) {
322                    contentDisposition.put(pair.getName(), pair.getValue());
323                }
324            }
325        }
326        return contentDisposition;
327    }
328}