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}