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