001package co.codewizards.cloudstore.ls.rest.client; 002 003import static co.codewizards.cloudstore.core.util.Util.*; 004import static java.util.Objects.*; 005 006import java.util.LinkedList; 007import java.util.List; 008import java.util.concurrent.CopyOnWriteArrayList; 009 010import javax.ws.rs.WebApplicationException; 011import javax.ws.rs.client.Client; 012import javax.ws.rs.client.ClientBuilder; 013import javax.ws.rs.client.Invocation; 014import javax.ws.rs.client.ResponseProcessingException; 015import javax.ws.rs.core.Response; 016 017import org.glassfish.jersey.client.ClientConfig; 018import org.glassfish.jersey.client.ClientProperties; 019import org.glassfish.jersey.client.authentication.HttpAuthenticationFeature; 020import org.slf4j.Logger; 021import org.slf4j.LoggerFactory; 022 023import co.codewizards.cloudstore.core.Uid; 024import co.codewizards.cloudstore.core.concurrent.DeferredCompletionException; 025import co.codewizards.cloudstore.core.config.Config; 026import co.codewizards.cloudstore.core.config.ConfigImpl; 027import co.codewizards.cloudstore.core.dto.Error; 028import co.codewizards.cloudstore.core.dto.RemoteException; 029import co.codewizards.cloudstore.core.dto.RemoteExceptionUtil; 030import co.codewizards.cloudstore.ls.core.LocalServerPropertiesManager; 031import co.codewizards.cloudstore.ls.core.provider.JavaNativeMessageBodyReader; 032import co.codewizards.cloudstore.ls.core.provider.JavaNativeMessageBodyWriter; 033import co.codewizards.cloudstore.ls.rest.client.request.Request; 034 035/** 036 * Client for executing REST requests. 037 * <p> 038 * An instance of this class is used to send data to, query data from or execute logic on the server. 039 * <p> 040 * If a series of multiple requests is to be sent to the server, it is recommended to keep an instance of 041 * this class (because it caches resources) and invoke multiple requests with it. 042 * <p> 043 * This class is thread-safe. 044 * @author Marco หงุ่ยตระกูล-Schulze - marco at codewizards dot co 045 */ 046public class LocalServerRestClient { 047 048 private static final Logger logger = LoggerFactory.getLogger(LocalServerRestClient.class); 049 050 private static final int DEFAULT_SOCKET_CONNECT_TIMEOUT = 30 * 1000; // 30 seconds should really be sufficient to connect to localhost 051 private static final int DEFAULT_SOCKET_READ_TIMEOUT = 3 * 60 * 1000; // 3 minutes is more than enough, because we have DelayedMethodInvocationResponse 052 053 /** 054 * The {@code key} for the connection timeout used with {@link Config#getPropertyAsInt(String, int)}. 055 * <p> 056 * The configuration can be overridden by a system property - see {@link Config#SYSTEM_PROPERTY_PREFIX}. 057 */ 058 public static final String CONFIG_KEY_SOCKET_CONNECT_TIMEOUT = "localServer.socket.connectTimeout"; //$NON-NLS-1$ 059 060 /** 061 * The {@code key} for the read timeout used with {@link Config#getPropertyAsInt(String, int)}. 062 * <p> 063 * The configuration can be overridden by a system property - see {@link Config#SYSTEM_PROPERTY_PREFIX}. 064 */ 065 public static final String CONFIG_KEY_SOCKET_READ_TIMEOUT = "localServer.socket.readTimeout"; //$NON-NLS-1$ 066 067 private Integer socketConnectTimeout; 068 069 private Integer socketReadTimeout; 070 071 private String baseUrl; 072 073 private final LinkedList<Client> clientCache = new LinkedList<Client>(); 074 075 private CredentialsProvider credentialsProvider; 076 077 private static final class Holder { 078 public static final LocalServerRestClient instance = new LocalServerRestClient(); 079 } 080 081 public static LocalServerRestClient getInstance() { 082 return Holder.instance; 083 } 084 085 public synchronized Integer getSocketConnectTimeout() { 086 if (socketConnectTimeout == null) 087 socketConnectTimeout = ConfigImpl.getInstance().getPropertyAsPositiveOrZeroInt( 088 CONFIG_KEY_SOCKET_CONNECT_TIMEOUT, 089 DEFAULT_SOCKET_CONNECT_TIMEOUT); 090 091 return socketConnectTimeout; 092 } 093 public synchronized void setSocketConnectTimeout(Integer socketConnectTimeout) { 094 if (socketConnectTimeout != null && socketConnectTimeout < 0) 095 socketConnectTimeout = null; 096 097 this.socketConnectTimeout = socketConnectTimeout; 098 } 099 100 public synchronized Integer getSocketReadTimeout() { 101 if (socketReadTimeout == null) 102 socketReadTimeout = ConfigImpl.getInstance().getPropertyAsPositiveOrZeroInt( 103 CONFIG_KEY_SOCKET_READ_TIMEOUT, 104 DEFAULT_SOCKET_READ_TIMEOUT); 105 106 return socketReadTimeout; 107 } 108 public synchronized void setSocketReadTimeout(Integer socketReadTimeout) { 109 if (socketReadTimeout != null && socketReadTimeout < 0) 110 socketReadTimeout = null; 111 112 this.socketReadTimeout = socketReadTimeout; 113 } 114 115 /** 116 * Get the server's base-URL. 117 * <p> 118 * This base-URL is the base of the <code>LocalServerRest</code> application. Hence all URLs 119 * beneath this base-URL are processed by the <code>LocalServerRest</code> application. 120 * @return the base-URL. This URL always ends with "/". 121 */ 122 public synchronized String getBaseUrl() { 123 if (baseUrl == null) 124 baseUrl = LocalServerPropertiesManager.getInstance().getBaseUrl(); 125 126 return baseUrl; 127 } 128 129 /** 130 * Create a new client. 131 */ 132 protected LocalServerRestClient() { 133 // The clientId is used for memory management in the server: if a client is closed or disappears, i.e. doesn't 134 // send keep-alives regularly, anymore, the server removes all objectRef-references kept for this client in its ObjectManager. 135 final String clientId = new Uid().toString(); 136 137 setCredentialsProvider(new CredentialsProvider() { 138 @Override 139 public String getUserName() { 140 return clientId; 141 } 142 @Override 143 public String getPassword() { 144 return LocalServerPropertiesManager.getInstance().getPassword(); 145 } 146 }); 147 } 148 149// private static String appendFinalSlashIfNeeded(final String url) { 150// return url.endsWith("/") ? url : url + "/"; 151// } 152 153 public <R> R execute(final Request<R> request) { 154 requireNonNull(request, "request"); 155 RuntimeException firstException = null; 156 int retryCounter = 0; // *re*-try: first (normal) invocation is 0, first re-try is 1 157 final int retryMax = 1; // *re*-try: 1 retries means 2 invocations in total 158 while (true) { 159 acquireClient(); 160 try { 161 final long start = System.currentTimeMillis(); 162 163 if (logger.isDebugEnabled()) 164 logger.debug("execute: starting try {} of {}", retryCounter + 1, retryMax + 1); 165 166 try { 167 request.setLocalServerRestClient(this); 168 final R result = request.execute(); 169 170 if (logger.isDebugEnabled()) 171 logger.debug("execute: invocation took {} ms", System.currentTimeMillis() - start); 172 173 if (result == null && !request.isResultNullable()) 174 throw new IllegalStateException("result == null, but request.resultNullable == false!"); 175 176 return result; 177 } catch (final RuntimeException x) { 178 if (firstException == null) 179 firstException = x; 180 181 final String oldBaseUrl = getBaseUrl(); 182 baseUrl = null; 183 if (!oldBaseUrl.equals(getBaseUrl())) { 184 retryCounter = 0; // reset to make sure we really try again with the new URL 185 clearClientCache(); 186 } 187 188 markClientBroken(); // make sure we do not reuse this client 189 if (++retryCounter > retryMax || !retryExecuteAfterException(x)) { 190 logger.warn("execute: invocation failed (will NOT retry): " + x, x); 191 handleAndRethrowException(firstException); // TODO maybe we should make a MultiCauseException?! 192 throw firstException; 193 } 194 logger.warn("execute: invocation failed (will retry): " + x, x); 195 196 // Wait a bit before retrying (increasingly longer). 197 try { Thread.sleep(retryCounter * 1000L); } catch (Exception y) { doNothing(); } 198 } 199 } finally { 200 releaseClient(); 201 request.setLocalServerRestClient(null); 202 } 203 } 204 } 205 206 private synchronized void clearClientCache() { 207 clientCache.clear(); 208 } 209 210 private boolean retryExecuteAfterException(final Exception x) { 211// final Class<?>[] exceptionClassesCausingRetry = new Class<?>[] { 212// SSLException.class, 213// SocketException.class 214// }; 215// for (final Class<?> exceptionClass : exceptionClassesCausingRetry) { 216// @SuppressWarnings("unchecked") 217// final Class<? extends Throwable> xc = (Class<? extends Throwable>) exceptionClass; 218// if (ExceptionUtil.getCause(x, xc) != null) { 219// logger.warn( 220// String.format("retryExecuteAfterException: Encountered %s and will retry.", xc.getSimpleName()), 221// x); 222// return true; 223// } 224// } 225// return false; 226 return true; 227 } 228 229 public Invocation.Builder assignCredentials(final Invocation.Builder builder) { 230 final CredentialsProvider credentialsProvider = getCredentialsProviderOrFail(); 231 builder.property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_USERNAME, credentialsProvider.getUserName()); 232 builder.property(HttpAuthenticationFeature.HTTP_AUTHENTICATION_BASIC_PASSWORD, credentialsProvider.getPassword()); 233 return builder; 234 } 235 236 private final ThreadLocal<ClientRef> clientThreadLocal = new ThreadLocal<ClientRef>(); 237 238 private static class ClientRef { 239 public final Client client; 240 public int refCount = 1; 241 public boolean broken; 242 243 public ClientRef(final Client client) { 244 this.client = requireNonNull(client, "client"); 245 } 246 } 247 248 /** 249 * Acquire a {@link Client} and bind it to the current thread. 250 * <p> 251 * <b>Important: You must {@linkplain #releaseClient() release} the client!</b> Use a try/finally block! 252 * @see #releaseClient() 253 * @see #getClientOrFail() 254 */ 255 private synchronized void acquireClient() 256 { 257 final ClientRef clientRef = clientThreadLocal.get(); 258 if (clientRef != null) { 259 ++clientRef.refCount; 260 return; 261 } 262 263 Client client = clientCache.poll(); 264 if (client == null) { 265 final ClientConfig clientConfig = new ClientConfig(CloudStoreJaxbContextResolver.class); 266 clientConfig.property(ClientProperties.CONNECT_TIMEOUT, getSocketConnectTimeout()); // must be a java.lang.Integer 267 clientConfig.property(ClientProperties.READ_TIMEOUT, getSocketReadTimeout()); // must be a java.lang.Integer 268 269 final ClientBuilder clientBuilder = ClientBuilder.newBuilder().withConfig(clientConfig); 270 271 clientBuilder.register(JavaNativeMessageBodyReader.class); 272 clientBuilder.register(JavaNativeMessageBodyWriter.class); 273 274 for (final Object restComponent : restComponents) { 275 if (restComponent instanceof Class<?>) 276 clientBuilder.register((Class<?>) restComponent); 277 else 278 clientBuilder.register(restComponent); 279 } 280 281 client = clientBuilder.build(); 282 283 // An authentication is always required. Otherwise Jersey throws an exception. 284 // Hence, we set it to "anonymous" here and set it to the real values for those 285 // requests really requiring it. 286 final HttpAuthenticationFeature feature = HttpAuthenticationFeature.basic("anonymous", ""); 287 client.register(feature); 288 289// configFrozen = true; 290 } 291 clientThreadLocal.set(new ClientRef(client)); 292 } 293 294 private final List<Object> restComponents = new CopyOnWriteArrayList<Object>(); 295 296 public void registerRestComponent(Object restComponent) { 297 restComponents.add(restComponent); 298 } 299 300 /** 301 * Get the {@link Client} which was previously {@linkplain #acquireClient() acquired} (and not yet 302 * {@linkplain #releaseClient() released}) on the same thread. 303 * @return the {@link Client}. Never <code>null</code>. 304 * @throws IllegalStateException if there is no {@link Client} bound to the current thread. 305 * @see #acquireClient() 306 */ 307 public Client getClientOrFail() { 308 final ClientRef clientRef = clientThreadLocal.get(); 309 if (clientRef == null) 310 throw new IllegalStateException("acquireClient() not called on the same thread (or releaseClient() already called)!"); 311 312 return clientRef.client; 313 } 314 315 /** 316 * Release a {@link Client} which was previously {@linkplain #acquireClient() acquired}. 317 * @see #acquireClient() 318 */ 319 private synchronized void releaseClient() { 320 final ClientRef clientRef = clientThreadLocal.get(); 321 if (clientRef == null) 322 throw new IllegalStateException("acquireClient() not called on the same thread (or releaseClient() called more often than acquireClient())!"); 323 324 if (--clientRef.refCount == 0) { 325 clientThreadLocal.remove(); 326 327 if (!clientRef.broken) 328 clientCache.add(clientRef.client); 329 } 330 } 331 332 private void markClientBroken() { 333 final ClientRef clientRef = clientThreadLocal.get(); 334 if (clientRef == null) 335 throw new IllegalStateException("acquireClient() not called on the same thread (or releaseClient() called more often than acquireClient())!"); 336 337 clientRef.broken = true; 338 } 339 340 public void handleAndRethrowException(final RuntimeException x) 341 { 342 Response response = null; 343 if (x instanceof WebApplicationException) 344 response = ((WebApplicationException)x).getResponse(); 345 else if (x instanceof ResponseProcessingException) 346 response = ((ResponseProcessingException)x).getResponse(); 347 348 if (response == null) 349 throw x; 350 351 Error error = null; 352 try { 353 response.bufferEntity(); 354 if (response.hasEntity()) 355 error = response.readEntity(Error.class); 356 357 if (error != null && DeferredCompletionException.class.getName().equals(error.getClassName())) 358 logger.debug("handleException: " + x, x); 359 else 360 logger.error("handleException: " + x, x); 361 362 } catch (final Exception y) { 363 logger.error("handleException: " + x, x); 364 logger.error("handleException: " + y, y); 365 } 366 367 if (error != null) { 368 RemoteExceptionUtil.throwOriginalExceptionIfPossible(error); 369 throw new RemoteException(error); 370 } 371 372 throw x; 373 } 374 375 public CredentialsProvider getCredentialsProvider() { 376 return credentialsProvider; 377 } 378 private CredentialsProvider getCredentialsProviderOrFail() { 379 final CredentialsProvider credentialsProvider = getCredentialsProvider(); 380 if (credentialsProvider == null) 381 throw new IllegalStateException("credentialsProvider == null"); 382 return credentialsProvider; 383 } 384 public void setCredentialsProvider(final CredentialsProvider credentialsProvider) { 385 this.credentialsProvider = credentialsProvider; 386 } 387}