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 GET request to retrieve the content of a resource 170 * 171 * @param url the URL of the resource to which to GET 172 * @return a get request builder object 173 */ 174 public GetBuilder get(final URI url) { 175 return new GetBuilder(url, this); 176 } 177 178 /** 179 * Make a HEAD request to retrieve resource headers. 180 * 181 * @param url the URL of the resource to make the HEAD request on. 182 * @return a HEAD request builder object 183 */ 184 public HeadBuilder head(final URI url) { 185 return new HeadBuilder(url, this); 186 } 187 188 /** 189 * Make a OPTIONS request to output information about the supported HTTP methods, etc. 190 * 191 * @param url the URL of the resource to make the OPTIONS request on. 192 * @return a OPTIONS request builder object 193 */ 194 public OptionsBuilder options(final URI url) { 195 return new OptionsBuilder(url, this); 196 } 197 198 /** 199 * Execute a HTTP request 200 * 201 * @param url URI the request is made to 202 * @param request the request 203 * @return the repository response 204 * @throws FcrepoOperationFailedException when the underlying HTTP request results in an error 205 */ 206 public FcrepoResponse executeRequest(final URI url, final HttpRequestBase request) 207 throws FcrepoOperationFailedException { 208 LOGGER.debug("Fcrepo {} request to resource {}", request.getMethod(), url); 209 final CloseableHttpResponse response = executeRequest(request); 210 211 return fcrepoGenericResponse(url, response, throwExceptionOnFailure); 212 } 213 214 /** 215 * Execute the HTTP request 216 */ 217 private CloseableHttpResponse executeRequest(final HttpRequestBase request) 218 throws FcrepoOperationFailedException { 219 try { 220 return httpclient.execute(request); 221 } catch (final IOException ex) { 222 LOGGER.debug("HTTP Operation failed: ", ex); 223 throw new FcrepoOperationFailedException(request.getURI(), -1, ex.getMessage()); 224 } 225 } 226 227 /** 228 * Handle the general case with responses. 229 */ 230 private FcrepoResponse fcrepoGenericResponse(final URI url, final CloseableHttpResponse response, 231 final Boolean throwExceptionOnFailure) throws FcrepoOperationFailedException { 232 final int status = response.getStatusLine().getStatusCode(); 233 final Map<String, List<String>> headers = getHeaders(response); 234 235 if ((status >= HttpStatus.SC_OK && status < HttpStatus.SC_BAD_REQUEST) || !throwExceptionOnFailure) { 236 return new FcrepoResponse(url, status, headers, getEntityContent(response)); 237 } else { 238 free(response); 239 throw new FcrepoOperationFailedException(url, status, 240 response.getStatusLine().getReasonPhrase()); 241 } 242 } 243 244 /** 245 * Frees resources associated with the HTTP response. Specifically, closing the {@code response} frees the 246 * connection of the {@link org.apache.http.conn.HttpClientConnectionManager} underlying this {@link #httpclient}. 247 * 248 * @param response the response object to close 249 */ 250 private void free(final CloseableHttpResponse response) { 251 // Free resources associated with the response. 252 try { 253 response.close(); 254 } catch (final IOException e) { 255 LOGGER.warn("Unable to close HTTP response.", e); 256 } 257 } 258 259 /** 260 * Extract the response body as an input stream 261 */ 262 private static InputStream getEntityContent(final HttpResponse response) { 263 try { 264 final HttpEntity entity = response.getEntity(); 265 if (entity == null) { 266 return null; 267 } else { 268 return entity.getContent(); 269 } 270 } catch (final IOException ex) { 271 LOGGER.debug("Unable to extract HttpEntity response into an InputStream: ", ex); 272 return null; 273 } 274 } 275 276 /** 277 * Retrieve all header values 278 * 279 * @param response response from request 280 * @return Map of all values for all response headers 281 */ 282 private static Map<String, List<String>> getHeaders(final HttpResponse response) { 283 final Map<String, List<String>> headers = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); 284 285 for (final Header header : response.getAllHeaders()) { 286 final List<String> values; 287 if (headers.containsKey(header.getName())) { 288 values = headers.get(header.getName()); 289 } else { 290 values = new ArrayList<>(); 291 headers.put(header.getName(), values); 292 } 293 values.add(header.getValue()); 294 } 295 return headers; 296 } 297 298 /** 299 * Builds an FcrepoClient 300 * 301 * @author bbpennel 302 */ 303 public static class FcrepoClientBuilder { 304 305 private String authUser; 306 307 private String authPassword; 308 309 private String authHost; 310 311 private boolean throwExceptionOnFailure; 312 313 /** 314 * Add basic authentication credentials to this client 315 * 316 * @param username username for authentication 317 * @param password password for authentication 318 * @return the client builder 319 */ 320 public FcrepoClientBuilder credentials(final String username, final String password) { 321 this.authUser = username; 322 this.authPassword = password; 323 return this; 324 } 325 326 /** 327 * Add an authentication scope to this client 328 * 329 * @param authHost authentication scope value 330 * @return this builder 331 */ 332 public FcrepoClientBuilder authScope(final String authHost) { 333 this.authHost = authHost; 334 335 return this; 336 } 337 338 /** 339 * Client should throw exceptions when failures occur 340 * 341 * @return this builder 342 */ 343 public FcrepoClientBuilder throwExceptionOnFailure() { 344 this.throwExceptionOnFailure = true; 345 return this; 346 } 347 348 /** 349 * Get the client 350 * 351 * @return the client constructed by this builder 352 */ 353 public FcrepoClient build() { 354 return new FcrepoClient(authUser, authPassword, authHost, throwExceptionOnFailure); 355 } 356 } 357}