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