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