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 &amp; 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}