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 */ 016package org.fcrepo.client; 017 018import static org.slf4j.LoggerFactory.getLogger; 019 020import java.io.IOException; 021import java.io.InputStream; 022import java.net.URI; 023import java.util.ArrayList; 024import java.util.List; 025 026import org.apache.http.Header; 027import org.apache.http.HttpEntity; 028import org.apache.http.HttpResponse; 029import org.apache.http.HttpStatus; 030import org.apache.http.client.methods.CloseableHttpResponse; 031import org.apache.http.client.methods.HttpEntityEnclosingRequestBase; 032import org.apache.http.client.methods.HttpRequestBase; 033import org.apache.http.entity.InputStreamEntity; 034import org.apache.http.impl.client.CloseableHttpClient; 035import org.slf4j.Logger; 036 037/** 038 * Represents a client to interact with Fedora's HTTP API. 039 * <p> 040 * Users of the {@code FcrepoClient} are responsible for managing connection resources. Specifically, the underlying 041 * HTTP connections of this client must be freed. Suggested usage is to create the {@code FcrepoResponse} within 042 * a {@code 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> 052 * 053 * @author Aaron Coburn 054 * @since October 20, 2014 055 */ 056public class FcrepoClient { 057 058 private static final String DESCRIBED_BY = "describedby"; 059 060 private static final String CONTENT_TYPE = "Content-Type"; 061 062 private static final String LOCATION = "Location"; 063 064 private CloseableHttpClient httpclient; 065 066 private Boolean throwExceptionOnFailure = true; 067 068 private static final Logger LOGGER = getLogger(FcrepoClient.class); 069 070 /** 071 * Create a FcrepoClient with a set of authentication values. 072 * @param username the username for the repository 073 * @param password the password for the repository 074 * @param host the authentication hostname (realm) for the repository 075 * @param throwExceptionOnFailure whether to throw an exception on any non-2xx or 3xx HTTP responses 076 */ 077 public FcrepoClient(final String username, final String password, final String host, 078 final Boolean throwExceptionOnFailure) { 079 080 final FcrepoHttpClientBuilder client = new FcrepoHttpClientBuilder(username, password, host); 081 082 this.throwExceptionOnFailure = throwExceptionOnFailure; 083 this.httpclient = client.build(); 084 } 085 086 /** 087 * Make a HEAD response 088 * @param url the URL of the resource to check 089 * @return the repository response 090 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 091 */ 092 public FcrepoResponse head(final URI url) 093 throws FcrepoOperationFailedException { 094 095 final HttpRequestBase request = HttpMethods.HEAD.createRequest(url); 096 final CloseableHttpResponse response = executeRequest(request); 097 final int status = response.getStatusLine().getStatusCode(); 098 final String contentType = getContentTypeHeader(response); 099 100 LOGGER.debug("Fcrepo HEAD request returned status [{}]", status); 101 102 if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !this.throwExceptionOnFailure) { 103 URI describedBy = null; 104 final List<URI> links = getLinkHeaders(response, DESCRIBED_BY); 105 if (links.size() == 1) { 106 describedBy = links.get(0); 107 } 108 return new FcrepoResponse(url, status, contentType, describedBy, null); 109 } else { 110 free(response); 111 throw new FcrepoOperationFailedException(url, status, 112 response.getStatusLine().getReasonPhrase()); 113 } 114 } 115 116 /** 117 * Make a PUT request 118 * @param url the URL of the resource to PUT 119 * @param body the contents of the resource to send 120 * @param contentType the MIMEType of the resource 121 * @return the repository response 122 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 123 */ 124 public FcrepoResponse put(final URI url, final InputStream body, final String contentType) 125 throws FcrepoOperationFailedException { 126 127 final HttpMethods method = HttpMethods.PUT; 128 final HttpEntityEnclosingRequestBase request = (HttpEntityEnclosingRequestBase)method.createRequest(url); 129 130 if (contentType != null) { 131 request.addHeader(CONTENT_TYPE, contentType); 132 } 133 if (body != null) { 134 request.setEntity(new InputStreamEntity(body)); 135 } 136 137 LOGGER.debug("Fcrepo PUT request headers: {}", request.getAllHeaders()); 138 139 final CloseableHttpResponse response = executeRequest(request); 140 141 LOGGER.debug("Fcrepo PUT request returned status [{}]", response.getStatusLine().getStatusCode()); 142 143 return fcrepoGenericResponse(url, response, throwExceptionOnFailure); 144 } 145 146 /** 147 * Make a PATCH request 148 * Please note: the body should have an application/sparql-update content-type 149 * @param url the URL of the resource to PATCH 150 * @param body the body to be sent to the repository 151 * @return the repository response 152 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 153 */ 154 public FcrepoResponse patch(final URI url, final InputStream body) 155 throws FcrepoOperationFailedException { 156 157 final HttpMethods method = HttpMethods.PATCH; 158 final HttpEntityEnclosingRequestBase request = (HttpEntityEnclosingRequestBase)method.createRequest(url); 159 160 request.addHeader(CONTENT_TYPE, "application/sparql-update"); 161 request.setEntity(new InputStreamEntity(body)); 162 163 LOGGER.debug("Fcrepo PATCH request headers: {}", request.getAllHeaders()); 164 165 final CloseableHttpResponse response = executeRequest(request); 166 167 LOGGER.debug("Fcrepo PATCH request returned status [{}]", response.getStatusLine().getStatusCode()); 168 169 return fcrepoGenericResponse(url, response, throwExceptionOnFailure); 170 } 171 172 /** 173 * Make a POST request 174 * @param url the URL of the resource to which to POST 175 * @param body the content to be sent to the server 176 * @param contentType the Content-Type of the body 177 * @return the repository response 178 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 179 */ 180 public FcrepoResponse post(final URI url, final InputStream body, final String contentType) 181 throws FcrepoOperationFailedException { 182 183 final HttpMethods method = HttpMethods.POST; 184 final HttpEntityEnclosingRequestBase request = (HttpEntityEnclosingRequestBase)method.createRequest(url); 185 186 if (contentType != null) { 187 request.addHeader(CONTENT_TYPE, contentType); 188 } 189 if (body != null) { 190 request.setEntity(new InputStreamEntity(body)); 191 } 192 193 LOGGER.debug("Fcrepo POST request headers: {}", request.getAllHeaders()); 194 195 final CloseableHttpResponse response = executeRequest(request); 196 197 LOGGER.debug("Fcrepo POST request returned status [{}]", response.getStatusLine().getStatusCode()); 198 199 return fcrepoGenericResponse(url, response, throwExceptionOnFailure); 200 } 201 202 /** 203 * Make a DELETE request 204 * @param url the URL of the resource to delete 205 * @return the repository response 206 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 207 */ 208 public FcrepoResponse delete(final URI url) 209 throws FcrepoOperationFailedException { 210 211 final HttpRequestBase request = HttpMethods.DELETE.createRequest(url); 212 final CloseableHttpResponse response = executeRequest(request); 213 214 LOGGER.debug("Fcrepo DELETE request returned status [{}]", response.getStatusLine().getStatusCode()); 215 216 return fcrepoGenericResponse(url, response, throwExceptionOnFailure); 217 } 218 219 /** 220 * Make a GET request 221 * @param url the URL of the resource to fetch 222 * @param accept the requested MIMEType of the resource to be retrieved 223 * @param prefer the value for a prefer header sent in the request 224 * @return the repository response 225 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 226 */ 227 public FcrepoResponse get(final URI url, final String accept, final String prefer) 228 throws FcrepoOperationFailedException { 229 230 final HttpRequestBase request = HttpMethods.GET.createRequest(url); 231 232 if (accept != null) { 233 request.setHeader("Accept", accept); 234 } 235 236 if (prefer != null) { 237 request.setHeader("Prefer", prefer); 238 } 239 240 LOGGER.debug("Fcrepo GET request headers: {}", request.getAllHeaders()); 241 242 final CloseableHttpResponse response = executeRequest(request); 243 final int status = response.getStatusLine().getStatusCode(); 244 final String contentType = getContentTypeHeader(response); 245 246 LOGGER.debug("Fcrepo GET request returned status [{}]", status); 247 248 if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !this.throwExceptionOnFailure) { 249 URI describedBy = null; 250 final List<URI> links = getLinkHeaders(response, DESCRIBED_BY); 251 if (links.size() == 1) { 252 describedBy = links.get(0); 253 } 254 return new FcrepoResponse(url, status, contentType, describedBy, 255 getEntityContent(response)); 256 } else { 257 free(response); 258 throw new FcrepoOperationFailedException(url, status, 259 response.getStatusLine().getReasonPhrase()); 260 } 261 } 262 263 /** 264 * Execute the HTTP request 265 */ 266 private CloseableHttpResponse executeRequest(final HttpRequestBase request) throws FcrepoOperationFailedException { 267 try { 268 return httpclient.execute(request); 269 } catch (IOException ex) { 270 LOGGER.debug("HTTP Operation failed: ", ex); 271 throw new FcrepoOperationFailedException(request.getURI(), -1, ex.getMessage()); 272 } 273 } 274 275 /** 276 * Handle the general case with responses. 277 */ 278 private FcrepoResponse fcrepoGenericResponse(final URI url, final CloseableHttpResponse response, 279 final Boolean throwExceptionOnFailure) throws FcrepoOperationFailedException { 280 final int status = response.getStatusLine().getStatusCode(); 281 final URI locationHeader = getLocationHeader(response); 282 final String contentTypeHeader = getContentTypeHeader(response); 283 284 if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !throwExceptionOnFailure) { 285 return new FcrepoResponse(url, status, contentTypeHeader, locationHeader, getEntityContent(response)); 286 } else { 287 free(response); 288 throw new FcrepoOperationFailedException(url, status, 289 response.getStatusLine().getReasonPhrase()); 290 } 291 } 292 293 /** 294 * Frees resources associated with the HTTP response. Specifically, closing the {@code response} frees the 295 * connection of the {@link org.apache.http.conn.HttpClientConnectionManager} underlying this {@link #httpclient}. 296 * 297 * @param response the response object to close 298 */ 299 private void free(final CloseableHttpResponse response) { 300 // Free resources associated with the response. 301 try { 302 response.close(); 303 } catch (IOException e) { 304 LOGGER.warn("Unable to close HTTP response.", e); 305 } 306 } 307 308 /** 309 * Extract the response body as an input stream 310 */ 311 private static InputStream getEntityContent(final HttpResponse response) { 312 try { 313 final HttpEntity entity = response.getEntity(); 314 if (entity == null) { 315 return null; 316 } else { 317 return entity.getContent(); 318 } 319 } catch (IOException ex) { 320 LOGGER.debug("Unable to extract HttpEntity response into an InputStream: ", ex); 321 return null; 322 } 323 } 324 325 /** 326 * Extract the location header value 327 */ 328 private static URI getLocationHeader(final HttpResponse response) { 329 final Header location = response.getFirstHeader(LOCATION); 330 if (location != null) { 331 return URI.create(location.getValue()); 332 } else { 333 return null; 334 } 335 } 336 337 /** 338 * Extract the content-type header value 339 */ 340 private static String getContentTypeHeader(final HttpResponse response) { 341 final Header contentType = response.getFirstHeader(CONTENT_TYPE); 342 if (contentType != null) { 343 return contentType.getValue(); 344 } else { 345 return null; 346 } 347 } 348 349 /** 350 * Extract any Link headers 351 */ 352 private static List<URI> getLinkHeaders(final HttpResponse response, final String relationship) { 353 final List<URI> uris = new ArrayList<URI>(); 354 final Header[] links = response.getHeaders("Link"); 355 for (Header header: links) { 356 final FcrepoLink link = new FcrepoLink(header.getValue()); 357 if (link.getRel().equals(relationship)) { 358 uris.add(link.getUri()); 359 } 360 } 361 return uris; 362 } 363 }