From 161fc450487342ccb76aad053768c5c9aad64fe1 Mon Sep 17 00:00:00 2001 From: evgeny Date: Mon, 23 Sep 2024 16:27:33 +0100 Subject: [PATCH 1/2] chore: rearrange code before refactoring --- .../main/java/io/ably/lib/http/HttpCore.java | 268 ++++++++++-------- .../lib/transport/WebSocketTransport.java | 182 ++++++------ 2 files changed, 229 insertions(+), 221 deletions(-) diff --git a/lib/src/main/java/io/ably/lib/http/HttpCore.java b/lib/src/main/java/io/ably/lib/http/HttpCore.java index e224262c6..7b3bb64bb 100644 --- a/lib/src/main/java/io/ably/lib/http/HttpCore.java +++ b/lib/src/main/java/io/ably/lib/http/HttpCore.java @@ -1,21 +1,6 @@ package io.ably.lib.http; -import java.io.ByteArrayOutputStream; -import java.io.InputStream; -import java.io.IOException; -import java.io.OutputStream; -import java.lang.reflect.Field; -import java.net.HttpURLConnection; -import java.net.InetSocketAddress; -import java.net.Proxy; -import java.net.URL; -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - import com.google.gson.JsonParseException; - import io.ably.lib.debug.DebugOptions; import io.ably.lib.debug.DebugOptions.RawHttpListener; import io.ably.lib.rest.Auth; @@ -32,11 +17,56 @@ import io.ably.lib.util.Log; import io.ably.lib.util.PlatformAgentProvider; +import java.io.ByteArrayOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.lang.reflect.Field; +import java.net.HttpURLConnection; +import java.net.InetSocketAddress; +import java.net.Proxy; +import java.net.URL; +import java.util.HashMap; +import java.util.List; +import java.util.Locale; +import java.util.Map; + /** * HttpCore performs authenticated HTTP synchronously. Internal; use Http or HttpScheduler instead. */ public class HttpCore { + private static final String TAG = HttpCore.class.getName(); + + /************************* + * Private state + *************************/ + + static { + /* if on Android, check version */ + Field androidVersionField = null; + int androidVersion = 0; + try { + androidVersionField = Class.forName("android.os.Build$VERSION").getField("SDK_INT"); + androidVersion = androidVersionField.getInt(androidVersionField); + } catch (Exception e) { + } + if (androidVersionField != null && androidVersion < 8) { + /* HTTP connection reuse which was buggy pre-froyo */ + System.setProperty("httpCore.keepAlive", "false"); + } + } + + public final String scheme; + public final int port; + final ClientOptions options; + final Hosts hosts; + private final Auth auth; + private final ProxyOptions proxyOptions; + private final PlatformAgentProvider platformAgentProvider; + private HttpAuth proxyAuth; + private Proxy proxy = Proxy.NO_PROXY; + /************************* * Public API *************************/ @@ -50,16 +80,22 @@ public HttpCore(ClientOptions options, Auth auth, PlatformAgentProvider platform this.hosts = new Hosts(options.restHost, Defaults.HOST_REST, options); this.proxyOptions = options.proxy; - if(proxyOptions != null) { + if (proxyOptions != null) { String proxyHost = proxyOptions.host; - if(proxyHost == null) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy host", 40000, 400)); } + if (proxyHost == null) { + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy host", 40000, 400)); + } int proxyPort = proxyOptions.port; - if(proxyPort == 0) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy port", 40000, 400)); } + if (proxyPort == 0) { + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy port", 40000, 400)); + } this.proxy = new Proxy(Proxy.Type.HTTP, new InetSocketAddress(proxyHost, proxyPort)); String proxyUser = proxyOptions.username; - if(proxyUser != null) { + if (proxyUser != null) { String proxyPassword = proxyOptions.password; - if(proxyPassword == null) { throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy password", 40000, 400)); } + if (proxyPassword == null) { + throw AblyException.fromErrorInfo(new ErrorInfo("Unable to configure proxy without proxy password", 40000, 400)); + } proxyAuth = new HttpAuth(proxyUser, proxyPassword, proxyOptions.prefAuthType); } } @@ -67,6 +103,7 @@ public HttpCore(ClientOptions options, Auth auth, PlatformAgentProvider platform /** * Make a synchronous HTTP request specified by URL and proxy, retrying if necessary on WWW-Authenticate + * * @param url * @param method * @param headers @@ -77,21 +114,21 @@ public HttpCore(ClientOptions options, Auth auth, PlatformAgentProvider platform */ public T httpExecuteWithRetry(URL url, String method, Param[] headers, RequestBody requestBody, ResponseHandler responseHandler, boolean requireAblyAuth) throws AblyException { boolean renewPending = true, proxyAuthPending = true; - if(requireAblyAuth) { + if (requireAblyAuth) { authorize(false); } - while(true) { + while (true) { try { return httpExecute(url, getProxy(url), method, headers, requestBody, true, responseHandler); - } catch(AuthRequiredException are) { - if(are.authChallenge != null && requireAblyAuth) { - if(are.expired && renewPending) { + } catch (AuthRequiredException are) { + if (are.authChallenge != null && requireAblyAuth) { + if (are.expired && renewPending) { authorize(true); renewPending = false; continue; } } - if(are.proxyAuthChallenge != null && proxyAuthPending && proxyAuth != null) { + if (are.proxyAuthChallenge != null && proxyAuthPending && proxyAuth != null) { proxyAuth.processAuthenticateHeaders(are.proxyAuthChallenge); proxyAuthPending = false; continue; @@ -102,22 +139,21 @@ public T httpExecuteWithRetry(URL url, String method, Param[] headers, Reque } /** - * Sets host for this HTTP client + * Gets host for this HTTP client * - * @param host URL string + * @return */ - public void setPreferredHost(String host) { - hosts.setPreferredHost(host, false); + public String getPreferredHost() { + return hosts.getPreferredHost(); } /** - * Gets host for this HTTP client + * Sets host for this HTTP client * - * @return - + * @param host URL string */ - public String getPreferredHost() { - return hosts.getPreferredHost(); + public void setPreferredHost(String host) { + hosts.setPreferredHost(host, false); } /** @@ -139,6 +175,7 @@ void authorize(boolean renew) throws AblyException { /** * Make a synchronous HTTP request specified by URL and proxy + * * @param url * @param proxy * @param method @@ -152,13 +189,13 @@ void authorize(boolean renew) throws AblyException { public T httpExecute(URL url, Proxy proxy, String method, Param[] headers, RequestBody requestBody, boolean withCredentials, ResponseHandler responseHandler) throws AblyException { HttpURLConnection conn = null; try { - conn = (HttpURLConnection)url.openConnection(proxy); + conn = (HttpURLConnection) url.openConnection(proxy); boolean withProxyCredentials = (proxy != Proxy.NO_PROXY) && (proxyAuth != null); return httpExecute(conn, method, headers, requestBody, withCredentials, withProxyCredentials, responseHandler); - } catch(IOException ioe) { + } catch (IOException ioe) { throw AblyException.fromThrowable(ioe); } finally { - if(conn != null) { + if (conn != null) { conn.disconnect(); } } @@ -166,6 +203,7 @@ public T httpExecute(URL url, Proxy proxy, String method, Param[] headers, R /** * Make a synchronous HTTP request with a given HttpURLConnection + * * @param conn * @param method * @param headers @@ -191,32 +229,37 @@ T httpExecute(HttpURLConnection conn, String method, Param[] headers, Reques if (authHeader == null && auth != null) { authHeader = auth.getAuthorizationHeader(); } - if(withCredentials && authHeader != null) { + if (withCredentials && authHeader != null) { conn.setRequestProperty(HttpConstants.Headers.AUTHORIZATION, authHeader); credentialsIncluded = true; } - if(withProxyCredentials && proxyAuth.hasChallenge()) { + if (withProxyCredentials && proxyAuth.hasChallenge()) { byte[] encodedRequestBody = (requestBody != null) ? requestBody.getEncoded() : null; String proxyAuthorizationHeader = proxyAuth.getAuthorizationHeader(method, conn.getURL().getPath(), encodedRequestBody); conn.setRequestProperty(HttpConstants.Headers.PROXY_AUTHORIZATION, proxyAuthorizationHeader); } boolean acceptSet = false; - if(headers != null) { - for(Param header: headers) { + if (headers != null) { + for (Param header : headers) { conn.setRequestProperty(header.key, header.value); - if(header.key.equals(HttpConstants.Headers.ACCEPT)) { acceptSet = true; } + if (header.key.equals(HttpConstants.Headers.ACCEPT)) { + acceptSet = true; + } } } - if(!acceptSet) { conn.setRequestProperty(HttpConstants.Headers.ACCEPT, HttpConstants.ContentTypes.JSON); } + if (!acceptSet) { + conn.setRequestProperty(HttpConstants.Headers.ACCEPT, HttpConstants.ContentTypes.JSON); + } /* pass required headers */ conn.setRequestProperty(Defaults.ABLY_PROTOCOL_VERSION_HEADER, Defaults.ABLY_PROTOCOL_VERSION); // RSC7a conn.setRequestProperty(Defaults.ABLY_AGENT_HEADER, AgentHeaderCreator.create(options.agents, platformAgentProvider)); - if (options.clientId != null) conn.setRequestProperty(Defaults.ABLY_CLIENT_ID_HEADER, Base64Coder.encodeString(options.clientId)); + if (options.clientId != null) + conn.setRequestProperty(Defaults.ABLY_CLIENT_ID_HEADER, Base64Coder.encodeString(options.clientId)); /* prepare request body */ byte[] body = null; - if(requestBody != null) { + if (requestBody != null) { body = prepareRequestBody(requestBody, conn); // Check the logging level to avoid performance hit associated with building the message if (Log.level <= Log.VERBOSE) @@ -235,9 +278,9 @@ T httpExecute(HttpURLConnection conn, String method, Param[] headers, Reques Log.v(TAG, " " + entry.getKey() + ": " + val); } - if(options instanceof DebugOptions) { - rawHttpListener = ((DebugOptions)options).httpListener; - if(rawHttpListener != null) { + if (options instanceof DebugOptions) { + rawHttpListener = ((DebugOptions) options).httpListener; + if (rawHttpListener != null) { id = String.valueOf(Math.random()).substring(2); response = rawHttpListener.onRawHttpRequest(id, conn, method, (credentialsIncluded ? authHeader : null), requestProperties, requestBody); if (response != null) { @@ -247,15 +290,15 @@ T httpExecute(HttpURLConnection conn, String method, Param[] headers, Reques } /* send request body */ - if(requestBody != null) { + if (requestBody != null) { writeRequestBody(body, conn); } response = readResponse(conn); - if(rawHttpListener != null) { + if (rawHttpListener != null) { rawHttpListener.onRawHttpResponse(id, method, response); } - } catch(IOException ioe) { - if(rawHttpListener != null) { + } catch (IOException ioe) { + if (rawHttpListener != null) { rawHttpListener.onRawHttpException(id, method, ioe); } throw AblyException.fromThrowable(ioe); @@ -266,6 +309,7 @@ T httpExecute(HttpURLConnection conn, String method, Param[] headers, Reques /** * Handle HTTP response + * * @param conn * @param credentialsIncluded * @param response @@ -278,19 +322,19 @@ private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded return null; } - if (response.statusCode >=500 && response.statusCode <= 504) { + if (response.statusCode >= 500 && response.statusCode <= 504) { ErrorInfo error = ErrorInfo.fromResponseStatus(response.statusLine, response.statusCode); throw AblyException.fromErrorInfo(error); } - if(response.statusCode >= 200 && response.statusCode < 300) { + if (response.statusCode >= 200 && response.statusCode < 300) { return (responseHandler != null) ? responseHandler.handleResponse(response, null) : null; } /* get any in-body error details */ ErrorInfo error = null; - if(response.body != null && response.body.length > 0) { - if(response.contentType != null && response.contentType.contains("msgpack")) { + if (response.body != null && response.body.length > 0) { + if (response.contentType != null && response.contentType.contains("msgpack")) { try { error = ErrorInfo.fromMsgpackBody(response.body); } catch (IOException e) { @@ -302,10 +346,10 @@ private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded String bodyText = new String(response.body); try { ErrorResponse errorResponse = ErrorResponse.fromJSON(bodyText); - if(errorResponse != null) { + if (errorResponse != null) { error = errorResponse.error; } - } catch(JsonParseException jse) { + } catch (JsonParseException jse) { /* error pages aren't necessarily going to satisfy our Accept criteria ... */ System.err.println("Error message in unexpected format: " + bodyText); } @@ -313,50 +357,53 @@ private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded } /* handle error details in header */ - if(error == null) { + if (error == null) { String errorCodeHeader = conn.getHeaderField("X-Ably-ErrorCode"); String errorMessageHeader = conn.getHeaderField("X-Ably-ErrorMessage"); - if(errorCodeHeader != null) { + if (errorCodeHeader != null) { try { error = new ErrorInfo(errorMessageHeader, response.statusCode, Integer.parseInt(errorCodeHeader)); - } catch(NumberFormatException e) {} + } catch (NumberFormatException e) { + } } } /* handle www-authenticate */ - if(response.statusCode == 401) { + if (response.statusCode == 401) { boolean stale = (error != null && error.code == 40140); List wwwAuthHeaders = response.getHeaderFields(HttpConstants.Headers.WWW_AUTHENTICATE); - if(wwwAuthHeaders != null && wwwAuthHeaders.size() > 0) { + if (wwwAuthHeaders != null && wwwAuthHeaders.size() > 0) { Map headersByType = HttpAuth.sortAuthenticateHeaders(wwwAuthHeaders); String tokenHeader = headersByType.get(HttpAuth.Type.X_ABLY_TOKEN); - if(tokenHeader != null) { stale |= (tokenHeader.indexOf("stale") > -1); } + if (tokenHeader != null) { + stale |= (tokenHeader.indexOf("stale") > -1); + } AuthRequiredException exception = new AuthRequiredException(null, error); exception.authChallenge = headersByType; - if(stale) { + if (stale) { exception.expired = true; throw exception; } - if(!credentialsIncluded) { + if (!credentialsIncluded) { throw exception; } } } /* handle proxy-authenticate */ - if(response.statusCode == 407) { + if (response.statusCode == 407) { List proxyAuthHeaders = response.getHeaderFields(HttpConstants.Headers.PROXY_AUTHENTICATE); - if(proxyAuthHeaders != null && proxyAuthHeaders.size() > 0) { + if (proxyAuthHeaders != null && proxyAuthHeaders.size() > 0) { AuthRequiredException exception = new AuthRequiredException(null, error); exception.proxyAuthChallenge = HttpAuth.sortAuthenticateHeaders(proxyAuthHeaders); throw exception; } } - if(error == null) { + if (error == null) { error = ErrorInfo.fromResponseStatus(response.statusLine, response.statusCode); } else { } - Log.e(TAG, "Error response from server: err = " + error.toString()); - if(responseHandler != null) { + Log.e(TAG, "Error response from server: err = " + error); + if (responseHandler != null) { return responseHandler.handleResponse(response, error); } throw AblyException.fromErrorInfo(error); @@ -364,6 +411,7 @@ private T handleResponse(HttpURLConnection conn, boolean credentialsIncluded /** * Emit the request body for an HTTP request + * * @param requestBody * @param conn * @return body @@ -386,6 +434,7 @@ private void writeRequestBody(byte[] body, HttpURLConnection conn) throws IOExce /** * Read the response for an HTTP request + * * @param connection * @return * @throws IOException @@ -410,7 +459,7 @@ private Response readResponse(HttpURLConnection connection) throws IOException { } } - if(response.statusCode == HttpURLConnection.HTTP_NO_CONTENT) { + if (response.statusCode == HttpURLConnection.HTTP_NO_CONTENT) { return response; } @@ -420,7 +469,8 @@ private Response readResponse(HttpURLConnection connection) throws IOException { InputStream is = null; try { is = connection.getInputStream(); - } catch (Throwable e) {} + } catch (Throwable e) { + } if (is == null) is = connection.getErrorStream(); @@ -433,7 +483,8 @@ private Response readResponse(HttpURLConnection connection) throws IOException { if (is != null) { try { is.close(); - } catch (IOException e) {} + } catch (IOException e) { + } } } @@ -451,16 +502,15 @@ private byte[] readInputStream(InputStream inputStream, int bytes) throws IOExce if (bytes == -1) { ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); byte[] buffer = new byte[4 * 1024]; - while((bytesRead = inputStream.read(buffer)) > -1) { + while ((bytesRead = inputStream.read(buffer)) > -1) { outputStream.write(buffer, 0, bytesRead); } return outputStream.toByteArray(); - } - else { + } else { int idx = 0; byte[] output = new byte[bytes]; - while((bytesRead = inputStream.read(output, idx, bytes - idx)) > -1) { + while ((bytesRead = inputStream.read(output, idx, bytes - idx)) > -1) { idx += bytesRead; } @@ -474,11 +524,11 @@ Proxy getProxy(URL url) { } private Proxy getProxy(String host) { - if(proxyOptions != null) { + if (proxyOptions != null) { String[] nonProxyHosts = proxyOptions.nonProxyHosts; - if(nonProxyHosts != null) { - for(String nonProxyHostPattern : nonProxyHosts) { - if(host.matches(nonProxyHostPattern)) { + if (nonProxyHosts != null) { + for (String nonProxyHostPattern : nonProxyHosts) { + if (host.matches(nonProxyHostPattern)) { return null; } } @@ -487,47 +537,18 @@ private Proxy getProxy(String host) { return proxy; } - /************************* - * Private state - *************************/ - - static { - /* if on Android, check version */ - Field androidVersionField = null; - int androidVersion = 0; - try { - androidVersionField = Class.forName("android.os.Build$VERSION").getField("SDK_INT"); - androidVersion = androidVersionField.getInt(androidVersionField); - } catch (Exception e) {} - if(androidVersionField != null && androidVersion < 8) { - /* HTTP connection reuse which was buggy pre-froyo */ - System.setProperty("httpCore.keepAlive", "false"); - } - } - - public final String scheme; - public final int port; - final ClientOptions options; - final Hosts hosts; - - private final Auth auth; - private final ProxyOptions proxyOptions; - private HttpAuth proxyAuth; - private Proxy proxy = Proxy.NO_PROXY; - private final PlatformAgentProvider platformAgentProvider; - - private static final String TAG = HttpCore.class.getName(); - /** * Interface for an entity that supplies an httpCore request body */ public interface RequestBody { byte[] getEncoded(); + String getContentType(); } /** * Interface for an entity that performs type-specific processing on an httpCore response body + * * @param */ public interface BodyHandler { @@ -536,6 +557,7 @@ public interface BodyHandler { /** * Interface for an entity that performs type-specific processing on an httpCore response + * * @param */ public interface ResponseHandler { @@ -548,7 +570,7 @@ public interface ResponseHandler { public static class Response { public int statusCode; public String statusLine; - public Map> headers; + public Map> headers; public String contentType; public int contentLength; public byte[] body; @@ -559,13 +581,12 @@ public static class Response { * If called on a connection that sets the same header multiple times * with possibly different values, only the last value is returned. * - * - * @param name the name of a header field. - * @return the value of the named header field, or {@code null} - * if there is no such field in the header. + * @param name the name of a header field. + * @return the value of the named header field, or {@code null} + * if there is no such field in the header. */ public List getHeaderFields(String name) { - if(headers == null) { + if (headers == null) { return null; } @@ -578,11 +599,12 @@ public List getHeaderFields(String name) { */ public static class AuthRequiredException extends AblyException { private static final long serialVersionUID = 1L; - public AuthRequiredException(Throwable throwable, ErrorInfo reason) { - super(throwable, reason); - } public boolean expired; public Map authChallenge; public Map proxyAuthChallenge; + + public AuthRequiredException(Throwable throwable, ErrorInfo reason) { + super(throwable, reason); + } } } diff --git a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java index ba3399b7e..c389be18c 100644 --- a/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java +++ b/lib/src/main/java/io/ably/lib/transport/WebSocketTransport.java @@ -7,39 +7,50 @@ import io.ably.lib.types.ProtocolMessage; import io.ably.lib.types.ProtocolSerializer; import io.ably.lib.util.Log; - -import java.net.URI; -import java.nio.ByteBuffer; -import java.util.Timer; -import java.util.TimerTask; - -import javax.net.ssl.HttpsURLConnection; -import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLParameters; -import javax.net.ssl.SSLSession; - +import org.java_websocket.WebSocket; import org.java_websocket.client.WebSocketClient; import org.java_websocket.exceptions.WebsocketNotConnectedException; import org.java_websocket.framing.CloseFrame; import org.java_websocket.framing.Framedata; import org.java_websocket.handshake.ServerHandshake; -import org.java_websocket.WebSocket; + +import javax.net.ssl.HttpsURLConnection; +import javax.net.ssl.SSLContext; +import javax.net.ssl.SSLParameters; +import javax.net.ssl.SSLSession; +import java.net.URI; +import java.nio.ByteBuffer; +import java.util.Timer; +import java.util.TimerTask; public class WebSocketTransport implements ITransport { private static final String TAG = WebSocketTransport.class.getName(); - + private static final int NEVER_CONNECTED = -1; + private static final int BUGGYCLOSE = -2; + private static final int CLOSE_NORMAL = 1000; + private static final int GOING_AWAY = 1001; + private static final int CLOSE_PROTOCOL_ERROR = 1002; + private static final int REFUSE = 1003; + /* private static final int UNUSED = 1004; */ + /* private static final int NOCODE = 1005; */ + private static final int ABNORMAL_CLOSE = 1006; + private static final int NO_UTF8 = 1007; + private static final int POLICY_VALIDATION = 1008; + private static final int TOOBIG = 1009; + private static final int EXTENSION = 1010; + private static final int UNEXPECTED_CONDITION = 1011; + private static final int TLS_ERROR = 1015; /****************** - * public factory API + * private members ******************/ - public static class Factory implements ITransport.Factory { - @Override - public WebSocketTransport getTransport(TransportParams params, ConnectionManager connectionManager) { - return new WebSocketTransport(params, connectionManager); - } - } - + private final TransportParams params; + private final ConnectionManager connectionManager; + private final boolean channelBinaryMode; + private String wsUri; + private ConnectListener connectListener; + private WsClient wsConnection; /****************** * protected constructor ******************/ @@ -65,24 +76,24 @@ public void connect(ConnectListener connectListener) { wsUri = wsScheme + params.host + ':' + params.port + "/"; Param[] authParams = connectionManager.ably.auth.getAuthParams(); Param[] connectParams = params.getConnectParams(authParams); - if(connectParams.length > 0) + if (connectParams.length > 0) wsUri = HttpUtils.encodeParams(wsUri, connectParams); Log.d(TAG, "connect(); wsUri = " + wsUri); - synchronized(this) { + synchronized (this) { wsConnection = new WsClient(URI.create(wsUri), this::receive); - if(isTls) { + if (isTls) { SSLContext sslContext = SSLContext.getInstance("TLS"); - sslContext.init( null, null, null ); + sslContext.init(null, null, null); SafeSSLSocketFactory factory = new SafeSSLSocketFactory(sslContext.getSocketFactory()); wsConnection.setSocketFactory(factory); } } wsConnection.connect(); - } catch(AblyException e) { + } catch (AblyException e) { Log.e(TAG, "Unexpected exception attempting connection; wsUri = " + wsUri, e); connectListener.onTransportUnavailable(this, e.errorInfo); - } catch(Throwable t) { + } catch (Throwable t) { Log.e(TAG, "Unexpected exception attempting connection; wsUri = " + wsUri, t); connectListener.onTransportUnavailable(this, AblyException.fromThrowable(t).errorInfo); } @@ -91,8 +102,8 @@ public void connect(ConnectListener connectListener) { @Override public void close() { Log.d(TAG, "close()"); - synchronized(this) { - if(wsConnection != null) { + synchronized (this) { + if (wsConnection != null) { wsConnection.close(); wsConnection = null; } @@ -108,7 +119,7 @@ public void receive(ProtocolMessage msg) throws AblyException { public void send(ProtocolMessage msg) throws AblyException { Log.d(TAG, "send(); action = " + msg.action); try { - if(channelBinaryMode) { + if (channelBinaryMode) { byte[] encodedMsg = ProtocolSerializer.writeMsgpack(msg); // Check the logging level to avoid performance hit associated with building the message @@ -123,14 +134,12 @@ public void send(ProtocolMessage msg) throws AblyException { Log.v(TAG, "send(): " + new String(ProtocolSerializer.writeJSON(msg))); wsConnection.send(ProtocolSerializer.writeJSON(msg)); } - } - catch (WebsocketNotConnectedException e){ - if(connectListener != null) { + } catch (WebsocketNotConnectedException e) { + if (connectListener != null) { connectListener.onTransportUnavailable(this, AblyException.fromThrowable(e).errorInfo); } else throw AblyException.fromThrowable(e); - } - catch (Exception e) { + } catch (Exception e) { throw AblyException.fromThrowable(e); } } @@ -140,22 +149,47 @@ public String getHost() { return params.host; } - protected void preProcessReceivedMessage(ProtocolMessage message) - { + protected void preProcessReceivedMessage(ProtocolMessage message) { //Gives the chance to child classes to do message pre-processing } + public String toString() { + return WebSocketTransport.class.getName() + " {" + getURL() + "}"; + } + + public String getURL() { + return wsUri; + } //interface to transfer Protocol message from websocket interface WebSocketReceiver { void onMessage(ProtocolMessage protocolMessage) throws AblyException; } + /****************** + * public factory API + ******************/ + + public static class Factory implements ITransport.Factory { + @Override + public WebSocketTransport getTransport(TransportParams params, ConnectionManager connectionManager) { + return new WebSocketTransport(params, connectionManager); + } + } + /************************** * WebSocketHandler methods **************************/ - class WsClient extends WebSocketClient { - private final WebSocketReceiver receiver; + class WsClient extends WebSocketClient { + private final WebSocketReceiver receiver; + /*************************** + * WsClient private members + ***************************/ + + private Timer timer = new Timer(); + private TimerTask activityTimerTask = null; + private long lastActivityTime; + private boolean shouldExplicitlyVerifyHostname = true; WsClient(URI serverUri, WebSocketReceiver receiver) { super(serverUri); @@ -219,10 +253,10 @@ public void onMessage(String string) { /* This allows us to detect a websocket ping, so we don't need Ably pings. */ @Override - public void onWebsocketPing( WebSocket conn, Framedata f ) { + public void onWebsocketPing(WebSocket conn, Framedata f) { Log.d(TAG, "onWebsocketPing()"); /* Call superclass to ensure the pong is sent. */ - super.onWebsocketPing( conn, f ); + super.onWebsocketPing(conn, f); flagActivity(); } @@ -231,7 +265,7 @@ public void onClose(final int wsCode, final String wsReason, final boolean remot Log.d(TAG, "onClose(): wsCode = " + wsCode + "; wsReason = " + wsReason + "; remote = " + remote); ErrorInfo reason; - switch(wsCode) { + switch (wsCode) { case NEVER_CONNECTED: case CLOSE_NORMAL: case BUGGYCLOSE: @@ -291,7 +325,8 @@ private synchronized void dispose() { try { timer.cancel(); timer = null; - } catch(IllegalStateException e) {} + } catch (IllegalStateException e) { + } } private synchronized void flagActivity() { @@ -324,15 +359,13 @@ private synchronized void checkActivity() { startActivityTimer(timeout + 100); } - - private synchronized void startActivityTimer(long timeout) - { + private synchronized void startActivityTimer(long timeout) { if (activityTimerTask == null) { schedule((activityTimerTask = new TimerTask() { public void run() { try { onActivityTimerExpiry(); - } catch(Throwable t) { + } catch (Throwable t) { Log.e(TAG, "Unexpected exception in activity timer handler", t); } } @@ -341,17 +374,16 @@ public void run() { } private synchronized void schedule(TimerTask task, long delay) { - if(timer != null) { + if (timer != null) { try { timer.schedule(task, delay); - } catch(IllegalStateException ise) { + } catch (IllegalStateException ise) { Log.e(TAG, "Unexpected exception scheduling activity timer", ise); } } } - private synchronized void onActivityTimerExpiry() - { + private synchronized void onActivityTimerExpiry() { activityTimerTask = null; long timeSinceLastActivity = System.currentTimeMillis() - lastActivityTime; long timeRemaining = getActivityTimeout() - timeSinceLastActivity; @@ -368,55 +400,9 @@ private synchronized void onActivityTimerExpiry() startActivityTimer(timeRemaining + 100); } - private long getActivityTimeout() - { + private long getActivityTimeout() { return connectionManager.maxIdleInterval + connectionManager.ably.options.realtimeRequestTimeout; } - - /*************************** - * WsClient private members - ***************************/ - - private Timer timer = new Timer(); - private TimerTask activityTimerTask = null; - private long lastActivityTime; - private boolean shouldExplicitlyVerifyHostname = true; } - public String toString() { - return WebSocketTransport.class.getName() + " {" + getURL() + "}"; - } - - public String getURL() { - return wsUri; - } - - /****************** - * private members - ******************/ - - private final TransportParams params; - private final ConnectionManager connectionManager; - private final boolean channelBinaryMode; - private String wsUri; - private ConnectListener connectListener; - - private WsClient wsConnection; - - private static final int NEVER_CONNECTED = -1; - private static final int BUGGYCLOSE = -2; - private static final int CLOSE_NORMAL = 1000; - private static final int GOING_AWAY = 1001; - private static final int CLOSE_PROTOCOL_ERROR = 1002; - private static final int REFUSE = 1003; -/* private static final int UNUSED = 1004; */ -/* private static final int NOCODE = 1005; */ - private static final int ABNORMAL_CLOSE = 1006; - private static final int NO_UTF8 = 1007; - private static final int POLICY_VALIDATION = 1008; - private static final int TOOBIG = 1009; - private static final int EXTENSION = 1010; - private static final int UNEXPECTED_CONDITION = 1011; - private static final int TLS_ERROR = 1015; - } From 7d70cd93d2206b838486c2f941ad6b8b9309f702 Mon Sep 17 00:00:00 2001 From: evgeny Date: Tue, 24 Sep 2024 15:10:03 +0100 Subject: [PATCH 2/2] chore: update gradle wrapper --- .editorconfig | 2 +- .github/workflows/check.yml | 7 +- .github/workflows/emulate.yml | 6 + .github/workflows/integration-test.yml | 12 ++ .github/workflows/javadoc.yml | 4 +- CONTRIBUTING.md | 13 +- android/build.gradle | 105 ---------------- android/build.gradle.kts | 66 ++++++++++ android/gradle.properties | 4 + android/maven.gradle | 149 ----------------------- android/src/main/AndroidManifest.xml | 5 +- build.gradle | 14 --- build.gradle.kts | 35 ++++++ common.gradle | 12 -- dependencies.gradle | 16 --- gradle.properties | 18 +++ gradle/libs.versions.toml | 49 ++++++++ gradle/wrapper/gradle-wrapper.jar | Bin 55190 -> 59203 bytes gradle/wrapper/gradle-wrapper.properties | 3 +- gradlew | 53 +++++--- gradlew.bat | 43 ++++--- java/build.gradle | 113 ----------------- java/build.gradle.kts | 89 ++++++++++++++ java/gradle.properties | 4 + java/maven.gradle | 123 ------------------- settings.gradle | 4 - settings.gradle.kts | 13 ++ 27 files changed, 370 insertions(+), 592 deletions(-) delete mode 100644 android/build.gradle create mode 100644 android/build.gradle.kts create mode 100644 android/gradle.properties delete mode 100644 android/maven.gradle delete mode 100644 build.gradle create mode 100644 build.gradle.kts delete mode 100644 common.gradle delete mode 100644 dependencies.gradle create mode 100644 gradle/libs.versions.toml delete mode 100644 java/build.gradle create mode 100644 java/build.gradle.kts create mode 100644 java/gradle.properties delete mode 100644 java/maven.gradle delete mode 100644 settings.gradle create mode 100644 settings.gradle.kts diff --git a/.editorconfig b/.editorconfig index 0fdd49060..d107cd585 100644 --- a/.editorconfig +++ b/.editorconfig @@ -9,7 +9,7 @@ charset = utf-8 indent_style = space indent_size = 2 -[*.{java,groovy,gradle}] +[*.{java,groovy,gradle,kts}] indent_size = 4 [*.md] diff --git a/.github/workflows/check.yml b/.github/workflows/check.yml index 4f4e9376d..98508cf54 100644 --- a/.github/workflows/check.yml +++ b/.github/workflows/check.yml @@ -12,4 +12,9 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - - run: ./gradlew checkstyleMain checkstyleTest checkWithCodenarc runUnitTests + - name: Set up the JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - run: ./gradlew checkWithCodenarc checkstyleMain checkstyleTest runUnitTests diff --git a/.github/workflows/emulate.yml b/.github/workflows/emulate.yml index 64eda9500..10a3ad2d5 100644 --- a/.github/workflows/emulate.yml +++ b/.github/workflows/emulate.yml @@ -18,6 +18,12 @@ jobs: - name: checkout uses: actions/checkout@v4 + - name: Set up the JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules diff --git a/.github/workflows/integration-test.yml b/.github/workflows/integration-test.yml index 08a3b459f..64db71862 100644 --- a/.github/workflows/integration-test.yml +++ b/.github/workflows/integration-test.yml @@ -15,6 +15,12 @@ jobs: with: submodules: 'recursive' + - name: Set up the JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - run: ./gradlew :java:testRestSuite - uses: actions/upload-artifact@v3 @@ -30,6 +36,12 @@ jobs: with: submodules: 'recursive' + - name: Set up the JDK + uses: actions/setup-java@v3 + with: + java-version: '17' + distribution: 'temurin' + - run: ./gradlew :java:testRealtimeSuite - uses: actions/upload-artifact@v3 diff --git a/.github/workflows/javadoc.yml b/.github/workflows/javadoc.yml index fff44a37b..6c5ccdcbc 100644 --- a/.github/workflows/javadoc.yml +++ b/.github/workflows/javadoc.yml @@ -25,8 +25,8 @@ jobs: - name: Set up the JDK uses: actions/setup-java@v3 with: - java-version: '11' - distribution: 'adopt' + java-version: '17' + distribution: 'temurin' - name: Build docs run: ./gradlew javadoc diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b98399897..7bf2ca18c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -201,8 +201,7 @@ implementation files('libs/ably-android-1.2.42.aar') This library uses [semantic versioning](http://semver.org/). For each release, the following needs to be done: 1. Create a branch for the release, named like `release/1.2.4` (where `1.2.4` is what you're releasing, being the new version) -2. Replace all references of the current version number with the new version number (check the [README.md](./README.md) and [common.gradle](./common.gradle)) and commit the changes - a. Increment the `versionCode` in the Android project's `build.gradle` by 1 +2. Replace all references of the current version number with the new version number (check the [README.md](./README.md) and [gradle.properties](./gradle.properties)) and commit the changes 3. Run [`github_changelog_generator`](https://github.com/github-changelog-generator/github-changelog-generator) to automate the update of the [CHANGELOG](./CHANGELOG.md). This may require some manual intervention, both in terms of how the command is run and how the change log file is modified. Your mileage may vary: - The command you will need to run will look something like this: `github_changelog_generator -u ably -p ably-java --since-tag v1.2.3 --output delta.md --token $GITHUB_TOKEN_WITH_REPO_ACCESS`. Generate token [here](https://github.com/settings/tokens/new?description=GitHub%20Changelog%20Generator%20token). - Using the command above, `--output delta.md` writes changes made after `--since-tag` to a new file. @@ -212,12 +211,10 @@ This library uses [semantic versioning](http://semver.org/). For each release, t 5. Make a PR against `main` 6. Once the PR is approved, merge it into `main` 7. From the updated `main` branch on your local workstation, assemble and upload: - 1. Run `./gradlew java:assembleRelease -PpublishTarget=MavenCentral` to build and upload `ably-java` to Nexus staging repository - 2. Run `./gradlew android:assembleRelease -PpublishTarget=MavenCentral` build and upload `ably-android` to Nexus staging repository - 3. Find the new staging repository using the [Nexus Repository Manager](https://oss.sonatype.org/#stagingRepositories) - 4. Check that it contains `ably-android` and `ably-java` releases - 5. "Close" it - this will take a few minutes during which time it will say (after a refresh of your browser) that "Activity: Operation in Progress" - 6. Once it has closed you will have "Release" available. You can allow it to "automatically drop" after successful release. A refresh or two later of the browser and the staging repository will have disappeared from the list (i.e. it's been dropped which implies it was released successfully) + 1. Run `./gradlew publishToMavenCentral` to build and upload `ably-java` and `ably-android` to Nexus staging repository + 2. Find the new staging repository using the [Nexus Repository Manager](https://oss.sonatype.org/#stagingRepositories) + 3. Check that it contains `ably-android` and `ably-java` releases + 4. "Release" it - this will take a few minutes during which time it will say (after a refresh of your browser) that "Activity: Operation in Progress". You can allow it to "automatically drop" after successful release. A refresh or two later of the browser and the staging repository will have disappeared from the list (i.e. it's been dropped which implies it was released successfully) 7. A [search for Ably packages](https://oss.sonatype.org/#nexus-search;quick~io.ably) should now list the new version for both `ably-android` and `ably-java` 8. Add a tag and push to origin - e.g.: `git tag v1.2.4 && git push origin v1.2.4` 9. Create the release on Github including populating the release notes diff --git a/android/build.gradle b/android/build.gradle deleted file mode 100644 index c422c3373..000000000 --- a/android/build.gradle +++ /dev/null @@ -1,105 +0,0 @@ -buildscript { - repositories { - mavenCentral() - mavenLocal() - google() - } - dependencies { - classpath 'com.android.tools.build:gradle:4.1.0' - } -} - -apply plugin: 'com.android.library' -apply from: '../common.gradle' - -ext { - artifactId = 'ably-android' -} - -allprojects { - repositories { - google() - } -} - -android { - compileSdkVersion 30 - - defaultConfig { - buildConfigField 'String', 'LIBRARY_NAME', '"android"' - buildConfigField 'String', 'VERSION', "\"$version\"" - minSdkVersion 19 - targetSdkVersion 30 - // This MUST be incremented by 1 on each ably-java release - versionCode 17 - versionName version - setProperty('archivesBaseName', "ably-android-$versionName") - testInstrumentationRunner 'android.support.test.runner.AndroidJUnitRunner' - testInstrumentationRunnerArgument 'class', 'io.ably.lib.test.android.AndroidPushTest' - //testInstrumentationRunnerArgument "class", "io.ably.lib.test.rest.RestSuite,io.ably.lib.test.realtime.RealtimeSuite,io.ably.lib.test.android.AndroidSuite,io.ably.lib.test.android.AndroidPushTest" - testInstrumentationRunnerArgument 'timeout_msec', '300000' -// testInstrumentationRunnerArgument "ABLY_ENV", "\"$System.env.ABLY_ENV\"" - consumerProguardFiles 'proguard.txt' - } - - compileOptions { - sourceCompatibility 1.8 - targetCompatibility 1.8 - } - - buildTypes { - release { - minifyEnabled false - } - } - - lintOptions { - abortOnError false - } - - sourceSets { - main { - java { - srcDirs = ['src/main/java', '../lib/src/main/java'] - } - } - androidTest { - java { - srcDirs = ['src/androidTest/java', '../lib/src/test/java'] - } - assets { - srcDirs = ['../lib/src/test/resources'] - } - } - } -} - -/* Fix for android test logging. Source: https://code.google.com/p/android/issues/detail?id=182307 */ -tasks.withType(com.android.build.gradle.internal.tasks.AndroidTestTask) { task -> - task.doFirst { - logging.level = LogLevel.INFO - } - task.doLast { - logging.level = LogLevel.LIFECYCLE - } -} - -apply from: '../dependencies.gradle' -dependencies { - implementation 'com.google.firebase:firebase-messaging:22.0.0' - androidTestImplementation 'com.android.support.test:runner:0.5' - androidTestImplementation 'com.android.support.test:rules:0.5' - androidTestImplementation 'com.crittercism.dexmaker:dexmaker:1.4' - androidTestImplementation 'com.crittercism.dexmaker:dexmaker-dx:1.4' - androidTestImplementation 'com.crittercism.dexmaker:dexmaker-mockito:1.4' - androidTestImplementation 'net.sourceforge.streamsupport:android-retrostreams:1.7.4' -} - -configurations { - all*.exclude group: 'org.hamcrest', module: 'hamcrest-core' - androidTestImplementation { - extendsFrom testImplementation - } -} - -apply from: 'maven.gradle' diff --git a/android/build.gradle.kts b/android/build.gradle.kts new file mode 100644 index 000000000..c66a5ee66 --- /dev/null +++ b/android/build.gradle.kts @@ -0,0 +1,66 @@ +plugins { + alias(libs.plugins.android.library) + alias(libs.plugins.maven.publish) +} + +android { + namespace = "io.ably.lib" + defaultConfig { + minSdk = 19 + compileSdk = 30 + buildConfigField("String", "LIBRARY_NAME", "\"android\"") + buildConfigField("String", "VERSION", "\"${property("VERSION_NAME")}\"") + testInstrumentationRunner = "android.support.test.runner.AndroidJUnitRunner" + testInstrumentationRunnerArguments["class"] = "io.ably.lib.test.android.AndroidPushTest" + testInstrumentationRunnerArguments["timeout_msec"] = "300000" + consumerProguardFiles("proguard.txt") + } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 + } + + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + + buildFeatures { + buildConfig = true + } + + lint { + abortOnError = false + } + + testOptions.targetSdk = 30 + + sourceSets { + getByName("main") { + java.srcDirs("src/main/java", "../lib/src/main/java") + } + getByName("androidTest") { + java.srcDirs("src/androidTest/java", "../lib/src/test/java") + assets.srcDirs("../lib/src/test/resources") + } + } +} + +dependencies { + api(libs.gson) + implementation(libs.bundles.common) + testImplementation(libs.bundles.tests) + implementation(libs.firebase.messaging) + androidTestImplementation(libs.bundles.instrumental.android) +} + +configurations { + all { + exclude(group = "org.hamcrest", module = "hamcrest-core") + } + getByName("androidTestImplementation") { + extendsFrom(configurations.getByName("testImplementation")) + } +} diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 000000000..c08c36bea --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=ably-android +POM_NAME=Ably Android client library SDK +POM_DESCRIPTION=An Android Realtime and REST client library SDK for the Ably platform. +POM_PACKAGING=aar diff --git a/android/maven.gradle b/android/maven.gradle deleted file mode 100644 index 88e1ec7ae..000000000 --- a/android/maven.gradle +++ /dev/null @@ -1,149 +0,0 @@ -apply plugin: 'maven' -apply plugin: 'signing' - -final String GROUP_ID = 'io.ably' -final String ARTIFACT_ID = 'ably-android' -final String LOCAL_RELEASE_DESTINATION = "${buildDir}/release/${version}" -final String MAVEN_USER = findProperty('ossrhUsername') -final String MAVEN_PASSWORD = findProperty('ossrhPassword') - -final boolean IS_PUBLISHING_TO_MAVEN_CENTRAL = findProperty('publishTarget') == 'MavenCentral' -if (IS_PUBLISHING_TO_MAVEN_CENTRAL && (MAVEN_USER == null || MAVEN_PASSWORD == null)) { - throw new GradleException('Either ossrhUsername or ossrhPassword not specified when publishTarget is MavenCentral.') -} - -/* - * Task which signs and uploads the Android artifacts to Nexus OSSRH. - */ -uploadArchives { - signing { - sign configurations.archives - } - repositories.mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - pom.groupId = GROUP_ID - pom.artifactId = ARTIFACT_ID - pom.version = version - - pom.project { - name 'Ably Android client library SDK' - description 'An Android Realtime and REST client library SDK for the Ably platform.' - packaging 'aar' - inceptionYear '2015' - url 'https://www.github.com/ably/ably-java' - developers { - developer { - id 'ably' // our company org in GitHub: https://github.com/ably - name 'Ably' // UK based company: Ably Real-time Ltd - email 'support@ably.com' - url 'https://ably.com/' - } - } - scm { - url 'https://github.com/ably/ably-java' - connection 'scm:git:git://github.com/ably/ably-java.git' - developerConnection 'scm:git:ssh://github.com/ably/ably-java.git' - tag = 'v' + version - } - organization { - name 'Ably' // UK based company: Ably Real-time Ltd - url 'https://ably.com/' - } - issueManagement { - system 'Github' - url 'https://github.com/ably/ably-java/issues' - } - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'https://raw.github.com/ably/ably-java/main/LICENSE' - distribution 'repo' - } - } - } - - pom.whenConfigured { p -> - p.dependencies = p.dependencies.findAll { - // Exclude dependency on lib subproject. - dep -> dep.artifactId != 'lib' - }.findAll { - // Exclude Google services since we don't want to impose a particular - // version on users. Ideally we would specify a version range, - // but the Google services Gradle plugin doesn't seem to - // support that. - // TODO: Make sure this works when installing from Maven! - dep -> dep.artifactId != 'play-services-gcm' && dep.artifactId != 'firebase-messaging' - } - } - - if (IS_PUBLISHING_TO_MAVEN_CENTRAL) { - repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') { - authentication(userName: MAVEN_USER, password: MAVEN_PASSWORD) - } - - snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { - authentication(userName: MAVEN_USER, password: MAVEN_PASSWORD) - } - } else { - // Export to local Maven cache - repository(url: repositories.mavenLocal().url) - - // Export files to local storage - repository(url: "file://${LOCAL_RELEASE_DESTINATION}") - } - } -} - -task zipRelease(type: Zip) { - from LOCAL_RELEASE_DESTINATION - destinationDir buildDir - archiveName "release-${version}.zip" -} - -tasks.whenTaskAdded { task -> - if (task.name == 'assembleRelease') { - task.doLast { - if (IS_PUBLISHING_TO_MAVEN_CENTRAL) { - logger.quiet('✅ Release uploaded to Sonatype Staging Repository') - } else { - logger.quiet("✅ Release ${version} can be found at ${LOCAL_RELEASE_DESTINATION}/") - logger.quiet("✅ Release ${version} zipped can be found ${buildDir}/release-${version}.zip") - } - } - - task.dependsOn(uploadArchives) - task.dependsOn(zipRelease) - } -} - -task sourcesJar(type: Jar) { - classifier = 'sources' - from android.sourceSets.main.java.srcDirs -} - -task javadoc(type: Javadoc) { - source = android.sourceSets.main.java.srcDirs - classpath += project.files(android.bootClasspath.join(File.pathSeparator)) - failOnError false - title = 'Ably documentation' - options.overview = '../overview.html' -} - -afterEvaluate { - javadoc.classpath += files(android.libraryVariants.collect { variant -> - variant.javaCompile.classpath.files - }) -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir - javadoc.title = 'Ably documentation' - javadoc.options.overview = '../overview.html' -} - -artifacts { - archives sourcesJar - archives javadocJar -} diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml index c904374e2..6fc6facb7 100644 --- a/android/src/main/AndroidManifest.xml +++ b/android/src/main/AndroidManifest.xml @@ -1,7 +1,4 @@ - - + - diff --git a/build.gradle b/build.gradle deleted file mode 100644 index 8cf9d7745..000000000 --- a/build.gradle +++ /dev/null @@ -1,14 +0,0 @@ -// Top-level build file where you can add configuration options common to all sub-projects/modules. - -plugins { - id 'io.codearte.nexus-staging' version '0.21.1' -} - -repositories { - google() - mavenCentral() -} - -nexusStaging { - packageGroup = 'io.ably' -} diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 000000000..d031f19d9 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,35 @@ +import com.vanniktech.maven.publish.MavenPublishBaseExtension +import com.vanniktech.maven.publish.SonatypeHost + +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +plugins { + alias(libs.plugins.android.library) apply false + alias(libs.plugins.maven.publish) apply false +} + +subprojects { + repositories { + google() + mavenCentral() + } + + tasks.withType { + // To prevent javadoc warnings with Java 8 + options { + this as StandardJavadocDocletOptions + addBooleanOption("Xdoclint:none", true) + addBooleanOption("quiet", true) + addStringOption("Xmaxwarns", "1") + } + } +} + +configure(subprojects) { + pluginManager.withPlugin("com.vanniktech.maven.publish") { + extensions.configure { + publishToMavenCentral(SonatypeHost.DEFAULT) + signAllPublications() + } + } +} diff --git a/common.gradle b/common.gradle deleted file mode 100644 index 7d98567bd..000000000 --- a/common.gradle +++ /dev/null @@ -1,12 +0,0 @@ -repositories { - mavenCentral() -} - -group = 'io.ably' -version = '1.2.42' -description = 'Ably java client library' - -tasks.withType(Javadoc) { - // To prevent javadoc warnings with Java 8 - options.addStringOption('Xdoclint:none', '-quiet') -} diff --git a/dependencies.gradle b/dependencies.gradle deleted file mode 100644 index 6d1b9ea12..000000000 --- a/dependencies.gradle +++ /dev/null @@ -1,16 +0,0 @@ -// These dependencies have to be in lib/build.gradle for compilation _and_ -// in java/build.gradle and android/build.gradle for maven. -dependencies { - implementation 'org.msgpack:msgpack-core:0.8.11' - implementation 'org.java-websocket:Java-WebSocket:1.5.3' - implementation 'com.google.code.gson:gson:2.9.0' - implementation 'com.davidehrmann.vcdiff:vcdiff-core:0.1.1' - testImplementation 'org.hamcrest:hamcrest-all:1.3' - testImplementation 'junit:junit:4.12' - testImplementation 'org.nanohttpd:nanohttpd:2.3.0' - testImplementation 'org.nanohttpd:nanohttpd-nanolets:2.3.0' - testImplementation 'org.nanohttpd:nanohttpd-websocket:2.3.0' - testImplementation 'org.mockito:mockito-core:1.10.19' - testImplementation 'net.jodah:concurrentunit:0.4.2' - testImplementation 'org.slf4j:slf4j-simple:1.7.30' -} diff --git a/gradle.properties b/gradle.properties index d9cf55df7..b24da7ddf 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,2 +1,20 @@ +GROUP=io.ably +VERSION_NAME=1.2.42 + +POM_INCEPTION_YEAR=2015 +POM_URL=https://github.com/ably/ably-java +POM_SCM_URL=https://github.com/ably/ably-java/ +POM_SCM_CONNECTION=scm:git:git://github.com/ably/ably-java.git +POM_SCM_DEV_CONNECTION=scm:git:git@github.com:ably/ably-java.git + +POM_LICENSE_NAME=The Apache Software License, Version 2.0 +POM_LICENSE_URL=https://raw.github.com/ably/ably-java/main/LICENSE +POM_LICENSE_DIST=repo + +POM_DEVELOPER_ID=ably +POM_DEVELOPER_NAME=Ably +POM_DEVELOPER_URL=https://github.com/ably/ +SONATYPE_STAGING_PROFILE=io.ably + org.gradle.jvmargs=-Xmx1536M android.useAndroidX=true diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 000000000..241d7195c --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,49 @@ +[versions] +agp = "8.5.2" +junit = "4.12" +gson = "2.9.0" +msgpack = "0.8.11" +java-websocket = "1.5.3" +vcdiff = "0.1.1" +hamcrest = "1.3" +nanohttpd = "2.3.0" +mockito = "1.10.19" +concurrentunit = "0.4.2" +slf4j = "1.7.30" +build-config = "5.4.0" +firebase-messaging = "22.0.0" +android-test = "0.5" +dexmaker = "1.4" +android-retrostreams = "1.7.4" +maven-publish = "0.29.0" + +[libraries] +gson = { group = "com.google.code.gson", name = "gson", version.ref = "gson" } +msgpack = { group = "org.msgpack", name = "msgpack-core", version.ref = "msgpack" } +java-websocket = { group = "org.java-websocket", name = "Java-WebSocket", version.ref = "java-websocket" } +vcdiff-core = { group = "com.davidehrmann.vcdiff", name = "vcdiff-core", version.ref = "vcdiff" } +junit = { group = "junit", name = "junit", version.ref = "junit" } +hamcrest-all = { group = "org.hamcrest", name = "hamcrest-all", version.ref = "hamcrest" } +nanohttpd = { group = "org.nanohttpd", name = "nanohttpd", version.ref = "nanohttpd" } +nanohttpd-nanolets = { group = "org.nanohttpd", name = "nanohttpd-nanolets", version.ref = "nanohttpd" } +nanohttpd-websocket = { group = "org.nanohttpd", name = "nanohttpd-websocket", version.ref = "nanohttpd" } +mockito-core = { group = "org.mockito", name = "mockito-core", version.ref = "mockito" } +concurrentunit = { group = "net.jodah", name = "concurrentunit", version.ref = "concurrentunit" } +slf4j-simple = { group = "org.slf4j", name = "slf4j-simple", version.ref = "slf4j" } +firebase-messaging = { group = "com.google.firebase", name = "firebase-messaging", version.ref = "firebase-messaging" } +android-test-runner = { group = "com.android.support.test", name = "runner", version.ref = "android-test" } +android-test-rules = { group = "com.android.support.test", name = "rules", version.ref = "android-test" } +dexmaker = { group = "com.crittercism.dexmaker", name = "dexmaker", version.ref = "dexmaker" } +dexmaker-dx = { group = "com.crittercism.dexmaker", name = "dexmaker-dx", version.ref = "dexmaker" } +dexmaker-mockito = { group = "com.crittercism.dexmaker", name = "dexmaker-mockito", version.ref = "dexmaker" } +android-retrostreams = { group = "net.sourceforge.streamsupport", name = "android-retrostreams", version.ref = "android-retrostreams" } + +[bundles] +common = ["msgpack", "java-websocket", "vcdiff-core"] +tests = ["junit","hamcrest-all", "nanohttpd", "nanohttpd-nanolets", "nanohttpd-websocket", "mockito-core", "concurrentunit", "slf4j-simple"] +instrumental-android = ["android-test-runner", "android-test-rules", "dexmaker", "dexmaker-dx", "dexmaker-mockito", "android-retrostreams"] + +[plugins] +android-library = { id = "com.android.library", version.ref = "agp" } +build-config = { id = "com.github.gmazzo.buildconfig", version.ref = "build-config" } +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "maven-publish" } diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index 87b738cbd051603d91cc39de6cb000dd98fe6b02..e708b1c023ec8b20f512888fe07c5bd3ff77bb8f 100644 GIT binary patch delta 26193 zcmZ6yQ+S|Vu&o>0wrv|7+qP{xU&pp>+fK)}ZKK1EI!T}Z?6uCtKF>3+>b7Q$8a3;k z=?&n+bKs5iponRd%jI35ARxHlARx>siR4%*il7((1uK)8y@{J!oa(gW@(&EbVLvhOxk#E0z~5*_n$Tfk4BT1W zh~4H^yI$w!jrIW$@8~{|r_Pqh9?;*1{Rs-h$o?FVSot<3yKX_cH33Wqgy&Ugow#-- zd$AFKpvAm7vspRndDP5Y*{X$rg0EvCe9(Ow>lBeyGY!V@xQpXomHjCWwMA-rDRPSv zY@gq7;|J=t%N|R7799odG(V`K@OXpjH2q11r=-spt+w zmng+yzxXW&2lk&G)N z-h)16N@V9eFCNeZrbSiuE44@1#kS@h5wqX)wgpxfDiThLBx=5wyP_q&uT8OO)g!i} z`ony65!)LBh`yjIhNFW{%5vZka3CNsFd!fxA|N)P3^l}%ARrX~g&6-g&ji4>8oCzF zKSH<7MutdMx~SkLQ5g_)<~Gen%{ZC`NJdbH)-9$<(ppE)OUsf4+q=3xf!CmpZ`c>g z4Ys!B49{{P<@lMuM@Gi9cVK3-W&h8s0rx+luP@f0C2um4An0s{!;rApVwtHdlxBE$ zQ}-fiEaWDdk_Z{*`eS})9~03LOH#dsT^&c)G^Img%Th&3)4?O4Wq0Y&WfVC|VC zaAQwCL*mCoU593J>8NRWao@R|t#I*_etEAkj;Pnx3Px?Zvdq$zVwkChuPxdoUTJ_f z91pe6B-4rYiTj&D4-yF14*5BofspUDM3g|`#ZB$#P?p~-U9+h+Tp`mRxh*=&=&T*z2q>oXsGC%dWpshF`TNHT@_F>BTVa9G zFHYaGE;{c#SSHF=YdV#s&=@2HPVty?i1KRKnHMZOfiniPe%}^B8HFW z*bJMvi!j9I(^KSPI$-KTx8V>{A^SDH?XXHk2r zl3KMvKg}3t6q5GEn_a9c#&WLtG*Z+HlGMW<(x+DV$pO_9MbEmMd(|gjX$n1RcRRtv zDT~=Ns?nTUr{?a8bL%J`yhpBI_HJ=#J82CYL&{%*ODAh3aQ7C!&9@kuw2vTv^C5Jv zw{IBtth8_1lePLj%=eb2<|{r*Tr*eZc-3zAmND82-Y2(LCRJAYureb+>#gbkwS;}t z*dnlaO#wXPiqx378QPITyhU~VJF;i9bY&O>9`a<{Z=?tck-b5V=Ao(5iSinj^|JG0 z^Mei%8;vfT5yjl03`{X#`7o70WDu59Odv`qB&Kd4C_bQnCkPF-Z;JgZ^g{2~-?;k+ z9+d0E@fT(gQ5tY0OC?v8n)9lJ>xx!1rO|MACzpKq&HkF1K{ zFMRybn|7SN@1S0)$Tgnp%zhGmU0>jDj(qPx9cd6`;(p;9lq7up`hoX{FkphHyA0DN z5*0_8Cr`bKX4N+FM+NK)D;S+PQe|0pN`4GS%9VQJ!F@1AmvLj;s* z?5g2wVEo&)=YpSxQkAAjZU5QM2_ajp*;-oX5M*sllctPP$Cq)!W#4miWC{L-|8byZ z^iiy&Xyktx3$vQ_qG0ub{r0Drov-9Lg!p(omOcL5a7e1+=Q3+nuHS2}-`t&-(97AN zF!4V4J;ELv>Nxx#>p@srsIrMn3PoV;Fg0q~y9pFFHz~V?YR;q*bQp&>(1D%FPs zC*IM^c|*^YFd>8K!*CNjO?IyyVo0Oj58J=V+HZAg4EGQ_44PdzlAt7maXilOhlg5a01 zY;8K$cR z`?@05v}ykdXcpLFzwCu_^1Ix5oA^_#4P!q~iRGZaX}VfO>ocM-TIR_OVK|WIk=>*a zeNESLpfO2pCzrnXhvy4zVJJ|)bs@SBYiq&LgqV+kKtj0biu*n~3>Ls(AQ(f9*~_s) zl-KPHr3GJJdZbJ9z~4gno4QuCR6$+sXgnW-SR$UwdY>J(?y@VzOw-!z29BZMAa@*>cOMvu0j;NRm&a zA?dwChkQuKL|!${FHs;*>8zcvo{*&C&2uo)%+*>dx4mwK>fGN+2G`s+E+?FoDMB>o zeEk&R)YUb-)hB0btx0ZnL88NiU<4a4`*dXC&pJay_*!BvzV5L3egZfJ^3pQHC8zFj z6=tLQ9h+!XzlAle1MVUJR85LGy?b&&${lv)c!u?eR_HV5VGs~FMZC*XTEE>nWMNC4 zB-UE7S$o)*022^2SpSi8XR351W?e8S@6i^nRhZK21FxbQo=r|Xn3;a2gqkA9jC)ti z1U4zFR*N)5@{VZmvX8drmAd;H`V9VYPp)`CJ<24J3+oWFOOh|D+JrBTtRrT-UNiS+ z(J%rlp)E0cuK~b2h31^v%*?vFF%$`yaX*844h@8tiqzpL`V|45>?UdyCevybaaT+l zdZ{j2LANKlT=)%J%zpa;hj+KHasV#AP+j%_aV7mNFYy(I_e39m$ld%;hB{CR3NVHM zs?_7ryClg9%EdjJGu)cCVf%af}^xD1`=ey;xTSwvKswhL}f-h0rN zx$a-|8ICEr+~Tj|Q4jJ&D%Al!W{4)^8UYCecR(m4n1Xox7yTqZ!IweY}ynJI3 zMz}L%pqst|R9anw=H*@%l{5QYAlsH-?qK4IOT|U_%?X}+&3)yyoEsv22Yx~r!!S#D zFHjffGZQ^)k^dxFhZ09Hl^#$)1$%|>4LpawxEh+VJLRz_6n%#;LPdOiZ?-?QopPOR@$pf0U=M zotnQ+LX;pV(2mBcz5(Tq6mtI6^{nnb0ZCeqdc2jimiO)`7S5L5d*s~AL(wwRx^y_) zh#GRJ6CxyPN*6Zaw(!3)H6nkpJ5EKdx44c~YYcFRRlLrSK}q>QJLrKU+@6Erj&$a; zqseNY2DlS-f#nv2LUG7?M@oSa$z=|r!z!Vove26#Jt4%MY5?-5EAFbS6yguDs?gCE z(vc>HKlF#duyAd`Ef7JyP%dBGd}$B5LM>{YZJ8-rOT-4~#18)T(~5~bG7eBFL@D*sXp~E2b)#Kj-6#_;rKFTcZ5;~q>XBa%9qtn zh|*NK&npj)_X0e@`Q)Duq;*G(ix)9}{FJ(tBr%|Zl{%`3vm<3;ZTao!@JCy!w$Hd$ zeTsHc-QO?{AjJ$$xyVeeiaa(mIS{g>H~RWDd^7NrcuhG+n6;;IOM;Pnv9AXK%*F>l zh;aH=_~cR-sWJe&{k3#sLEJ9Q;xpFrzQ!1IA($)K6A(HHJ78{YNzs2fS9t(^@rq2; z%#zYDCmxz&BIxr`?~%}btlSLY*nUOqSIu}@IIZ6m+uae;rw{n@(ccR5i+I!DYr1e1 za(MOzHzGaa-+2Qi4lE{yhB?MQdUJSqM$cf$?F?6pb~QsYC}kb`SX9W4V`o%%*{zQJ z64`;gk{^o`6(Of^2y<*)?pbt(p*cCs&QLrs354H|(M+Bl84yFLM>{4$a^t<&uOd2( zDhK}WqM`tq3_L~#0nsJ_0U`b0qJjbbXWp&Th4scC_XtdYXp(dG5kaH82(=)@Kwe1p zNKUs;DyER`6;Dj1)k)SGNDhTGJscIq$m5B>ort=n@wBIQ$t`!x`S0)~<-(*&Y|AE0 z)a`OzqP|LRKT9XHDk!b@B{%*6j6c1Qx`5psOT1Mo>jGwg; zn#*@S7dQbm1brRiPk)Qw!Na~6#2i1!S>M_ZKFrd-N5lZxeU;03j1M7M?Pf4N8dDV?~ChR zl~dm^ZdlcjsW>`r+GpIb96^EutTZQ7HZJ;Ji9;!9fm+SDzseGRa8}V!>`vLfEA5< z^)ZQgMs(z0cl=&{{#-n$H4i7s&F>pR0-jaEn=80*K7h4_Idl}?F4O-j&+iq}!q|0u zW>KXn0n?zIbBoRPGAVPB&EzsF)TGUQQbxPlc&8)*U!LeW9DyE}^H`oU%IDjfiRxVP zcZm6`!=m@f-tdayub+@lecT1jHj$I7CX&YH9$FlZ&!uBZ_-j7{`7B}n<(LR^mFlUm zdPXw&F#ypd4Z0KNnbu6!n+YwuuibHJChS6JgbF%iJ2%OW%yroEX{36{1($2+bB-&K zC0J1^1xfhaH|c}lg(Z}>?KcTy2vs7B`wT%jT2_Co)Dt-(~ z$*7abZzyD0vyDjXlMsm7MuDfEld>A6c;^VEn_#?$SSettv}`*0^ICen24YL)KM7sp>Adr36Z)i zLZ)C~O4l-Gf7j-`wbz!yKgf7>5oT>A5&2stuDEVk(Mb*S$E|S5S*`6RXM&8_DcsVv zCK2jHYi|StkgaGR5AN&9NqFZaeIZ_hgv4iEeNG!)WAE2;tZty`kUh#rx0T{i<$L(pl}iOmk&Jn&`s#jtF66@s#y-I zrs`Ym7<%r)TW;h`wil?x%EZp6y^@4kv#)JSw#ajW_F1RVu}r@b$DP>2UqH&8hkfa) zgPn~tP`HoQzMD8L-$()v073u!X)Q$4`&f7(NRb`EPA>$p&-63+XPOTAUl+O`CVMBv zUrQ*z96r#y(>OjwsSPUDv!=|yZT$0zP51zJCqSw3{3pOd>*?-nTsY6nx@rVd9r#ph z^8SY>H;-e8C;6+TlQz|S9$*y4dPkmczn4KLYdPZkMB5YQKCyx1k zJU4XSsE4Vem=3%L+>&Z1KHvXd;|(^h;3Q!f2m$|_<7NAaA#5;^0T^^w!jQ4QL;le} z*zsm?=EF;Xc)4tMRH8y4kh-rGShhn)F}9Mo8($sHHs?z!aFY1UepV8{ZGt?)QusJ7 zzf~$ngGIL`3tXTAjsF%C+r97MR_g|>aA)^c*OI<*yVs|9XU79XL4qJI26Uk7Ijv3x zoINaggyd9a%t_q%0Ph8Ql54a^ty-n_TBVQcb?WVteFYzbIN`|xuv|=$?1TOt2d7n0 zl5X7BBE4qHiyuUa#im|F_P^ddNQq;Qd~V)7b~RAGgIK3?C-mhqFa+JRkpBJJtsb?Y z62_+%!ETHm0%1mb%*fmFd z-(YSD22!E5B|nk@2o@GOO91cOjR9JiitxXy-!;sEW|(do8;%0P&eP`;44pK7O*MhX zdlfBHfc!vIi~q{+{~Yhf~URiRcY@Pkc?o3SCkQQg}E+y{>J{{cN^5EY^KdbZj0eL(u#c~39>G3r3`=a|y(g9VG ze}!4C?0rHYoy*G-LX{VUGPp;XC>s6yOH$*fi2M)v$zDQ~Z-5?Et!K&4XKhzSA@f~# z2D4gmA=DNBUoxh8%ui`)gXJ{mzO;`Ka7Yro`3THr#>S{ih`Fm&Af_p<$IpA2`> zcde1;fXK1=Y%7#s1`@e0{)Ze>eOIs%ZAx25fAtKS2&3m==&$*)WJQuNTkc$bqb}pE z-|}i=Zhj)B^u#$9ue!%(%F|I3Z>Ea=V?a$?fMBd} ze1q9wcHuXQpXD!fBk-UaRsr3cun@5r2vH(J2asdupCZQXtO!W>%S8LAv!xe0kfu2n zjW5_uuq+)vtGwrR=+l{QqF2kBE78!W(J}GR<%hyOSa^#KW2A9#7?v-EpG}|G-ghyn z=?rspMd0U@OLxiB;Y?ZmX`qtu$BtOjBlV)!Sme@`-#+XmY|ZzS&1tu!IJe(QY_SR1 z0UV=l1SuTQ1Woeb)q8dM)^=&@D0MIY=odw=R}Ix1`s7uYSGi_ZR9?Ypz~{)8DQKuF z;;$|^>WXy8;kVBCj)zk}goNWAo+T`+Eu^`)Rq5;Orj}=OtP(k(_`S?Ia*=lu&i(!Y z6ky!UGfXJmharkF3Z=N zw|spi95hC)Z#|XXFr~?^gLGr^%K#mG*CRRdI!@8olzM+?;<` zmMBkcY<*15Y~<5?on)3RjW#`011xN|mi+U4bC%t&;oTcGP~j?Uw(l_ls= z>}ftPt(>WqU$ooRt&WD#h(kUpTahm$0)a5J z-Z=WZ(i=h)mcpDWFgE-@IdH`}B3S$|R`_jjI00_L3YJ|7a(6Hf0Ikg*&c>H}i8a<* z^1^4vKTOgld+Y*{4;&xunXo&fUk)n(nZ7>(A~0v{k}!HW8=|4a&j(!xARx)

$|c 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∨fd|u2@Kuk2 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 F_djzcFxCJ7 diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 33682bbbf..0ebc4df25 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,5 +1,6 @@ +#Tue Sep 24 14:50:52 BST 2024 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-6.6.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-all.zip zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew index af6708ff2..4f906e0c8 100755 --- a/gradlew +++ b/gradlew @@ -1,5 +1,21 @@ #!/usr/bin/env sh +# +# Copyright 2015 the original author or authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + ############################################################################## ## ## Gradle start up script for UN*X @@ -28,7 +44,7 @@ APP_NAME="Gradle" APP_BASE_NAME=`basename "$0"` # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m"' +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD="maximum" @@ -66,6 +82,7 @@ esac CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + # Determine the Java command to use to start the JVM. if [ -n "$JAVA_HOME" ] ; then if [ -x "$JAVA_HOME/jre/sh/java" ] ; then @@ -109,10 +126,11 @@ if $darwin; then GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" fi -# For Cygwin, switch paths to Windows format before running java -if $cygwin ; then +# For Cygwin or MSYS, switch paths to Windows format before running java +if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then APP_HOME=`cygpath --path --mixed "$APP_HOME"` CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` # We build the pattern for arguments to be converted via cygpath @@ -138,19 +156,19 @@ if $cygwin ; then else eval `echo args$i`="\"$arg\"" fi - i=$((i+1)) + i=`expr $i + 1` done case $i in - (0) set -- ;; - (1) set -- "$args0" ;; - (2) set -- "$args0" "$args1" ;; - (3) set -- "$args0" "$args1" "$args2" ;; - (4) set -- "$args0" "$args1" "$args2" "$args3" ;; - (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; - (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; - (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; - (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; - (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + 0) set -- ;; + 1) set -- "$args0" ;; + 2) set -- "$args0" "$args1" ;; + 3) set -- "$args0" "$args1" "$args2" ;; + 4) set -- "$args0" "$args1" "$args2" "$args3" ;; + 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; esac fi @@ -159,14 +177,9 @@ save () { for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done echo " " } -APP_ARGS=$(save "$@") +APP_ARGS=`save "$@"` # Collect all arguments for the java command, following the shell quoting and substitution rules eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" -# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong -if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then - cd "$(dirname "$0")" -fi - exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat index 0f8d5937c..ac1b06f93 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -1,3 +1,19 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + @if "%DEBUG%" == "" @echo off @rem ########################################################################## @rem @@ -13,15 +29,18 @@ if "%DIRNAME%" == "" set DIRNAME=. set APP_BASE_NAME=%~n0 set APP_HOME=%DIRNAME% +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" @rem Find java.exe if defined JAVA_HOME goto findJavaFromJavaHome set JAVA_EXE=java.exe %JAVA_EXE% -version >NUL 2>&1 -if "%ERRORLEVEL%" == "0" goto init +if "%ERRORLEVEL%" == "0" goto execute echo. echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. @@ -35,7 +54,7 @@ goto fail set JAVA_HOME=%JAVA_HOME:"=% set JAVA_EXE=%JAVA_HOME%/bin/java.exe -if exist "%JAVA_EXE%" goto init +if exist "%JAVA_EXE%" goto execute echo. echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% @@ -45,28 +64,14 @@ echo location of your Java installation. goto fail -:init -@rem Get command-line arguments, handling Windows variants - -if not "%OS%" == "Windows_NT" goto win9xME_args - -:win9xME_args -@rem Slurp the command line arguments. -set CMD_LINE_ARGS= -set _SKIP=2 - -:win9xME_args_slurp -if "x%~1" == "x" goto execute - -set CMD_LINE_ARGS=%* - :execute @rem Setup the command line set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + @rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* :end @rem End local scope for the variables with windows NT shell diff --git a/java/build.gradle b/java/build.gradle deleted file mode 100644 index a4637f926..000000000 --- a/java/build.gradle +++ /dev/null @@ -1,113 +0,0 @@ -plugins { - id 'de.fuerstenau.buildconfig' version '1.1.8' - id 'checkstyle' -} - -apply plugin: 'java' -apply plugin: 'idea' -apply from: '../common.gradle' -apply from: 'maven.gradle' - -sourceCompatibility = 1.8 -targetCompatibility = 1.8 - -apply from: '../dependencies.gradle' - -buildConfig { - packageName 'io.ably.lib' - clsName 'BuildConfig' - buildConfigField 'String', 'LIBRARY_NAME', 'java' -} - -sourceSets { - main { - java { - srcDirs = ['src/main/java', '../lib/src/main/java'] - } - } - test { - java { - srcDirs = ['src/test/java', '../lib/src/test/java'] - } - } -} - -// Default jar: add io.ably classes from :lib dependency. -jar { - baseName = 'ably-java' - from { - configurations.compile.collect { file -> - file.directory ? file : zipTree(file) - } - } - includes = ['**/io/ably/**'] - includeEmptyDirs false - exclude 'META-INF/**' -} - -// fullJar: add all classes from dependencies transitively. -task fullJar(type: Jar) { - baseName = 'ably-java' - classifier = 'full' - from { - configurations.compile.collect { file -> - file.directory ? file : zipTree(file) - } - } - with jar - exclude 'META-INF/**' -} - -assemble.dependsOn fullJar -assembleRelease.dependsOn checkstyleMain - -configurations { - fullConfiguration - testsConfiguration -} - -artifacts { - fullConfiguration fullJar -} - -task testRealtimeSuite(type: Test) { - filter { - includeTestsMatching '*RealtimeSuite' - } - beforeTest { descriptor -> - logger.lifecycle("-> $descriptor") - } - outputs.upToDateWhen { false } - testLogging.exceptionFormat = 'full' -} - -task testRestSuite(type: Test) { - filter { - includeTestsMatching '*RestSuite' - } - beforeTest { descriptor -> - logger.lifecycle("-> $descriptor") - } - outputs.upToDateWhen { false } - testLogging.exceptionFormat = 'full' -} - -/* -Test task to run pure unit tests, where pure means that they only run -locally and do not need to communicate with Ably servers. -This is achieved by excluding everything in the io.ably.lib.test package, -as it only contains the REST and Realtime suites. -*/ -task runUnitTests(type: Test) { - filter { - excludeTestsMatching 'io.ably.lib.test.*' - } - beforeTest { descriptor -> - // informational, so we're not flying blind at runtime - logger.lifecycle("-> $descriptor") - } - - // force tests to run every time this task is invoked - outputs.upToDateWhen { false } -} - diff --git a/java/build.gradle.kts b/java/build.gradle.kts new file mode 100644 index 000000000..21c3f87af --- /dev/null +++ b/java/build.gradle.kts @@ -0,0 +1,89 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + +plugins { + alias(libs.plugins.build.config) + alias(libs.plugins.maven.publish) + checkstyle + `java-library` +} + +java { + sourceCompatibility = JavaVersion.VERSION_1_8 + targetCompatibility = JavaVersion.VERSION_1_8 +} + +tasks.withType { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE +} + +dependencies { + api(libs.gson) + implementation(libs.bundles.common) + testImplementation(libs.bundles.tests) +} + +buildConfig { + useJavaOutput() + packageName = "io.ably.lib" + buildConfigField("String", "LIBRARY_NAME", "\"java\"") + buildConfigField("String", "VERSION", "\"${property("VERSION_NAME")}\"") +} + +sourceSets { + named("main") { + java { + srcDirs("src/main/java", "../lib/src/main/java") + } + } + named("test") { + java { + srcDirs("src/test/java", "../lib/src/test/java") + } + } +} + +tasks.checkstyleMain.configure { + exclude("io/ably/lib/BuildConfig.java") +} + +tasks.register("testRealtimeSuite") { + filter { + includeTestsMatching("*RealtimeSuite") + } + jvmArgs("--add-opens", "java.base/java.time=ALL-UNNAMED") + jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") + beforeTest(closureOf { logger.lifecycle("-> $this") }) + outputs.upToDateWhen { false } + testLogging { + exceptionFormat = TestExceptionFormat.FULL + } +} + +tasks.register("testRestSuite") { + filter { + includeTestsMatching("*RestSuite") + } + jvmArgs("--add-opens", "java.base/java.time=ALL-UNNAMED") + jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") + beforeTest(closureOf { logger.lifecycle("-> $this") }) + outputs.upToDateWhen { false } + testLogging { + exceptionFormat = TestExceptionFormat.FULL + } +} + +/* +Test task to run pure unit tests, where pure means that they only run +locally and do not need to communicate with Ably servers. +This is achieved by excluding everything in the io.ably.lib.test package, +as it only contains the REST and Realtime suites. +*/ +tasks.register("runUnitTests") { + filter { + excludeTestsMatching("io.ably.lib.test.*") + } + jvmArgs("--add-opens", "java.base/java.time=ALL-UNNAMED") + jvmArgs("--add-opens", "java.base/java.lang=ALL-UNNAMED") + beforeTest(closureOf { logger.lifecycle("-> $this") }) + outputs.upToDateWhen { false } +} diff --git a/java/gradle.properties b/java/gradle.properties new file mode 100644 index 000000000..bff480295 --- /dev/null +++ b/java/gradle.properties @@ -0,0 +1,4 @@ +POM_ARTIFACT_ID=ably-java +POM_NAME=Ably Java client library SDK +POM_DESCRIPTION=A Java Realtime and REST client library SDK for the Ably platform. +POM_PACKAGING=jar diff --git a/java/maven.gradle b/java/maven.gradle deleted file mode 100644 index 0679702d7..000000000 --- a/java/maven.gradle +++ /dev/null @@ -1,123 +0,0 @@ -apply plugin: 'java' -apply plugin: 'maven' -apply plugin: 'signing' - -final String GROUP_ID = 'io.ably' -final String ARTIFACT_ID = 'ably-java' -final String LOCAL_RELEASE_DESTINATION = "${buildDir}/release/${version}" -final String MAVEN_USER = findProperty('ossrhUsername') -final String MAVEN_PASSWORD = findProperty('ossrhPassword') - -final boolean IS_PUBLISHING_TO_MAVEN_CENTRAL = findProperty('publishTarget') == 'MavenCentral' -if (IS_PUBLISHING_TO_MAVEN_CENTRAL && (MAVEN_USER == null || MAVEN_PASSWORD == null)) { - throw new GradleException('Either ossrhUsername or ossrhPassword not specified when publishTarget is MavenCentral.') -} - -/* - * Task which signs and uploads the Java artifacts to Nexus OSSRH. - */ -uploadArchives { - signing { - sign configurations.archives - } - repositories.mavenDeployer { - beforeDeployment { MavenDeployment deployment -> signing.signPom(deployment) } - - pom.groupId = GROUP_ID - pom.artifactId = ARTIFACT_ID - pom.version = version - - pom.project { - name 'Ably Java client library SDK' - description 'A Java Realtime and REST client library SDK for the Ably platform.' - packaging 'jar' - inceptionYear '2015' - url 'https://www.github.com/ably/ably-java' - developers { - developer { - id 'ably' // our company org in GitHub: https://github.com/ably - name 'Ably' // UK based company: Ably Real-time Ltd - email 'support@ably.com' - url 'https://ably.com/' - } - } - scm { - url 'https://github.com/ably/ably-java' - connection 'scm:git:git://github.com/ably/ably-java.git' - developerConnection 'scm:git:ssh://github.com/ably/ably-java.git' - tag = 'v' + version - } - organization { - name 'Ably' // UK based company: Ably Real-time Ltd - url 'https://ably.com/' - } - issueManagement { - system 'Github' - url 'https://github.com/ably/ably-java/issues' - } - licenses { - license { - name 'The Apache Software License, Version 2.0' - url 'https://raw.github.com/ably/ably-java/main/LICENSE' - distribution 'repo' - } - } - } - - // Exclude test dependencies - pom.whenConfigured { p -> - p.dependencies = p.dependencies.findAll { - dep -> dep.scope == 'runtime' - } - } - - if (IS_PUBLISHING_TO_MAVEN_CENTRAL) { - repository(url: 'https://oss.sonatype.org/service/local/staging/deploy/maven2/') { - authentication(userName: MAVEN_USER, password: MAVEN_PASSWORD) - } - - snapshotRepository(url: 'https://oss.sonatype.org/content/repositories/snapshots/') { - authentication(userName: MAVEN_USER, password: MAVEN_PASSWORD) - } - } else { - // Export files to local storage - repository(url: "file://${LOCAL_RELEASE_DESTINATION}") - } - } -} - -task zipRelease(type: Zip) { - from LOCAL_RELEASE_DESTINATION - destinationDir buildDir - archiveName "release-${version}.zip" -} - -task assembleRelease { - doLast { - if (IS_PUBLISHING_TO_MAVEN_CENTRAL) { - logger.quiet('✅ Release uploaded to Sonatype Staging Repository') - } else { - logger.quiet("✅ Release ${version} can be found at ${LOCAL_RELEASE_DESTINATION}") - logger.quiet("✅ Release ${version} zipped can be found ${buildDir}/release-${version}.zip") - } - } - dependsOn(uploadArchives) - dependsOn(zipRelease) -} - -task sourcesJar(type: Jar) { - classifier = 'sources' - from sourceSets.main.allSource -} - -task javadocJar(type: Jar, dependsOn: javadoc) { - classifier = 'javadoc' - from javadoc.destinationDir - javadoc.title = 'Ably documentation' - javadoc.options.overview = '../overview.html' -} - -artifacts { - archives sourcesJar - archives javadocJar -} diff --git a/settings.gradle b/settings.gradle deleted file mode 100644 index 9ee5ac941..000000000 --- a/settings.gradle +++ /dev/null @@ -1,4 +0,0 @@ -rootProject.name = 'ably-java' -include 'java', - 'android', - 'gradle-lint' diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 000000000..220fd80b7 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,13 @@ +pluginManagement { + repositories { + google() + gradlePluginPortal() + mavenCentral() + } +} + +rootProject.name = "ably-java" + +include("java") +include("android") +include("gradle-lint")