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 */ 016 017package org.fcrepo.client; 018 019import static org.slf4j.LoggerFactory.getLogger; 020 021import java.io.IOException; 022import java.io.InputStream; 023import java.net.URI; 024import java.util.ArrayList; 025import java.util.HashMap; 026import java.util.List; 027import java.util.Map; 028 029import org.apache.http.Header; 030import org.apache.http.HttpEntity; 031import org.apache.http.HttpResponse; 032import org.apache.http.HttpStatus; 033import org.apache.http.client.methods.CloseableHttpResponse; 034import org.apache.http.client.methods.HttpRequestBase; 035import org.apache.http.impl.client.CloseableHttpClient; 036import org.slf4j.Logger; 037 038/** 039 * Represents a client to interact with Fedora's HTTP API. 040 * <p> 041 * Users of the {@code FcrepoClient} are responsible for managing connection resources. Specifically, the underlying 042 * HTTP connections of this client must be freed. Suggested usage is to create the {@code FcrepoResponse} within a 043 * {@code 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(...).perform()) { 048 * // do something with the response 049 * } catch (FcrepoOperationFailedException|IOException e) { 050 * // handle any exceptions 051 * } 052 * </pre> 053 * 054 * @author Aaron Coburn 055 * @since October 20, 2014 056 */ 057public class FcrepoClient { 058 059 private CloseableHttpClient httpclient; 060 061 private Boolean throwExceptionOnFailure = true; 062 063 private static final Logger LOGGER = getLogger(FcrepoClient.class); 064 065 /** 066 * Build a FcrepoClient 067 * 068 * @return a client builder 069 */ 070 public static FcrepoClientBuilder client() { 071 return new FcrepoClientBuilder(); 072 } 073 074 /** 075 * Create a FcrepoClient with a set of authentication values. 076 * 077 * @param username the username for the repository 078 * @param password the password for the repository 079 * @param host the authentication hostname (realm) for the repository 080 * @param throwExceptionOnFailure whether to throw an exception on any non-2xx or 3xx HTTP responses 081 */ 082 protected FcrepoClient(final String username, final String password, final String host, 083 final Boolean throwExceptionOnFailure) { 084 085 final FcrepoHttpClientBuilder client = new FcrepoHttpClientBuilder(username, password, host); 086 087 this.throwExceptionOnFailure = throwExceptionOnFailure; 088 this.httpclient = client.build(); 089 } 090 091 /** 092 * Make a PUT request to create a resource with a specified path, or replace the triples associated with a 093 * resource with the triples provided in the request body. 094 * 095 * @param url the URL of the resource to which to PUT 096 * @return a put request builder object 097 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 098 */ 099 public PutBuilder put(final URI url) throws FcrepoOperationFailedException { 100 return new PutBuilder(url, this); 101 } 102 103 /** 104 * Make a PATCH request to modify the triples associated with a resource with SPARQL-Update. 105 * 106 * @param url the URL of the resource to which to PATCH 107 * @return a patch request builder object 108 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 109 */ 110 public PatchBuilder patch(final URI url) throws FcrepoOperationFailedException { 111 return new PatchBuilder(url, this); 112 } 113 114 /** 115 * Make a POST request to create a new resource within an LDP container. 116 * 117 * @param url the URL of the resource to which to POST 118 * @return a post request builder object 119 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 120 */ 121 public PostBuilder post(final URI url) throws FcrepoOperationFailedException { 122 return new PostBuilder(url, this); 123 } 124 125 /** 126 * Make a DELETE request to delete a resource 127 * 128 * @param url the URL of the resource to which to DELETE 129 * @return a delete request builder object 130 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 131 */ 132 public DeleteBuilder delete(final URI url) throws FcrepoOperationFailedException { 133 return new DeleteBuilder(url, this); 134 } 135 136 /** 137 * Make a MOVE request to copy a resource (and its subtree) to a new location. 138 * 139 * @param source url of the resource to copy 140 * @param destination url of the location for the copy 141 * @return a copy request builder object 142 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 143 */ 144 public CopyBuilder copy(final URI source, final URI destination) throws FcrepoOperationFailedException { 145 return new CopyBuilder(source, destination, this); 146 } 147 148 /** 149 * Make a COPY request to move a resource (and its subtree) to a new location. 150 * 151 * @param source url of the resource to move 152 * @param destination url of the new location for the resource 153 * @return a move request builder object 154 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 155 */ 156 public MoveBuilder move(final URI source, final URI destination) throws FcrepoOperationFailedException { 157 return new MoveBuilder(source, destination, this); 158 } 159 160 /** 161 * Make a GET request to retrieve the content of a resource 162 * 163 * @param url the URL of the resource to which to GET 164 * @return a get request builder object 165 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 166 */ 167 public GetBuilder get(final URI url) throws FcrepoOperationFailedException { 168 return new GetBuilder(url, this); 169 } 170 171 /** 172 * Make a HEAD request to retrieve resource headers. 173 * 174 * @param url the URL of the resource to make the HEAD request on. 175 * @return a HEAD request builder object 176 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 177 */ 178 public HeadBuilder head(final URI url) throws FcrepoOperationFailedException { 179 return new HeadBuilder(url, this); 180 } 181 182 /** 183 * Make a OPTIONS request to output information about the supported HTTP methods, etc. 184 * 185 * @param url the URL of the resource to make the OPTIONS request on. 186 * @return a OPTIONS request builder object 187 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 188 */ 189 public OptionsBuilder options(final URI url) throws FcrepoOperationFailedException { 190 return new OptionsBuilder(url, this); 191 } 192 193 /** 194 * Execute a HTTP request 195 * 196 * @param url URI the request is made to 197 * @param request the request 198 * @return the repository response 199 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 200 */ 201 public FcrepoResponse executeRequest(final URI url, final HttpRequestBase request) 202 throws FcrepoOperationFailedException { 203 LOGGER.debug("Fcrepo {} request to resource {}", request.getMethod(), url); 204 final CloseableHttpResponse response = executeRequest(request); 205 206 return fcrepoGenericResponse(url, response, throwExceptionOnFailure); 207 } 208 209 /** 210 * Execute the HTTP request 211 */ 212 private CloseableHttpResponse executeRequest(final HttpRequestBase request) 213 throws FcrepoOperationFailedException { 214 try { 215 return httpclient.execute(request); 216 } catch (IOException ex) { 217 LOGGER.debug("HTTP Operation failed: ", ex); 218 throw new FcrepoOperationFailedException(request.getURI(), -1, ex.getMessage()); 219 } 220 } 221 222 /** 223 * Handle the general case with responses. 224 */ 225 private FcrepoResponse fcrepoGenericResponse(final URI url, final CloseableHttpResponse response, 226 final Boolean throwExceptionOnFailure) throws FcrepoOperationFailedException { 227 final int status = response.getStatusLine().getStatusCode(); 228 final Map<String, List<String>> headers = getHeaders(response); 229 230 if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !throwExceptionOnFailure) { 231 return new FcrepoResponse(url, status, headers, getEntityContent(response)); 232 } else { 233 free(response); 234 throw new FcrepoOperationFailedException(url, status, 235 response.getStatusLine().getReasonPhrase()); 236 } 237 } 238 239 /** 240 * Frees resources associated with the HTTP response. Specifically, closing the {@code response} frees the 241 * connection of the {@link org.apache.http.conn.HttpClientConnectionManager} underlying this {@link #httpclient}. 242 * 243 * @param response the response object to close 244 */ 245 private void free(final CloseableHttpResponse response) { 246 // Free resources associated with the response. 247 try { 248 response.close(); 249 } catch (IOException e) { 250 LOGGER.warn("Unable to close HTTP response.", e); 251 } 252 } 253 254 /** 255 * Extract the response body as an input stream 256 */ 257 private static InputStream getEntityContent(final HttpResponse response) { 258 try { 259 final HttpEntity entity = response.getEntity(); 260 if (entity == null) { 261 return null; 262 } else { 263 return entity.getContent(); 264 } 265 } catch (IOException ex) { 266 LOGGER.debug("Unable to extract HttpEntity response into an InputStream: ", ex); 267 return null; 268 } 269 } 270 271 /** 272 * Retrieve all header values 273 * 274 * @param response response from request 275 * @return Map of all values for all response headers 276 */ 277 private static Map<String, List<String>> getHeaders(final HttpResponse response) { 278 final Map<String, List<String>> headers = new HashMap<>(); 279 280 for (Header header : response.getAllHeaders()) { 281 List<String> values; 282 if (headers.containsKey(header.getName())) { 283 values = headers.get(header.getName()); 284 } else { 285 values = new ArrayList<>(); 286 headers.put(header.getName(), values); 287 } 288 values.add(header.getValue()); 289 } 290 return headers; 291 } 292 293 /** 294 * Builds an FcrepoClient 295 * 296 * @author bbpennel 297 */ 298 public static class FcrepoClientBuilder { 299 300 private String authUser; 301 302 private String authPassword; 303 304 private String authHost; 305 306 private boolean throwExceptionOnFailure; 307 308 /** 309 * Add basic authentication credentials to this client 310 * 311 * @param username username for authentication 312 * @param password password for authentication 313 * @return the client builder 314 */ 315 public FcrepoClientBuilder credentials(final String username, final String password) { 316 this.authUser = username; 317 this.authPassword = password; 318 return this; 319 } 320 321 /** 322 * Add an authentication scope to this client 323 * 324 * @param authHost authentication scope value 325 * @return this builder 326 */ 327 public FcrepoClientBuilder authScope(final String authHost) { 328 this.authHost = authHost; 329 330 return this; 331 } 332 333 /** 334 * Client should throw exceptions when failures occur 335 * 336 * @return this builder 337 */ 338 public FcrepoClientBuilder throwExceptionOnFailure() { 339 this.throwExceptionOnFailure = true; 340 return this; 341 } 342 343 /** 344 * Get the client 345 * 346 * @return the client constructed by this builder 347 */ 348 public FcrepoClient build() { 349 return new FcrepoClient(authUser, authPassword, authHost, throwExceptionOnFailure); 350 } 351 } 352}