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