001package co.codewizards.cloudstore.rest.client; 002 003import static co.codewizards.cloudstore.core.util.Util.*; 004 005import java.lang.reflect.Constructor; 006import java.lang.reflect.InvocationTargetException; 007import java.net.URI; 008import java.net.URL; 009import java.util.Date; 010import java.util.LinkedList; 011import java.util.UUID; 012 013import javax.net.ssl.HostnameVerifier; 014import javax.net.ssl.SSLContext; 015import javax.ws.rs.WebApplicationException; 016import javax.ws.rs.client.Client; 017import javax.ws.rs.client.ClientBuilder; 018import javax.ws.rs.client.Entity; 019import javax.ws.rs.client.Invocation; 020import javax.ws.rs.client.ResponseProcessingException; 021import javax.ws.rs.client.WebTarget; 022import javax.ws.rs.core.MediaType; 023import javax.ws.rs.core.Response; 024 025import org.glassfish.jersey.client.ClientConfig; 026import org.glassfish.jersey.client.ClientProperties; 027import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; 028import org.glassfish.jersey.uri.UriComponent; 029import org.slf4j.Logger; 030import org.slf4j.LoggerFactory; 031 032import co.codewizards.cloudstore.core.auth.EncryptedSignedAuthToken; 033import co.codewizards.cloudstore.core.concurrent.DeferredCompletionException; 034import co.codewizards.cloudstore.core.config.Config; 035import co.codewizards.cloudstore.core.dto.ChangeSetDTO; 036import co.codewizards.cloudstore.core.dto.DateTime; 037import co.codewizards.cloudstore.core.dto.Error; 038import co.codewizards.cloudstore.core.dto.RepoFileDTO; 039import co.codewizards.cloudstore.core.dto.RepositoryDTO; 040import co.codewizards.cloudstore.core.util.StringUtil; 041import co.codewizards.cloudstore.rest.client.jersey.CloudStoreJaxbContextResolver; 042import co.codewizards.cloudstore.rest.shared.GZIPReaderInterceptor; 043import co.codewizards.cloudstore.rest.shared.GZIPWriterInterceptor; 044 045public class CloudStoreRESTClient { 046 047 private static final Logger logger = LoggerFactory.getLogger(CloudStoreRESTClient.class); 048 049 private static final int DEFAULT_SOCKET_CONNECT_TIMEOUT = 60 * 1000; 050 private static final int DEFAULT_SOCKET_READ_TIMEOUT = 5 * 60 * 1000; 051 052 /** 053 * The {@code key} for the connection timeout used with {@link Config#getPropertyAsInt(String, int)}. 054 * <p> 055 * The configuration can be overridden by a system property - see {@link Config#SYSTEM_PROPERTY_PREFIX}. 056 */ 057 public static final String CONFIG_KEY_SOCKET_CONNECT_TIMEOUT = "socket.connectTimeout"; //$NON-NLS-1$ 058 059 /** 060 * The {@code key} for the read timeout used with {@link Config#getPropertyAsInt(String, int)}. 061 * <p> 062 * The configuration can be overridden by a system property - see {@link Config#SYSTEM_PROPERTY_PREFIX}. 063 */ 064 public static final String CONFIG_KEY_SOCKET_READ_TIMEOUT = "socket.readTimeout"; //$NON-NLS-1$ 065 066 private Integer socketConnectTimeout; 067 068 private Integer socketReadTimeout; 069 070 private final String url; 071 private String baseURL; 072 073 private LinkedList<Client> clientCache = new LinkedList<Client>(); 074 075 private boolean configFrozen; 076 077 private HostnameVerifier hostnameVerifier; 078 private SSLContext sslContext; 079 080 private CredentialsProvider credentialsProvider; 081 082 public Integer getSocketConnectTimeout() { 083 if (socketConnectTimeout == null) 084 socketConnectTimeout = Config.getInstance().getPropertyAsPositiveOrZeroInt( 085 CONFIG_KEY_SOCKET_CONNECT_TIMEOUT, 086 DEFAULT_SOCKET_CONNECT_TIMEOUT); 087 088 return socketConnectTimeout; 089 } 090 public void setSocketConnectTimeout(Integer socketConnectTimeout) { 091 if (socketConnectTimeout != null && socketConnectTimeout < 0) 092 socketConnectTimeout = null; 093 094 this.socketConnectTimeout = socketConnectTimeout; 095 } 096 097 public Integer getSocketReadTimeout() { 098 if (socketReadTimeout == null) 099 socketReadTimeout = Config.getInstance().getPropertyAsPositiveOrZeroInt( 100 CONFIG_KEY_SOCKET_READ_TIMEOUT, 101 DEFAULT_SOCKET_READ_TIMEOUT); 102 103 return socketReadTimeout; 104 } 105 public void setSocketReadTimeout(Integer socketReadTimeout) { 106 if (socketReadTimeout != null && socketReadTimeout < 0) 107 socketReadTimeout = null; 108 109 this.socketReadTimeout = socketReadTimeout; 110 } 111 112 /** 113 * Get the server's base-URL. 114 * <p> 115 * This base-URL is the base of the <code>CloudStoreREST</code> application. Hence all URLs 116 * beneath this base-URL are processed by the <code>CloudStoreREST</code> application. 117 * <p> 118 * In other words: All repository-names are located directly beneath this base-URL. The special services, too, 119 * are located directly beneath this base-URL. 120 * <p> 121 * For example, if the server's base-URL is "https://host.domain:8443/", then the test-service is 122 * available via "https://host.domain:8443/_test" and the repository with the alias "myrepo" is 123 * "https://host.domain:8443/myrepo". 124 * @return the base-URL. This URL always ends with "/". 125 */ 126 public synchronized String getBaseURL() { 127 if (baseURL == null) { 128 determineBaseURL(); 129 } 130 return baseURL; 131 } 132 133 /** 134 * Create a new client. 135 * @param url any URL to the server. Must not be <code>null</code>. 136 * May be the base-URL, any repository's remote-root-URL or any URL within a remote-root-URL. 137 * The base-URL is automatically determined by cutting sub-paths, step by step. 138 */ 139 public CloudStoreRESTClient(URL url) 140 { 141 this(assertNotNull("url", url).toExternalForm()); 142 } 143 144 /** 145 * Create a new client. 146 * @param url any URL to the server. Must not be <code>null</code>. 147 * May be the base-URL, any repository's remote-root-URL or any URL within a remote-root-URL. 148 * The base-URL is automatically determined by cutting sub-paths, step by step. 149 */ 150 public CloudStoreRESTClient(String url) 151 { 152 this.url = assertNotNull("url", url); 153 } 154 155 private static String appendFinalSlashIfNeeded(String url) { 156 return url.endsWith("/") ? url : url + "/"; 157 } 158 159 private void determineBaseURL() { 160 acquireClient(); 161 try { 162 Client client = getClientOrFail(); 163 String url = appendFinalSlashIfNeeded(this.url); 164 while (true) { 165 String testUrl = url + "_test"; 166 try { 167 String response = client.target(testUrl).request(MediaType.TEXT_PLAIN).get(String.class); 168 if ("SUCCESS".equals(response)) { 169 baseURL = url; 170 break; 171 } 172 } catch (WebApplicationException x) { doNothing(); } 173 174 if (!url.endsWith("/")) 175 throw new IllegalStateException("url does not end with '/'!"); 176 177 int secondLastSlashIndex = url.lastIndexOf('/', url.length() - 2); 178 url = url.substring(0, secondLastSlashIndex + 1); 179 180 if (StringUtil.getIndexesOf(url, '/').size() < 3) 181 throw new IllegalStateException("baseURL not found!"); 182 } 183 } finally { 184 releaseClient(); 185 } 186 } 187 188 private static final void doNothing() { } 189 190 public void testSuccess() { 191 acquireClient(); 192 try { 193 String response = createWebTarget("_test").request(MediaType.TEXT_PLAIN).get(String.class); 194 if (!"SUCCESS".equals(response)) { 195 throw new IllegalStateException("Server response invalid: " + response); 196 } 197 } catch (RuntimeException x) { 198 handleException(x); 199 throw x; // we do not expect null 200 } finally { 201 releaseClient(); 202 } 203 } 204 205 public void testException() { 206 acquireClient(); 207 try { 208 Response response = createWebTarget("_test").queryParam("exception", true).request().get(); 209 assertResponseIndicatesSuccess(response); 210 throw new IllegalStateException("Server sent response instead of exception: " + response); 211 } catch (RuntimeException x) { 212 handleException(x); 213 throw x; // we do not expect null 214 } finally { 215 releaseClient(); 216 } 217 } 218 219 private void assertResponseIndicatesSuccess(Response response) { 220 if (400 <= response.getStatus() && response.getStatus() <= 599) { 221 response.bufferEntity(); 222 if (response.hasEntity()) { 223 Error error = null; 224 try { 225 error = response.readEntity(Error.class); 226 } catch (Exception y) { 227 logger.error("handleException: " + y, y); 228 } 229 if (error != null) { 230 throwOriginalExceptionIfPossible(error); 231 throw new RemoteException(error); 232 } 233 } 234 throw new WebApplicationException(response); 235 } 236 } 237 238 public RepositoryDTO getRepositoryDTO(String repositoryName) { 239 assertNotNull("repositoryName", repositoryName); 240 acquireClient(); 241 try { 242 RepositoryDTO repositoryDTO = createWebTarget(getPath(RepositoryDTO.class), urlEncode(repositoryName)) 243 .request().get(RepositoryDTO.class); 244 return repositoryDTO; 245 } catch (RuntimeException x) { 246 handleException(x); 247 throw x; // we do not expect null 248 } finally { 249 releaseClient(); 250 } 251 } 252 253 private String getPath(Class<?> dtoClass) { 254 return "_" + dtoClass.getSimpleName(); 255 } 256 257 public ChangeSetDTO getChangeSet(String repositoryName, boolean localSync) { 258 assertNotNull("repositoryName", repositoryName); 259 acquireClient(); 260 try { 261 WebTarget webTarget = createWebTarget(getPath(ChangeSetDTO.class), urlEncode(repositoryName)); 262 263 if (localSync) 264 webTarget = webTarget.queryParam("localSync", localSync); 265 266 ChangeSetDTO changeSetDTO = assignCredentials(webTarget.request(MediaType.APPLICATION_XML)).get(ChangeSetDTO.class); 267 return changeSetDTO; 268 } catch (RuntimeException x) { 269 handleException(x); 270 throw x; // we do not expect null 271 } finally { 272 releaseClient(); 273 } 274 } 275 276 public void requestRepoConnection(String repositoryName, String pathPrefix, RepositoryDTO clientRepositoryDTO) { 277 assertNotNull("clientRepositoryDTO", clientRepositoryDTO); 278 assertNotNull("clientRepositoryDTO.repositoryId", clientRepositoryDTO.getRepositoryId()); 279 assertNotNull("clientRepositoryDTO.publicKey", clientRepositoryDTO.getPublicKey()); 280 acquireClient(); 281 try { 282 Response response = createWebTarget("_requestRepoConnection", urlEncode(repositoryName), pathPrefix) 283 .request().post(Entity.entity(clientRepositoryDTO, MediaType.APPLICATION_XML)); 284 assertResponseIndicatesSuccess(response); 285 } catch (RuntimeException x) { 286 handleException(x); 287 throw x; 288 } finally { 289 releaseClient(); 290 } 291 } 292 293 public EncryptedSignedAuthToken getEncryptedSignedAuthToken(String repositoryName, UUID clientRepositoryId) { 294 assertNotNull("repositoryName", repositoryName); 295 assertNotNull("clientRepositoryId", clientRepositoryId); 296 acquireClient(); 297 try { 298 EncryptedSignedAuthToken encryptedSignedAuthToken = createWebTarget( 299 getPath(EncryptedSignedAuthToken.class), urlEncode(repositoryName), clientRepositoryId.toString()) 300 .request(MediaType.APPLICATION_XML).get(EncryptedSignedAuthToken.class); 301 return encryptedSignedAuthToken; 302 } catch (RuntimeException x) { 303 handleException(x); 304 throw x; // we should never receive (and return) null. 305 } finally { 306 releaseClient(); 307 } 308 } 309 310 public RepoFileDTO getRepoFileDTO(String repositoryName, String path) { 311 assertNotNull("repositoryName", repositoryName); 312 acquireClient(); 313 try { 314 WebTarget webTarget = createWebTarget(getPath(RepoFileDTO.class), urlEncode(repositoryName), encodePath(path)); 315 RepoFileDTO repoFileDTO = assignCredentials(webTarget.request(MediaType.APPLICATION_XML)).get(RepoFileDTO.class); 316 return repoFileDTO; 317 } catch (RuntimeException x) { 318 handleException(x); 319 return null; 320 } finally { 321 releaseClient(); 322 } 323 } 324 325 public void beginPutFile(String repositoryName, String path) { 326 assertNotNull("repositoryName", repositoryName); 327 acquireClient(); 328 try { 329 Response response = assignCredentials( 330 createWebTarget("_beginPutFile", urlEncode(repositoryName), encodePath(path)) 331 .request()).post(null); 332 assertResponseIndicatesSuccess(response); 333 } catch (RuntimeException x) { 334 handleException(x); 335 throw x; 336 } finally { 337 releaseClient(); 338 } 339 } 340 341 public void endSyncFromRepository(String repositoryName) { 342 assertNotNull("repositoryName", repositoryName); 343 acquireClient(); 344 try { 345 Response response = assignCredentials( 346 createWebTarget("_endSyncFromRepository", urlEncode(repositoryName)) 347 .request()).post(null); 348 assertResponseIndicatesSuccess(response); 349 } catch (RuntimeException x) { 350 handleException(x); 351 throw x; 352 } finally { 353 releaseClient(); 354 } 355 } 356 357 public void endSyncToRepository(String repositoryName, long fromLocalRevision) { 358 assertNotNull("repositoryName", repositoryName); 359 if (fromLocalRevision < 0) 360 throw new IllegalArgumentException("fromLocalRevision < 0"); 361 362 acquireClient(); 363 try { 364 Response response = assignCredentials( 365 createWebTarget("_endSyncToRepository", urlEncode(repositoryName)) 366 .queryParam("fromLocalRevision", fromLocalRevision) 367 .request()).post(null); 368 assertResponseIndicatesSuccess(response); 369 } catch (RuntimeException x) { 370 handleException(x); 371 throw x; 372 } finally { 373 releaseClient(); 374 } 375 } 376 377 public void endPutFile(String repositoryName, String path, DateTime lastModified, long length, String sha1) { 378 assertNotNull("repositoryName", repositoryName); 379 assertNotNull("path", path); 380 assertNotNull("lastModified", lastModified); 381 assertNotNull("sha1", sha1); 382 acquireClient(); 383 try { 384 Response response = assignCredentials( 385 createWebTarget("_endPutFile", urlEncode(repositoryName), encodePath(path)) 386 .queryParam("lastModified", lastModified.toString()) 387 .queryParam("length", length) 388 .queryParam("sha1", sha1) 389 .request()).post(null); 390 assertResponseIndicatesSuccess(response); 391 } catch (RuntimeException x) { 392 handleException(x); 393 throw x; 394 } finally { 395 releaseClient(); 396 } 397 } 398 399 private Invocation.Builder assignCredentials(Invocation.Builder builder) { 400 CredentialsProvider credentialsProvider = getCredentialsProviderOrFail(); 401 builder.property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_USERNAME, credentialsProvider.getUserName()); 402 builder.property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_PASSWORD, credentialsProvider.getPassword()); 403 return builder; 404 } 405 406 public byte[] getFileData(String repositoryName, String path, long offset, int length) { 407 assertNotNull("repositoryName", repositoryName); 408 acquireClient(); 409 try { 410 WebTarget webTarget = createWebTarget(urlEncode(repositoryName), encodePath(path)); 411 412 if (offset > 0) // defaults to 0 413 webTarget = webTarget.queryParam("offset", offset); 414 415 if (length >= 0) // defaults to -1 meaning "all" 416 webTarget = webTarget.queryParam("length", length); 417 418 return assignCredentials(webTarget.request(MediaType.APPLICATION_OCTET_STREAM)).get(byte[].class); 419 } catch (RuntimeException x) { 420 handleException(x); 421 throw x; // we should never receive (and return) null. 422 } finally { 423 releaseClient(); 424 } 425 } 426 427 public void putFileData(String repositoryName, String path, long offset, byte[] fileData) { 428 assertNotNull("repositoryName", repositoryName); 429 assertNotNull("path", path); 430 assertNotNull("fileData", fileData); 431 acquireClient(); 432 try { 433 WebTarget webTarget = createWebTarget(urlEncode(repositoryName), encodePath(path)); 434 435 if (offset > 0) 436 webTarget = webTarget.queryParam("offset", offset); 437 438 Response response = assignCredentials(webTarget.request()).put(Entity.entity(fileData, MediaType.APPLICATION_OCTET_STREAM)); 439 assertResponseIndicatesSuccess(response); 440 } catch (RuntimeException x) { 441 handleException(x); 442 throw x; 443 } finally { 444 releaseClient(); 445 } 446 } 447 448 public void delete(String repositoryName, String path) { 449 assertNotNull("repositoryName", repositoryName); 450 acquireClient(); 451 try { 452 Response response = assignCredentials( 453 createWebTarget(urlEncode(repositoryName), encodePath(path)).request()).delete(); 454 assertResponseIndicatesSuccess(response); 455 } catch (RuntimeException x) { 456 handleException(x); 457 throw x; // delete should never throw an exception, if it didn't have a real problem 458 } finally { 459 releaseClient(); 460 } 461 } 462 463 public void copy(String repositoryName, String fromPath, String toPath) { 464 assertNotNull("repositoryName", repositoryName); 465 acquireClient(); 466 try { 467 Response response = assignCredentials(createWebTarget("_copy", urlEncode(repositoryName), encodePath(fromPath)) 468 .queryParam("to", encodePath(toPath)) 469 .request()).post(null); 470 assertResponseIndicatesSuccess(response); 471 } catch (RuntimeException x) { 472 handleException(x); 473 throw x; 474 } finally { 475 releaseClient(); 476 } 477 } 478 479 public void move(String repositoryName, String fromPath, String toPath) { 480 assertNotNull("repositoryName", repositoryName); 481 acquireClient(); 482 try { 483 Response response = assignCredentials(createWebTarget("_move", urlEncode(repositoryName), encodePath(fromPath)) 484 .queryParam("to", encodePath(toPath)) 485 .request()).post(null); 486 assertResponseIndicatesSuccess(response); 487 } catch (RuntimeException x) { 488 handleException(x); 489 throw x; 490 } finally { 491 releaseClient(); 492 } 493 } 494 495// public void localSync(String repositoryName) { 496// assertNotNull("repositoryName", repositoryName); 497// Client client = acquireClient(); 498// try { 499// Response response = client.target(getBaseURL()).path("_localSync").path(repositoryName).request().post(null); 500// assertResponseIndicatesSuccess(response); 501// } catch (RuntimeException x) { 502// handleException(x); 503// throw x; // delete should never throw an exception, if it didn't have a real problem 504// } finally { 505// releaseClient(client); 506// } 507// } 508 509 public void makeDirectory(String repositoryName, String path, Date lastModified) { 510 assertNotNull("repositoryName", repositoryName); 511 assertNotNull("path", path); 512 acquireClient(); 513 try { 514// WebTarget webTarget = client.target(getBaseURL()).path(repositoryName).path(removeLeadingAndTrailingSlash(path)); 515// 516// if (lastModified != null) 517// webTarget = webTarget.queryParam("lastModified", new DateTime(lastModified)); 518// 519// Response response = webTarget.request().method("MKCOL"); 520// assertResponseIndicatesSuccess(response); 521 522 // The HTTP verb "MKCOL" is not yet supported by Jersey (and not even the unterlying HTTP client) 523 // by default. We first have to add this. This will be done later (for the WebDAV support). For 524 // now, we'll use the alternative MakeDirectoryService. 525 526 WebTarget webTarget = createWebTarget("_makeDirectory", urlEncode(repositoryName), encodePath(path)); 527 528 if (lastModified != null) 529 webTarget = webTarget.queryParam("lastModified", new DateTime(lastModified)); 530 531 Response response = assignCredentials(webTarget.request()).post(null); 532 assertResponseIndicatesSuccess(response); 533 } catch (RuntimeException x) { 534 handleException(x); 535 throw x; 536 } finally { 537 releaseClient(); 538 } 539 } 540 541 public void makeSymlink(String repositoryName, String path, String target, Date lastModified) { 542 assertNotNull("repositoryName", repositoryName); 543 assertNotNull("path", path); 544 assertNotNull("target", target); 545 acquireClient(); 546 try { 547 WebTarget webTarget = createWebTarget("_makeSymlink", urlEncode(repositoryName), encodePath(path)) 548 .queryParam("target", encodePath(target)); 549 550 if (lastModified != null) 551 webTarget = webTarget.queryParam("lastModified", new DateTime(lastModified)); 552 553 Response response = assignCredentials(webTarget.request()).post(null); 554 assertResponseIndicatesSuccess(response); 555 } catch (RuntimeException x) { 556 handleException(x); 557 throw x; 558 } finally { 559 releaseClient(); 560 } 561 } 562 563 /** 564 * Create a {@link WebTarget} from the given path segments. 565 * <p> 566 * This method prefixes the path with the {@link #getBaseURL() base-URL} and appends 567 * all path segments separated via slashes ('/'). 568 * <p> 569 * We do not use <code>client.target(getBaseURL()).path("...")</code>, because the 570 * {@link WebTarget#path(String) path(...)} method does not encode curly braces 571 * (which might be part of a file name!). 572 * Instead it resolves them using {@linkplain WebTarget#matrixParam(String, Object...) matrix-parameters}. 573 * The matrix-parameters need to be encoded manually, too (at least I tried it and it failed, if I didn't). 574 * Because of these reasons and in order to make the calls more compact, we assemble the path 575 * ourselves here. 576 * @param pathSegments the parts of the path. May be <code>null</code>. The path segments are 577 * appended to the path as they are. They are not encoded at all! If you require encoding, 578 * use {@link #encodePath(String)} or {@link #urlEncode(String)} before! Furthermore, all path segments 579 * are separated with a slash inbetween them, but <i>not</i> at the end. If a single path segment 580 * already contains a slash, duplicate slashes might occur. 581 * @return the target. Never <code>null</code>. 582 */ 583 private WebTarget createWebTarget(String ... pathSegments) { 584 Client client = getClientOrFail(); 585 586 StringBuilder sb = new StringBuilder(); 587 sb.append(getBaseURL()); 588 589 boolean first = true; 590 if (pathSegments != null && pathSegments.length != 0) { 591 for (String pathSegment : pathSegments) { 592 if (!first) // the base-URL already ends with a slash! 593 sb.append('/'); 594 first = false; 595 sb.append(pathSegment); 596 } 597 } 598 599 WebTarget webTarget = client.target(URI.create(sb.toString())); 600 return webTarget; 601 } 602 603 /** 604 * Encodes the given {@code path} (using {@link #urlEncode(String)}) and removes leading & trailing slashes. 605 * <p> 606 * Slashes are not encoded, but retained as they are; only the path segments (the strings between the slashes) are 607 * encoded. 608 * <p> 609 * Duplicate slashes are removed. 610 * <p> 611 * The result of this method can be used in both URL-paths and URL-query-parameters. 612 * <p> 613 * For example the input "/some//ex ample///path/" becomes "some/ex%20ample/path". 614 * @param path the path to be encoded. Must not be <code>null</code>. 615 * @return the encoded path. Never <code>null</code>. 616 */ 617 private String encodePath(String path) { 618 assertNotNull("path", path); 619 620 StringBuilder sb = new StringBuilder(); 621 String[] segments = path.split("/"); 622 for (String segment : segments) { 623 if (segment.isEmpty()) 624 continue; 625 626 if (sb.length() != 0) 627 sb.append('/'); 628 629 sb.append(urlEncode(segment)); 630 } 631 632 return sb.toString(); 633 } 634 635 /** 636 * Encodes the given {@code string}. 637 * <p> 638 * This method does <i>not</i> use {@link java.net.URLEncoder URLEncoder}, because of 639 * <a href="https://java.net/jira/browse/JERSEY-417">JERSEY-417</a>. 640 * <p> 641 * The result of this method can be used in both URL-paths and URL-query-parameters. 642 * @param string the {@code String} to be encoded. Must not be <code>null</code>. 643 * @return the encoded {@code String}. 644 */ 645 private static String urlEncode(String string) { 646 assertNotNull("string", string); 647 // This UriComponent method is safe. It does not try to handle the '{' and '}' 648 // specially and with type PATH_SEGMENT, it encodes spaces using '%20' instead of '+'. 649 // It can therefore be used for *both* path segments *and* query parameters. 650 return UriComponent.encode(string, UriComponent.Type.PATH_SEGMENT); 651 } 652 653 public synchronized HostnameVerifier getHostnameVerifier() { 654 return hostnameVerifier; 655 } 656 public synchronized void setHostnameVerifier(HostnameVerifier hostnameVerifier) { 657 if (configFrozen) 658 throw new IllegalStateException("Config already frozen! Cannot change hostnameVerifier anymore!"); 659 660 this.hostnameVerifier = hostnameVerifier; 661 } 662 663 public synchronized SSLContext getSslContext() { 664 return sslContext; 665 } 666 public synchronized void setSslContext(SSLContext sslContext) { 667 if (configFrozen) 668 throw new IllegalStateException("Config already frozen! Cannot change sslContext anymore!"); 669 670 this.sslContext = sslContext; 671 } 672 673 private ThreadLocal<ClientRef> clientThreadLocal = new ThreadLocal<ClientRef>(); 674 675 private static class ClientRef { 676 public final Client client; 677 public int refCount = 1; 678 679 public ClientRef(Client client) { 680 this.client = assertNotNull("client", client); 681 } 682 } 683 684 /** 685 * Acquire a {@link Client} and bind it to the current thread. 686 * <p> 687 * <b>Important: You must {@linkplain #releaseClient() release} the client!</b> Use a try/finally block! 688 * @see #releaseClient() 689 * @see #getClientOrFail() 690 */ 691 private synchronized void acquireClient() 692 { 693 ClientRef clientRef = clientThreadLocal.get(); 694 if (clientRef != null) { 695 ++clientRef.refCount; 696 return; 697 } 698 699 Client client = clientCache.poll(); 700 if (client == null) { 701 SSLContext sslContext = this.sslContext; 702 HostnameVerifier hostnameVerifier = this.hostnameVerifier; 703 704 ClientConfig clientConfig = new ClientConfig(CloudStoreJaxbContextResolver.class); 705 clientConfig.property(ClientProperties.CONNECT_TIMEOUT, getSocketConnectTimeout()); // must be a java.lang.Integer 706 clientConfig.property(ClientProperties.READ_TIMEOUT, getSocketReadTimeout()); // must be a java.lang.Integer 707 708 ClientBuilder clientBuilder = ClientBuilder.newBuilder().withConfig(clientConfig); 709 710 if (sslContext != null) 711 clientBuilder.sslContext(sslContext); 712 713 if (hostnameVerifier != null) 714 clientBuilder.hostnameVerifier(hostnameVerifier); 715 716 clientBuilder.register(GZIPReaderInterceptor.class); 717 clientBuilder.register(GZIPWriterInterceptor.class); 718 719 client = clientBuilder.build(); 720 721 // An authentication is always required. Otherwise Jersey throws an exception. 722 // Hence, we set it to "anonymous" here and set it to the real values for those 723 // requests really requiring it. 724 HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic("anonymous", ""); 725 client.register(feature); 726 727 configFrozen = true; 728 } 729 clientThreadLocal.set(new ClientRef(client)); 730 } 731 732 /** 733 * Get the {@link Client} which was previously {@linkplain #acquireClient() acquired} (and not yet 734 * {@linkplain #releaseClient() released}) on the same thread. 735 * @return the {@link Client}. Never <code>null</code>. 736 * @throws IllegalStateException if there is no {@link Client} bound to the current thread. 737 * @see #acquireClient() 738 */ 739 private Client getClientOrFail() { 740 ClientRef clientRef = clientThreadLocal.get(); 741 if (clientRef == null) 742 throw new IllegalStateException("acquireClient() not called on the same thread (or releaseClient() already called)!"); 743 744 return clientRef.client; 745 } 746 747 /** 748 * Release a {@link Client} which was previously {@linkplain #acquireClient() acquired}. 749 * @see #acquireClient() 750 */ 751 private void releaseClient() { 752 ClientRef clientRef = clientThreadLocal.get(); 753 if (clientRef == null) 754 throw new IllegalStateException("acquireClient() not called on the same thread (or releaseClient() called more often than acquireClient())!"); 755 756 if (--clientRef.refCount == 0) { 757 clientThreadLocal.remove(); 758 clientCache.add(clientRef.client); 759 } 760 } 761 762 private void handleException(RuntimeException x) 763 { 764 Response response = null; 765 if (x instanceof WebApplicationException) 766 response = ((WebApplicationException)x).getResponse(); 767 else if (x instanceof ResponseProcessingException) 768 response = ((ResponseProcessingException)x).getResponse(); 769 770 if (response == null) 771 throw x; 772 773 // Instead of returning null, jersey throws a com.sun.jersey.api.client.UniformInterfaceException 774 // when the server does not send a result. We therefore check for the result code 204 here. 775 if (Response.Status.NO_CONTENT.getStatusCode() == response.getStatus()) 776 return; 777 778 Error error = null; 779 try { 780 response.bufferEntity(); 781 if (response.hasEntity()) 782 error = response.readEntity(Error.class); 783 784 if (error != null && DeferredCompletionException.class.getName().equals(error.getClassName())) 785 logger.debug("handleException: " + x, x); 786 else 787 logger.error("handleException: " + x, x); 788 789 } catch (Exception y) { 790 logger.error("handleException: " + x, x); 791 logger.error("handleException: " + y, y); 792 } 793 794 if (error != null) { 795 throwOriginalExceptionIfPossible(error); 796 throw new RemoteException(error); 797 } 798 799 throw x; 800 } 801 802 private void throwOriginalExceptionIfPossible(Error error) { 803 Class<?> clazz; 804 try { 805 clazz = Class.forName(error.getClassName()); 806 } catch (ClassNotFoundException e) { 807 return; 808 } 809 if (!Throwable.class.isAssignableFrom(clazz)) 810 return; 811 812 Object throwableO = null; 813 if (throwableO == null) { 814 throwableO = getObjectOrNull(clazz, new Class<?>[] { String.class }, error.getMessage()); 815 } 816 817 if (throwableO == null) { 818 throwableO = getObjectOrNull(clazz, null); 819 } 820 821 if (throwableO != null) { 822 Throwable throwable = (Throwable) throwableO; 823 throwable.initCause(new RemoteException(error)); 824 if (throwable instanceof RuntimeException) 825 throw (RuntimeException) throwable; 826 827 if (throwable instanceof java.lang.Error) 828 throw (java.lang.Error) throwable; 829 830 throw new RuntimeException(throwable); 831 } 832 } 833 834 private Object getObjectOrNull(Class<?> clazz, Class<?>[] argumentTypes, Object ... arguments) { 835 Object result = null; 836 if (argumentTypes == null) 837 argumentTypes = new Class<?> [0]; 838 839 if (argumentTypes.length == 0) { 840 try { 841 result = clazz.newInstance(); 842 } catch (InstantiationException e) { 843 return null; 844 } catch (IllegalAccessException e) { 845 return null; 846 } 847 } 848 849 if (result == null) { 850 Constructor<?> constructor; 851 try { 852 constructor = clazz.getConstructor(argumentTypes); 853 } catch (NoSuchMethodException e) { 854 return null; 855 } catch (SecurityException e) { 856 return null; 857 } 858 859 try { 860 result = constructor.newInstance(arguments); 861 } catch (InstantiationException e) { 862 return null; 863 } catch (IllegalAccessException e) { 864 return null; 865 } catch (IllegalArgumentException e) { 866 return null; 867 } catch (InvocationTargetException e) { 868 return null; 869 } 870 } 871 872 return result; 873 } 874 875 public CredentialsProvider getCredentialsProvider() { 876 return credentialsProvider; 877 } 878 private CredentialsProvider getCredentialsProviderOrFail() { 879 CredentialsProvider credentialsProvider = getCredentialsProvider(); 880 if (credentialsProvider == null) 881 throw new IllegalStateException("credentialsProvider == null"); 882 return credentialsProvider; 883 } 884 public void setCredentialsProvider(CredentialsProvider credentialsProvider) { 885 this.credentialsProvider = credentialsProvider; 886 } 887}