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.FcrepoClient.TRANSACTION_ENDPOINT; 011import static org.fcrepo.client.FedoraHeaderConstants.ATOMIC_ID; 012import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_DISPOSITION; 013import static org.fcrepo.client.FedoraHeaderConstants.CONTENT_TYPE; 014import static org.fcrepo.client.FedoraHeaderConstants.LINK; 015import static org.fcrepo.client.FedoraHeaderConstants.LOCATION; 016import static org.fcrepo.client.LinkHeaderConstants.DESCRIBEDBY_REL; 017import static org.fcrepo.client.LinkHeaderConstants.TYPE_REL; 018import java.io.Closeable; 019import java.io.IOException; 020import java.io.InputStream; 021import java.net.URI; 022import java.util.HashMap; 023import java.util.List; 024import java.util.Map; 025 026import org.apache.http.HeaderElement; 027import org.apache.http.NameValuePair; 028import org.apache.http.message.BasicHeader; 029 030/** 031 * Represents a response from a fedora repository using a {@link FcrepoClient}. 032 * <p> 033 * This class implements {@link Closeable}. Suggested usage is to create the {@code FcrepoResponse} within a 034 * try-with-resources block, insuring that any resources held by the response are freed automatically. 035 * </p> 036 * <pre> 037 * FcrepoClient client = ...; 038 * try (FcrepoResponse res = client.get(...)) { 039 * // do something with the response 040 * } catch (FcrepoOperationFailedException|IOException e) { 041 * // handle any exceptions 042 * } 043 * </pre> Closed responses have no obligation to provide access to released resources. 044 * 045 * @author Aaron Coburn 046 * @since October 20, 2014 047 */ 048public class FcrepoResponse implements Closeable { 049 050 private URI url; 051 052 private int statusCode; 053 054 private URI location; 055 056 private Map<String, List<String>> headers; 057 058 private Map<String, String> contentDisposition; 059 060 private InputStream body; 061 062 private URI transactionUri; 063 064 private String contentType; 065 066 private boolean closed = false; 067 068 /** 069 * Create a FcrepoResponse object from the http response 070 * 071 * @param url the requested URL 072 * @param statusCode the HTTP status code 073 * @param headers a map of all response header names and values 074 * @param body the response body stream 075 */ 076 public FcrepoResponse(final URI url, final int statusCode, final Map<String, List<String>> headers, 077 final InputStream body) { 078 this.setUrl(url); 079 this.setStatusCode(statusCode); 080 this.setHeaders(headers); 081 this.setBody(body); 082 } 083 084 /** 085 * {@inheritDoc} 086 * <p> 087 * Implementation note: Invoking this method will close the underlying {@code InputStream} containing the entity 088 * body of the HTTP response. 089 * </p> 090 * 091 * @throws IOException if there is an error closing the underlying HTTP response stream. 092 */ 093 @Override 094 public void close() throws IOException { 095 if (!this.closed && this.body != null) { 096 try { 097 this.body.close(); 098 } finally { 099 this.closed = true; 100 } 101 } 102 } 103 104 /** 105 * Whether or not the resources have been freed from this response. There should be no expectation that a closed 106 * response provides access to the {@link #getBody() entity body}. 107 * 108 * @return {@code true} if resources have been freed, otherwise {@code false} 109 */ 110 public boolean isClosed() { 111 return closed; 112 } 113 114 /** 115 * url getter 116 * 117 * @return the requested URL 118 */ 119 public URI getUrl() { 120 return url; 121 } 122 123 /** 124 * url setter 125 * 126 * @param url the requested URL 127 */ 128 public void setUrl(final URI url) { 129 this.url = url; 130 } 131 132 /** 133 * statusCode getter 134 * 135 * @return the HTTP status code 136 */ 137 public int getStatusCode() { 138 return statusCode; 139 } 140 141 /** 142 * statusCode setter 143 * 144 * @param statusCode the HTTP status code 145 */ 146 public void setStatusCode(final int statusCode) { 147 this.statusCode = statusCode; 148 } 149 150 /** 151 * body getter 152 * 153 * @return the response body as a stream 154 */ 155 public InputStream getBody() { 156 return body; 157 } 158 159 /** 160 * body setter 161 * 162 * @param body the contents of the response body 163 */ 164 public void setBody(final InputStream body) { 165 this.body = body; 166 } 167 168 /** 169 * headers getter 170 * 171 * @return headers from the response 172 */ 173 public Map<String, List<String>> getHeaders() { 174 return this.headers; 175 } 176 177 /** 178 * Get all values for the specified header 179 * 180 * @param name name of the header to retrieve 181 * @return All values of the specified header 182 */ 183 public List<String> getHeaderValues(final String name) { 184 return headers == null ? emptyList() : headers.getOrDefault(name, emptyList()); 185 } 186 187 /** 188 * Get the first value for the specified header 189 * 190 * @param name name of the header to retrieve 191 * @return First value of the header, or null if not present 192 */ 193 public String getHeaderValue(final String name) { 194 final List<String> values = getHeaderValues(name); 195 if (values == null || values.size() == 0) { 196 return null; 197 } 198 199 return values.get(0); 200 } 201 202 /** 203 * headers setter 204 * 205 * @param headers headers from the response 206 */ 207 public void setHeaders(final Map<String, List<String>> headers) { 208 this.headers = headers; 209 } 210 211 /** 212 * Retrieve link header values matching the given relationship 213 * 214 * @param relationship the relationship of links to return 215 * @return list of link header URIs matching the given relationship 216 */ 217 public List<URI> getLinkHeaders(final String relationship) { 218 return getHeaderValues(LINK).stream().map(FcrepoLink::new).filter(link -> link.getRel().equals(relationship)) 219 .map(FcrepoLink::getUri).collect(toList()); 220 } 221 222 /** 223 * Return true if the response represents a resource with the given type 224 * 225 * @param typeString String containing the URI of the type 226 * @return true if the type is present. 227 */ 228 public boolean hasType(final String typeString) { 229 return hasType(URI.create(typeString)); 230 } 231 232 /** 233 * Return true if the response represents a resource with the given type 234 * 235 * @param typeUri URI of the type 236 * @return true if the type is present. 237 */ 238 public boolean hasType(final URI typeUri) { 239 return getHeaderValues(LINK).stream() 240 .map(FcrepoLink::new) 241 .anyMatch(l -> l.getRel().equals(TYPE_REL) && l.getUri().equals(typeUri)); 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(DESCRIBEDBY_REL); 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 322 /** 323 * Get the transaction location. If the location is not for a transaction, check for the Atomic-ID, 324 * otherwise return null. 325 * 326 * @return the transaction location or null 327 */ 328 public URI getTransactionUri() { 329 if (transactionUri == null) { 330 final var location = getHeaderValue(LOCATION); 331 final var atomicId = getHeaderValue(ATOMIC_ID); 332 333 if (location != null && location.contains(TRANSACTION_ENDPOINT)) { 334 transactionUri = URI.create(location); 335 } else if (atomicId != null) { 336 transactionUri = URI.create(atomicId); 337 } 338 } 339 340 return transactionUri; 341 } 342 343}