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