Base Configuration

This commit is contained in:
2018-03-25 13:13:01 +02:00
commit bcac6a3b85
334 changed files with 56965 additions and 0 deletions

View File

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Copyright (c) 2015-present, Parse, LLC.
~ All rights reserved.
~
~ This source code is licensed under the BSD-style license found in the
~ LICENSE file in the root directory of this source tree. An additional grant
~ of patent rights can be found in the PATENTS file in the same directory.
-->
<manifest package="com.parse.livequery">
<application />
</manifest>

View File

@ -0,0 +1,10 @@
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
/* package */ abstract class ClientOperation {
abstract JSONObject getJSONObjectRepresentation() throws JSONException;
}

View File

@ -0,0 +1,24 @@
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
/* package */ class ConnectClientOperation extends ClientOperation {
private final String applicationId;
private final String sessionToken;
/* package */ ConnectClientOperation(String applicationId, String sessionToken) {
this.applicationId = applicationId;
this.sessionToken = sessionToken;
}
@Override
/* package */ JSONObject getJSONObjectRepresentation() throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "connect");
jsonObject.put("applicationId", applicationId);
jsonObject.put("sessionToken", sessionToken);
return jsonObject;
}
}

View File

@ -0,0 +1,101 @@
package com.parse;
import java.util.Locale;
public abstract class LiveQueryException extends Exception {
private LiveQueryException() {
super();
}
private LiveQueryException(String detailMessage) {
super(detailMessage);
}
private LiveQueryException(String detailMessage, Throwable cause) {
super(detailMessage, cause);
}
private LiveQueryException(Throwable cause) {
super(cause);
}
/**
* An error that is reported when any other unknown {@link RuntimeException} occurs unexpectedly.
*/
public static class UnknownException extends LiveQueryException {
/* package */ UnknownException(String detailMessage, RuntimeException cause) {
super(detailMessage, cause);
}
}
/**
* An error that is reported when the server returns a response that cannot be parsed.
*/
public static class InvalidResponseException extends LiveQueryException {
/* package */ InvalidResponseException(String response) {
super(response);
}
}
/**
* An error that is reported when the server does not accept a query we've sent to it.
*/
public static class InvalidQueryException extends LiveQueryException {
}
/**
* An error that is reported when the server returns valid JSON, but it doesn't match the format we expect.
*/
public static class InvalidJSONException extends LiveQueryException {
// JSON used for matching.
private final String json;
/// Key that was expected to match.
private final String expectedKey;
/* package */ InvalidJSONException(String json, String expectedKey) {
super(String.format(Locale.US, "Invalid JSON; expectedKey: %s, json: %s", expectedKey, json));
this.json = json;
this.expectedKey = expectedKey;
}
public String getJson() {
return json;
}
public String getExpectedKey() {
return expectedKey;
}
}
/**
* An error that is reported when the live query server encounters an internal error.
*/
public static class ServerReportedException extends LiveQueryException {
private final int code;
private final String error;
private final boolean reconnect;
public ServerReportedException(int code, String error, boolean reconnect) {
super(String.format(Locale.US, "Server reported error; code: %d, error: %s, reconnect: %b", code, error, reconnect));
this.code = code;
this.error = error;
this.reconnect = reconnect;
}
public int getCode() {
return code;
}
public String getError() {
return error;
}
public boolean isReconnect() {
return reconnect;
}
}
}

View File

@ -0,0 +1,119 @@
package com.parse;
import android.util.Log;
import java.net.URI;
import java.util.Locale;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.WebSocket;
import okhttp3.WebSocketListener;
import okio.ByteString;
/* package */ public class OkHttp3SocketClientFactory implements WebSocketClientFactory {
OkHttpClient mClient;
public OkHttp3SocketClientFactory(OkHttpClient client) {
mClient = client;
}
public OkHttp3SocketClientFactory() {
mClient = new OkHttpClient();
}
@Override
public WebSocketClient createInstance(WebSocketClient.WebSocketClientCallback webSocketClientCallback, URI hostUrl) {
return new OkHttp3WebSocketClient(mClient, webSocketClientCallback, hostUrl);
}
static class OkHttp3WebSocketClient implements WebSocketClient {
private static final String LOG_TAG = "OkHttpWebSocketClient";
private final WebSocketClientCallback webSocketClientCallback;
private WebSocket webSocket;
private State state = State.NONE;
private final OkHttpClient client;
private final String url;
private final int STATUS_CODE = 1000;
private final String CLOSING_MSG = "User invoked close";
private final WebSocketListener handler = new WebSocketListener() {
@Override
public void onOpen(WebSocket webSocket, Response response) {
setState(State.CONNECTED);
webSocketClientCallback.onOpen();
}
@Override
public void onMessage(WebSocket webSocket, String text) {
webSocketClientCallback.onMessage(text);
}
@Override
public void onMessage(WebSocket webSocket, ByteString bytes) {
Log.w(LOG_TAG, String.format(Locale.US,
"Socket got into inconsistent state and received %s instead.",
bytes.toString()));
}
@Override
public void onClosed(WebSocket webSocket, int code, String reason) {
setState(State.DISCONNECTED);
webSocketClientCallback.onClose();
}
@Override
public void onFailure(okhttp3.WebSocket webSocket, Throwable t, Response response) {
webSocketClientCallback.onError(t);
}
};
private OkHttp3WebSocketClient(OkHttpClient okHttpClient,
WebSocketClientCallback webSocketClientCallback, URI hostUrl) {
client = okHttpClient;
this.webSocketClientCallback = webSocketClientCallback;
url = hostUrl.toString();
}
@Override
public synchronized void open() {
if (State.NONE == state) {
// OkHttp3 connects as soon as the socket is created so do it here.
Request request = new Request.Builder()
.url(url)
.build();
webSocket = client.newWebSocket(request, handler);
setState(State.CONNECTING);
}
}
@Override
public synchronized void close() {
setState(State.DISCONNECTING);
webSocket.close(STATUS_CODE, CLOSING_MSG);
}
@Override
public void send(String message) {
if (state == State.CONNECTED) {
webSocket.send(message);
}
}
@Override
public State getState() {
return state;
}
private synchronized void setState(State newState) {
this.state = newState;
this.webSocketClientCallback.stateChanged();
}
}
}

View File

@ -0,0 +1,47 @@
package com.parse;
import java.net.URI;
import java.util.concurrent.Executor;
public interface ParseLiveQueryClient {
<T extends ParseObject> SubscriptionHandling<T> subscribe(ParseQuery<T> query);
<T extends ParseObject> void unsubscribe(final ParseQuery<T> query);
<T extends ParseObject> void unsubscribe(final ParseQuery<T> query, final SubscriptionHandling<T> subscriptionHandling);
void connectIfNeeded();
void reconnect();
void disconnect();
void registerListener(ParseLiveQueryClientCallbacks listener);
void unregisterListener(ParseLiveQueryClientCallbacks listener);
class Factory {
public static ParseLiveQueryClient getClient() {
return new ParseLiveQueryClientImpl();
}
public static ParseLiveQueryClient getClient(WebSocketClientFactory webSocketClientFactory) {
return new ParseLiveQueryClientImpl(webSocketClientFactory);
}
public static ParseLiveQueryClient getClient(URI uri) {
return new ParseLiveQueryClientImpl(uri);
}
public static ParseLiveQueryClient getClient(URI uri, WebSocketClientFactory webSocketClientFactory) {
return new ParseLiveQueryClientImpl(uri, webSocketClientFactory);
}
/* package */
static ParseLiveQueryClient getClient(URI uri, WebSocketClientFactory webSocketClientFactory, Executor taskExecutor) {
return new ParseLiveQueryClientImpl(uri, webSocketClientFactory, taskExecutor);
}
}
}

View File

@ -0,0 +1,11 @@
package com.parse;
public interface ParseLiveQueryClientCallbacks {
void onLiveQueryClientConnected(ParseLiveQueryClient client);
void onLiveQueryClientDisconnected(ParseLiveQueryClient client, boolean userInitiated);
void onLiveQueryError(ParseLiveQueryClient client, LiveQueryException reason);
void onSocketError(ParseLiveQueryClient client, Throwable reason);
}

View File

@ -0,0 +1,435 @@
package com.parse;
import android.util.Log;
import android.util.SparseArray;
import org.json.JSONException;
import org.json.JSONObject;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URL;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.concurrent.Callable;
import java.util.concurrent.Executor;
import bolts.Continuation;
import bolts.Task;
import okhttp3.OkHttpClient;
import static com.parse.Parse.checkInit;
/* package */ class ParseLiveQueryClientImpl implements ParseLiveQueryClient {
private static final String LOG_TAG = "ParseLiveQueryClient";
private final Executor taskExecutor;
private final String applicationId;
private final String clientKey;
private final SparseArray<Subscription<? extends ParseObject>> subscriptions = new SparseArray<>();
private final URI uri;
private final WebSocketClientFactory webSocketClientFactory;
private final WebSocketClient.WebSocketClientCallback webSocketClientCallback;
private final List<ParseLiveQueryClientCallbacks> mCallbacks = new ArrayList<>();
private WebSocketClient webSocketClient;
private int requestIdCount = 1;
private boolean userInitiatedDisconnect = false;
private boolean hasReceivedConnected = false;
/* package */ ParseLiveQueryClientImpl() {
this(getDefaultUri());
}
/* package */ ParseLiveQueryClientImpl(URI uri) {
this(uri, new OkHttp3SocketClientFactory(new OkHttpClient()), Task.BACKGROUND_EXECUTOR);
}
/* package */ ParseLiveQueryClientImpl(URI uri, WebSocketClientFactory webSocketClientFactory) {
this(uri, webSocketClientFactory, Task.BACKGROUND_EXECUTOR);
}
/* package */ ParseLiveQueryClientImpl(WebSocketClientFactory webSocketClientFactory) {
this(getDefaultUri(), webSocketClientFactory, Task.BACKGROUND_EXECUTOR);
}
/* package */ ParseLiveQueryClientImpl(URI uri, WebSocketClientFactory webSocketClientFactory, Executor taskExecutor) {
checkInit();
this.uri = uri;
this.applicationId = ParsePlugins.get().applicationId();
this.clientKey = ParsePlugins.get().clientKey();
this.webSocketClientFactory = webSocketClientFactory;
this.taskExecutor = taskExecutor;
this.webSocketClientCallback = getWebSocketClientCallback();
}
private static URI getDefaultUri() {
URL serverUrl = ParseRESTCommand.server;
if (serverUrl == null) return null;
String url = serverUrl.toString();
if (serverUrl.getProtocol().equals("https")) {
url = url.replaceFirst("https", "wss");
} else {
url = url.replaceFirst("http", "ws");
}
try {
return new URI(url);
} catch (URISyntaxException e) {
e.printStackTrace();
throw new RuntimeException(e.getMessage());
}
}
@Override
public <T extends ParseObject> SubscriptionHandling<T> subscribe(ParseQuery<T> query) {
int requestId = requestIdGenerator();
Subscription<T> subscription = new Subscription<>(requestId, query);
subscriptions.append(requestId, subscription);
if (isConnected()) {
sendSubscription(subscription);
} else if (userInitiatedDisconnect) {
Log.w(LOG_TAG, "Warning: The client was explicitly disconnected! You must explicitly call .reconnect() in order to process your subscriptions.");
} else {
connectIfNeeded();
}
return subscription;
}
public void connectIfNeeded() {
switch (getWebSocketState()) {
case CONNECTED:
// nothing to do
break;
case CONNECTING:
// just wait for it to finish connecting
break;
case NONE:
case DISCONNECTING:
case DISCONNECTED:
reconnect();
break;
default:
break;
}
}
@Override
public <T extends ParseObject> void unsubscribe(final ParseQuery<T> query) {
if (query != null) {
for (int i = 0; i < subscriptions.size(); i++) {
Subscription subscription = subscriptions.valueAt(i);
if (query.equals(subscription.getQuery())) {
sendUnsubscription(subscription);
}
}
}
}
@Override
public <T extends ParseObject> void unsubscribe(final ParseQuery<T> query, final SubscriptionHandling<T> subscriptionHandling) {
if (query != null && subscriptionHandling != null) {
for (int i = 0; i < subscriptions.size(); i++) {
Subscription subscription = subscriptions.valueAt(i);
if (query.equals(subscription.getQuery()) && subscriptionHandling.equals(subscription)) {
sendUnsubscription(subscription);
}
}
}
}
@Override
public void reconnect() {
if (webSocketClient != null) {
webSocketClient.close();
}
userInitiatedDisconnect = false;
hasReceivedConnected = false;
webSocketClient = webSocketClientFactory.createInstance(webSocketClientCallback, uri);
webSocketClient.open();
}
@Override
public void disconnect() {
if (webSocketClient != null) {
webSocketClient.close();
webSocketClient = null;
}
userInitiatedDisconnect = true;
hasReceivedConnected = false;
}
@Override
public void registerListener(ParseLiveQueryClientCallbacks listener) {
mCallbacks.add(listener);
}
@Override
public void unregisterListener(ParseLiveQueryClientCallbacks listener) {
mCallbacks.remove(listener);
}
// Private methods
private synchronized int requestIdGenerator() {
return requestIdCount++;
}
private WebSocketClient.State getWebSocketState() {
WebSocketClient.State state = webSocketClient == null ? null : webSocketClient.getState();
return state == null ? WebSocketClient.State.NONE : state;
}
private boolean isConnected() {
return hasReceivedConnected && inAnyState(WebSocketClient.State.CONNECTED);
}
private boolean inAnyState(WebSocketClient.State... states) {
return Arrays.asList(states).contains(getWebSocketState());
}
private Task<Void> handleOperationAsync(final String message) {
return Task.call(new Callable<Void>() {
public Void call() throws Exception {
parseMessage(message);
return null;
}
}, taskExecutor);
}
private Task<Void> sendOperationAsync(final ClientOperation clientOperation) {
return Task.call(new Callable<Void>() {
public Void call() throws Exception {
JSONObject jsonEncoded = clientOperation.getJSONObjectRepresentation();
String jsonString = jsonEncoded.toString();
if (Parse.getLogLevel() <= Parse.LOG_LEVEL_DEBUG) {
Log.d(LOG_TAG, "Sending over websocket: " + jsonString);
}
webSocketClient.send(jsonString);
return null;
}
}, taskExecutor);
}
private void parseMessage(String message) throws LiveQueryException {
try {
JSONObject jsonObject = new JSONObject(message);
String rawOperation = jsonObject.getString("op");
switch (rawOperation) {
case "connected":
hasReceivedConnected = true;
dispatchConnected();
Log.v(LOG_TAG, "Connected, sending pending subscription");
for (int i = 0; i < subscriptions.size(); i++) {
sendSubscription(subscriptions.valueAt(i));
}
break;
case "redirect":
String url = jsonObject.getString("url");
// TODO: Handle redirect.
Log.d(LOG_TAG, "Redirect is not yet handled");
break;
case "subscribed":
handleSubscribedEvent(jsonObject);
break;
case "unsubscribed":
handleUnsubscribedEvent(jsonObject);
break;
case "enter":
handleObjectEvent(Subscription.Event.ENTER, jsonObject);
break;
case "leave":
handleObjectEvent(Subscription.Event.LEAVE, jsonObject);
break;
case "update":
handleObjectEvent(Subscription.Event.UPDATE, jsonObject);
break;
case "create":
handleObjectEvent(Subscription.Event.CREATE, jsonObject);
break;
case "delete":
handleObjectEvent(Subscription.Event.DELETE, jsonObject);
break;
case "error":
handleErrorEvent(jsonObject);
break;
default:
throw new LiveQueryException.InvalidResponseException(message);
}
} catch (JSONException e) {
throw new LiveQueryException.InvalidResponseException(message);
}
}
private void dispatchConnected() {
for (ParseLiveQueryClientCallbacks callback : mCallbacks) {
callback.onLiveQueryClientConnected(this);
}
}
private void dispatchDisconnected() {
for (ParseLiveQueryClientCallbacks callback : mCallbacks) {
callback.onLiveQueryClientDisconnected(this, userInitiatedDisconnect);
}
}
private void dispatchServerError(LiveQueryException exc) {
for (ParseLiveQueryClientCallbacks callback : mCallbacks) {
callback.onLiveQueryError(this, exc);
}
}
private void dispatchSocketError(Throwable reason) {
userInitiatedDisconnect = false;
for (ParseLiveQueryClientCallbacks callback : mCallbacks) {
callback.onSocketError(this, reason);
}
dispatchDisconnected();
}
private <T extends ParseObject> void handleSubscribedEvent(JSONObject jsonObject) throws JSONException {
final int requestId = jsonObject.getInt("requestId");
final Subscription<T> subscription = subscriptionForRequestId(requestId);
if (subscription != null) {
subscription.didSubscribe(subscription.getQuery());
}
}
private <T extends ParseObject> void handleUnsubscribedEvent(JSONObject jsonObject) throws JSONException {
final int requestId = jsonObject.getInt("requestId");
final Subscription<T> subscription = subscriptionForRequestId(requestId);
if (subscription != null) {
subscription.didUnsubscribe(subscription.getQuery());
subscriptions.remove(requestId);
}
}
private <T extends ParseObject> void handleObjectEvent(Subscription.Event event, JSONObject jsonObject) throws JSONException {
final int requestId = jsonObject.getInt("requestId");
final Subscription<T> subscription = subscriptionForRequestId(requestId);
if (subscription != null) {
T object = ParseObject.fromJSON(jsonObject.getJSONObject("object"), subscription.getQueryState().className(), ParseDecoder.get(), subscription.getQueryState().selectedKeys());
subscription.didReceive(event, subscription.getQuery(), object);
}
}
private <T extends ParseObject> void handleErrorEvent(JSONObject jsonObject) throws JSONException {
int requestId = jsonObject.getInt("requestId");
int code = jsonObject.getInt("code");
String error = jsonObject.getString("error");
Boolean reconnect = jsonObject.getBoolean("reconnect");
final Subscription<T> subscription = subscriptionForRequestId(requestId);
LiveQueryException exc = new LiveQueryException.ServerReportedException(code, error, reconnect);
if (subscription != null) {
subscription.didEncounter(exc, subscription.getQuery());
}
dispatchServerError(exc);
}
private <T extends ParseObject> Subscription<T> subscriptionForRequestId(int requestId) {
//noinspection unchecked
return (Subscription<T>) subscriptions.get(requestId);
}
private <T extends ParseObject> void sendSubscription(final Subscription<T> subscription) {
ParseUser.getCurrentSessionTokenAsync().onSuccess(new Continuation<String, Void>() {
@Override
public Void then(Task<String> task) throws Exception {
String sessionToken = task.getResult();
SubscribeClientOperation<T> op = new SubscribeClientOperation<>(subscription.getRequestId(), subscription.getQueryState(), sessionToken);
// dispatch errors
sendOperationAsync(op).continueWith(new Continuation<Void, Void>() {
public Void then(Task<Void> task) {
Exception error = task.getError();
if (error != null) {
if (error instanceof RuntimeException) {
subscription.didEncounter(new LiveQueryException.UnknownException(
"Error when subscribing", (RuntimeException) error), subscription.getQuery());
}
}
return null;
}
});
return null;
}
});
}
private void sendUnsubscription(Subscription subscription) {
sendOperationAsync(new UnsubscribeClientOperation(subscription.getRequestId()));
}
private WebSocketClient.WebSocketClientCallback getWebSocketClientCallback() {
return new WebSocketClient.WebSocketClientCallback() {
@Override
public void onOpen() {
hasReceivedConnected = false;
Log.v(LOG_TAG, "Socket opened");
ParseUser.getCurrentSessionTokenAsync().onSuccessTask(new Continuation<String, Task<Void>>() {
@Override
public Task<Void> then(Task<String> task) throws Exception {
String sessionToken = task.getResult();
return sendOperationAsync(new ConnectClientOperation(applicationId, sessionToken));
}
}).continueWith(new Continuation<Void, Void>() {
public Void then(Task<Void> task) {
Exception error = task.getError();
if (error != null) {
Log.e(LOG_TAG, "Error when connection client", error);
}
return null;
}
});
}
@Override
public void onMessage(String message) {
Log.v(LOG_TAG, "Socket onMessage " + message);
handleOperationAsync(message).continueWith(new Continuation<Void, Void>() {
public Void then(Task<Void> task) {
Exception error = task.getError();
if (error != null) {
Log.e(LOG_TAG, "Error handling message", error);
}
return null;
}
});
}
@Override
public void onClose() {
Log.v(LOG_TAG, "Socket onClose");
hasReceivedConnected = false;
dispatchDisconnected();
}
@Override
public void onError(Throwable exception) {
Log.e(LOG_TAG, "Socket onError", exception);
hasReceivedConnected = false;
dispatchSocketError(exception);
}
@Override
public void stateChanged() {
Log.v(LOG_TAG, "Socket stateChanged");
}
};
}
}

View File

@ -0,0 +1,38 @@
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
/* package */ class SubscribeClientOperation<T extends ParseObject> extends ClientOperation {
private final int requestId;
private final ParseQuery.State<T> state;
private final String sessionToken;
/* package */ SubscribeClientOperation(int requestId, ParseQuery.State<T> state, String sessionToken) {
this.requestId = requestId;
this.state = state;
this.sessionToken = sessionToken;
}
@Override
/* package */ JSONObject getJSONObjectRepresentation() throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "subscribe");
jsonObject.put("requestId", requestId);
jsonObject.put("sessionToken", sessionToken);
JSONObject queryJsonObject = new JSONObject();
queryJsonObject.put("className", state.className());
// TODO: add support for fields
// https://github.com/ParsePlatform/parse-server/issues/3671
PointerEncoder pointerEncoder = PointerEncoder.get();
queryJsonObject.put("where", pointerEncoder.encode(state.constraints()));
jsonObject.put("query", queryJsonObject);
return jsonObject;
}
}

View File

@ -0,0 +1,120 @@
package com.parse;
import java.util.ArrayList;
import java.util.List;
/* package */ class Subscription<T extends ParseObject> implements SubscriptionHandling<T> {
private final List<HandleEventsCallback<T>> handleEventsCallbacks = new ArrayList<>();
private final List<HandleErrorCallback<T>> handleErrorCallbacks = new ArrayList<>();
private final List<HandleSubscribeCallback<T>> handleSubscribeCallbacks = new ArrayList<>();
private final List<HandleUnsubscribeCallback<T>> handleUnsubscribeCallbacks = new ArrayList<>();
private final int requestId;
private final ParseQuery<T> query;
private final ParseQuery.State<T> state;
/* package */ Subscription(int requestId, ParseQuery<T> query) {
this.requestId = requestId;
this.query = query;
this.state = query.getBuilder().build();
}
@Override
public Subscription<T> handleEvents(HandleEventsCallback<T> callback) {
handleEventsCallbacks.add(callback);
return this;
}
@Override
public Subscription<T> handleEvent(final Event event, final HandleEventCallback<T> callback) {
return handleEvents(new HandleEventsCallback<T>() {
@Override
public void onEvents(ParseQuery<T> query, Event callbackEvent, T object) {
if (callbackEvent == event) {
callback.onEvent(query, object);
}
}
});
}
@Override
public Subscription<T> handleError(HandleErrorCallback<T> callback) {
handleErrorCallbacks.add(callback);
return this;
}
@Override
public Subscription<T> handleSubscribe(HandleSubscribeCallback<T> callback) {
handleSubscribeCallbacks.add(callback);
return this;
}
@Override
public Subscription<T> handleUnsubscribe(HandleUnsubscribeCallback<T> callback) {
handleUnsubscribeCallbacks.add(callback);
return this;
}
@Override
public int getRequestId() {
return requestId;
}
/* package */ ParseQuery<T> getQuery() {
return query;
}
/* package */ ParseQuery.State<T> getQueryState() {
return state;
}
/**
* Tells the handler that an event has been received from the live query server.
*
* @param event The event that has been received from the server.
* @param query The query that the event occurred on.
*/
/* package */ void didReceive(Event event, ParseQuery<T> query, T object) {
for (HandleEventsCallback<T> handleEventsCallback : handleEventsCallbacks) {
handleEventsCallback.onEvents(query, event, object);
}
}
/**
* Tells the handler that an error has been received from the live query server.
*
* @param error The error that the server has encountered.
* @param query The query that the error occurred on.
*/
/* package */ void didEncounter(LiveQueryException error, ParseQuery<T> query) {
for (HandleErrorCallback<T> handleErrorCallback : handleErrorCallbacks) {
handleErrorCallback.onError(query, error);
}
}
/**
* Tells the handler that a query has been successfully registered with the server.
* - note: This may be invoked multiple times if the client disconnects/reconnects.
*
* @param query The query that has been subscribed.
*/
/* package */ void didSubscribe(ParseQuery<T> query) {
for (HandleSubscribeCallback<T> handleSubscribeCallback : handleSubscribeCallbacks) {
handleSubscribeCallback.onSubscribe(query);
}
}
/**
* Tells the handler that a query has been successfully deregistered from the server.
* - note: This is not called unless `unregister()` is explicitly called.
*
* @param query The query that has been unsubscribed.
*/
/* package */ void didUnsubscribe(ParseQuery<T> query) {
for (HandleUnsubscribeCallback<T> handleUnsubscribeCallback : handleUnsubscribeCallbacks) {
handleUnsubscribeCallback.onUnsubscribe(query);
}
}
}

View File

@ -0,0 +1,72 @@
package com.parse;
public interface SubscriptionHandling<T extends ParseObject> {
/**
* Register a callback for when an event occurs.
*
* @param callback The callback to register.
* @return The same SubscriptionHandling, for easy chaining.
*/
SubscriptionHandling<T> handleEvents(Subscription.HandleEventsCallback<T> callback);
/**
* Register a callback for when an event occurs.
*
* @param event The event type to handle. You should pass one of the enum cases in Event
* @param callback The callback to register.
* @return The same SubscriptionHandling, for easy chaining.
*/
SubscriptionHandling<T> handleEvent(Subscription.Event event, Subscription.HandleEventCallback<T> callback);
/**
* Register a callback for when an event occurs.
*
* @param callback The callback to register.
* @return The same SubscriptionHandling, for easy chaining.
*/
SubscriptionHandling<T> handleError(Subscription.HandleErrorCallback<T> callback);
/**
* Register a callback for when a client succesfully subscribes to a query.
*
* @param callback The callback to register.
* @return The same SubscriptionHandling, for easy chaining.
*/
SubscriptionHandling<T> handleSubscribe(Subscription.HandleSubscribeCallback<T> callback);
/**
* Register a callback for when a query has been unsubscribed.
*
* @param callback The callback to register.
* @return The same SubscriptionHandling, for easy chaining.
*/
SubscriptionHandling<T> handleUnsubscribe(Subscription.HandleUnsubscribeCallback<T> callback);
int getRequestId();
interface HandleEventsCallback<T extends ParseObject> {
void onEvents(ParseQuery<T> query, Subscription.Event event, T object);
}
interface HandleEventCallback<T extends ParseObject> {
void onEvent(ParseQuery<T> query, T object);
}
interface HandleErrorCallback<T extends ParseObject> {
void onError(ParseQuery<T> query, LiveQueryException exception);
}
interface HandleSubscribeCallback<T extends ParseObject> {
void onSubscribe(ParseQuery<T> query);
}
interface HandleUnsubscribeCallback<T extends ParseObject> {
void onUnsubscribe(ParseQuery<T> query);
}
enum Event {
CREATE, ENTER, UPDATE, LEAVE, DELETE
}
}

View File

@ -0,0 +1,21 @@
package com.parse;
import org.json.JSONException;
import org.json.JSONObject;
/* package */ class UnsubscribeClientOperation extends ClientOperation {
private final int requestId;
/* package */ UnsubscribeClientOperation(int requestId) {
this.requestId = requestId;
}
@Override
/* package */ JSONObject getJSONObjectRepresentation() throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "unsubscribe");
jsonObject.put("requestId", requestId);
return jsonObject;
}
}

View File

@ -0,0 +1,27 @@
package com.parse;
/* package */ interface WebSocketClient {
void open();
void close();
void send(String message);
State getState();
interface WebSocketClientCallback {
void onOpen();
void onMessage(String message);
void onClose();
void onError(Throwable exception);
void stateChanged();
}
enum State {NONE, CONNECTING, CONNECTED, DISCONNECTING, DISCONNECTED}
}

View File

@ -0,0 +1,9 @@
package com.parse;
import java.net.URI;
/* package */ interface WebSocketClientFactory {
WebSocketClient createInstance(WebSocketClient.WebSocketClientCallback webSocketClientCallback, URI hostUrl);
}

View File

@ -0,0 +1,10 @@
package com.parse;
import java.util.concurrent.Executor;
class ImmediateExecutor implements Executor {
@Override
public void execute(Runnable runnable) {
runnable.run();
}
}

View File

@ -0,0 +1,609 @@
package com.parse;
import org.assertj.core.api.Assertions;
import org.json.JSONException;
import org.json.JSONObject;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
import org.mockito.invocation.InvocationOnMock;
import org.mockito.stubbing.Answer;
import org.robolectric.RobolectricTestRunner;
import org.robolectric.annotation.Config;
import org.robolectric.util.Transcript;
import java.io.IOException;
import java.net.URI;
import bolts.Task;
import static junit.framework.Assert.assertEquals;
import static junit.framework.Assert.assertNotNull;
import static junit.framework.Assert.assertTrue;
import static org.mockito.AdditionalMatchers.and;
import static org.mockito.AdditionalMatchers.not;
import static org.mockito.Matchers.any;
import static org.mockito.Matchers.anyBoolean;
import static org.mockito.Matchers.anyString;
import static org.mockito.Matchers.contains;
import static org.mockito.Matchers.eq;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import com.parse.livequery.BuildConfig;
@RunWith(RobolectricTestRunner.class)
@Config(constants = BuildConfig.class, sdk = 21)
public class TestParseLiveQueryClient {
private WebSocketClient webSocketClient;
private WebSocketClient.WebSocketClientCallback webSocketClientCallback;
private ParseLiveQueryClient parseLiveQueryClient;
private ParseUser mockUser;
@Before
public void setUp() throws Exception {
ParsePlugins.initialize("1234", "1234");
// Register a mock currentUserController to make getCurrentUser work
mockUser = mock(ParseUser.class);
ParseCurrentUserController currentUserController = mock(ParseCurrentUserController.class);
when(currentUserController.getAsync(anyBoolean())).thenAnswer(new Answer<Task<ParseUser>>() {
@Override
public Task<ParseUser> answer(InvocationOnMock invocation) throws Throwable {
return Task.forResult(mockUser);
}
});
when(currentUserController.getCurrentSessionTokenAsync()).thenAnswer(new Answer<Task<String>>() {
@Override
public Task<String> answer(InvocationOnMock invocation) throws Throwable {
return Task.forResult(mockUser.getSessionToken());
}
});
ParseCorePlugins.getInstance().registerCurrentUserController(currentUserController);
parseLiveQueryClient = ParseLiveQueryClient.Factory.getClient(new URI(""), new WebSocketClientFactory() {
@Override
public WebSocketClient createInstance(WebSocketClient.WebSocketClientCallback webSocketClientCallback, URI hostUrl) {
TestParseLiveQueryClient.this.webSocketClientCallback = webSocketClientCallback;
webSocketClient = mock(WebSocketClient.class);
return webSocketClient;
}
}, new ImmediateExecutor());
reconnect();
}
@After
public void tearDown() throws Exception {
ParseCorePlugins.getInstance().reset();
ParsePlugins.reset();
}
@Test
public void testSubscribeAfterSocketConnectBeforeConnectedOp() throws Exception {
// Bug: https://github.com/parse-community/ParseLiveQuery-Android/issues/46
ParseQuery<ParseObject> queryA = ParseQuery.getQuery("objA");
ParseQuery<ParseObject> queryB = ParseQuery.getQuery("objB");
clearConnection();
// This will trigger connectIfNeeded(), which calls reconnect()
SubscriptionHandling<ParseObject> subA = parseLiveQueryClient.subscribe(queryA);
verify(webSocketClient, times(1)).open();
verify(webSocketClient, never()).send(anyString());
// Now the socket is open
webSocketClientCallback.onOpen();
when(webSocketClient.getState()).thenReturn(WebSocketClient.State.CONNECTED);
// and we send op=connect
verify(webSocketClient, times(1)).send(contains("\"op\":\"connect\""));
// Now if we subscribe to queryB, we SHOULD NOT send the subscribe yet, until we get op=connected
SubscriptionHandling<ParseObject> subB = parseLiveQueryClient.subscribe(queryB);
verify(webSocketClient, never()).send(contains("\"op\":\"subscribe\""));
// on op=connected, _then_ we should send both subscriptions
webSocketClientCallback.onMessage(createConnectedMessage().toString());
verify(webSocketClient, times(2)).send(contains("\"op\":\"subscribe\""));
}
@Test
public void testSubscribeWhenSubscribedToCallback() throws Exception {
SubscriptionHandling.HandleSubscribeCallback<ParseObject> subscribeMockCallback = mock(SubscriptionHandling.HandleSubscribeCallback.class);
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
createSubscription(parseQuery, subscribeMockCallback);
verify(subscribeMockCallback, times(1)).onSubscribe(parseQuery);
}
@Test
public void testUnsubscribeWhenSubscribedToCallback() throws Exception {
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
SubscriptionHandling<ParseObject> subscriptionHandling = createSubscription(parseQuery,
mock(SubscriptionHandling.HandleSubscribeCallback.class));
parseLiveQueryClient.unsubscribe(parseQuery);
verify(webSocketClient, times(1)).send(any(String.class));
SubscriptionHandling.HandleUnsubscribeCallback<ParseObject> unsubscribeMockCallback = mock(
SubscriptionHandling.HandleUnsubscribeCallback.class);
subscriptionHandling.handleUnsubscribe(unsubscribeMockCallback);
webSocketClientCallback.onMessage(createUnsubscribedMessage(subscriptionHandling.getRequestId()).toString());
verify(unsubscribeMockCallback, times(1)).onUnsubscribe(parseQuery);
}
@Test
public void testErrorWhileSubscribing() throws Exception {
ParseQuery.State state = mock(ParseQuery.State.class);
when(state.constraints()).thenThrow(new RuntimeException("forced error"));
ParseQuery.State.Builder builder = mock(ParseQuery.State.Builder.class);
when(builder.build()).thenReturn(state);
ParseQuery query = mock(ParseQuery.class);
when(query.getBuilder()).thenReturn(builder);
SubscriptionHandling handling = parseLiveQueryClient.subscribe(query);
SubscriptionHandling.HandleErrorCallback<ParseObject> errorMockCallback = mock(SubscriptionHandling.HandleErrorCallback.class);
handling.handleError(errorMockCallback);
// Trigger a re-subscribe
webSocketClientCallback.onMessage(createConnectedMessage().toString());
// This will never get a chance to call op=subscribe, because an exception was thrown
verify(webSocketClient, never()).send(anyString());
ArgumentCaptor<LiveQueryException> errorCaptor = ArgumentCaptor.forClass(LiveQueryException.class);
verify(errorMockCallback, times(1)).onError(eq(query), errorCaptor.capture());
assertEquals("Error when subscribing", errorCaptor.getValue().getMessage());
assertNotNull(errorCaptor.getValue().getCause());
}
@Test
public void testErrorWhenSubscribedToCallback() throws Exception {
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
SubscriptionHandling<ParseObject> subscriptionHandling = createSubscription(parseQuery,
mock(SubscriptionHandling.HandleSubscribeCallback.class));
SubscriptionHandling.HandleErrorCallback<ParseObject> errorMockCallback = mock(SubscriptionHandling.HandleErrorCallback.class);
subscriptionHandling.handleError(errorMockCallback);
webSocketClientCallback.onMessage(createErrorMessage(subscriptionHandling.getRequestId()).toString());
ArgumentCaptor<LiveQueryException> errorCaptor = ArgumentCaptor.forClass(LiveQueryException.class);
verify(errorMockCallback, times(1)).onError(eq(parseQuery), errorCaptor.capture());
LiveQueryException genericError = errorCaptor.getValue();
assertTrue(genericError instanceof LiveQueryException.ServerReportedException);
LiveQueryException.ServerReportedException serverError = (LiveQueryException.ServerReportedException) genericError;
assertEquals(serverError.getError(), "testError");
assertEquals(serverError.getCode(), 1);
assertEquals(serverError.isReconnect(), true);
}
@Test
public void testHeterogeneousSubscriptions() throws Exception {
ParseObject.registerSubclass(MockClassA.class);
ParseObject.registerSubclass(MockClassB.class);
ParseQuery<MockClassA> query1 = ParseQuery.getQuery(MockClassA.class);
ParseQuery<MockClassB> query2 = ParseQuery.getQuery(MockClassB.class);
SubscriptionHandling<MockClassA> handle1 = parseLiveQueryClient.subscribe(query1);
SubscriptionHandling<MockClassB> handle2 = parseLiveQueryClient.subscribe(query2);
handle1.handleError(new SubscriptionHandling.HandleErrorCallback<MockClassA>() {
@Override
public void onError(ParseQuery<MockClassA> query, LiveQueryException exception) {
throw new RuntimeException(exception);
}
});
handle2.handleError(new SubscriptionHandling.HandleErrorCallback<MockClassB>() {
@Override
public void onError(ParseQuery<MockClassB> query, LiveQueryException exception) {
throw new RuntimeException(exception);
}
});
SubscriptionHandling.HandleEventCallback<MockClassA> eventMockCallback1 = mock(SubscriptionHandling.HandleEventCallback.class);
SubscriptionHandling.HandleEventCallback<MockClassB> eventMockCallback2 = mock(SubscriptionHandling.HandleEventCallback.class);
handle1.handleEvent(SubscriptionHandling.Event.CREATE, eventMockCallback1);
handle2.handleEvent(SubscriptionHandling.Event.CREATE, eventMockCallback2);
ParseObject parseObject1 = new MockClassA();
parseObject1.setObjectId("testId1");
ParseObject parseObject2 = new MockClassB();
parseObject2.setObjectId("testId2");
webSocketClientCallback.onMessage(createObjectCreateMessage(handle1.getRequestId(), parseObject1).toString());
webSocketClientCallback.onMessage(createObjectCreateMessage(handle2.getRequestId(), parseObject2).toString());
validateSameObject((SubscriptionHandling.HandleEventCallback) eventMockCallback1, (ParseQuery) query1, parseObject1);
validateSameObject((SubscriptionHandling.HandleEventCallback) eventMockCallback2, (ParseQuery) query2, parseObject2);
}
@Test
public void testCreateEventWhenSubscribedToCallback() throws Exception {
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
SubscriptionHandling<ParseObject> subscriptionHandling = createSubscription(parseQuery,
mock(SubscriptionHandling.HandleSubscribeCallback.class));
SubscriptionHandling.HandleEventCallback<ParseObject> eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class);
subscriptionHandling.handleEvent(SubscriptionHandling.Event.CREATE, eventMockCallback);
ParseObject parseObject = new ParseObject("Test");
parseObject.setObjectId("testId");
webSocketClientCallback.onMessage(createObjectCreateMessage(subscriptionHandling.getRequestId(), parseObject).toString());
validateSameObject(eventMockCallback, parseQuery, parseObject);
}
@Test
public void testEnterEventWhenSubscribedToCallback() throws Exception {
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
SubscriptionHandling<ParseObject> subscriptionHandling = createSubscription(parseQuery,
mock(SubscriptionHandling.HandleSubscribeCallback.class));
SubscriptionHandling.HandleEventCallback<ParseObject> eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class);
subscriptionHandling.handleEvent(SubscriptionHandling.Event.ENTER, eventMockCallback);
ParseObject parseObject = new ParseObject("Test");
parseObject.setObjectId("testId");
webSocketClientCallback.onMessage(createObjectEnterMessage(subscriptionHandling.getRequestId(), parseObject).toString());
validateSameObject(eventMockCallback, parseQuery, parseObject);
}
@Test
public void testUpdateEventWhenSubscribedToCallback() throws Exception {
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
SubscriptionHandling<ParseObject> subscriptionHandling = createSubscription(parseQuery,
mock(SubscriptionHandling.HandleSubscribeCallback.class));
SubscriptionHandling.HandleEventCallback<ParseObject> eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class);
subscriptionHandling.handleEvent(SubscriptionHandling.Event.UPDATE, eventMockCallback);
ParseObject parseObject = new ParseObject("Test");
parseObject.setObjectId("testId");
webSocketClientCallback.onMessage(createObjectUpdateMessage(subscriptionHandling.getRequestId(), parseObject).toString());
validateSameObject(eventMockCallback, parseQuery, parseObject);
}
@Test
public void testLeaveEventWhenSubscribedToCallback() throws Exception {
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
SubscriptionHandling<ParseObject> subscriptionHandling = createSubscription(parseQuery,
mock(SubscriptionHandling.HandleSubscribeCallback.class));
SubscriptionHandling.HandleEventCallback<ParseObject> eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class);
subscriptionHandling.handleEvent(SubscriptionHandling.Event.LEAVE, eventMockCallback);
ParseObject parseObject = new ParseObject("Test");
parseObject.setObjectId("testId");
webSocketClientCallback.onMessage(createObjectLeaveMessage(subscriptionHandling.getRequestId(), parseObject).toString());
validateSameObject(eventMockCallback, parseQuery, parseObject);
}
@Test
public void testDeleteEventWhenSubscribedToCallback() throws Exception {
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
SubscriptionHandling<ParseObject> subscriptionHandling = createSubscription(parseQuery,
mock(SubscriptionHandling.HandleSubscribeCallback.class));
SubscriptionHandling.HandleEventCallback<ParseObject> eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class);
subscriptionHandling.handleEvent(SubscriptionHandling.Event.DELETE, eventMockCallback);
ParseObject parseObject = new ParseObject("Test");
parseObject.setObjectId("testId");
webSocketClientCallback.onMessage(createObjectDeleteMessage(subscriptionHandling.getRequestId(), parseObject).toString());
validateSameObject(eventMockCallback, parseQuery, parseObject);
}
@Test
public void testCreateEventWhenSubscribedToAnyCallback() throws Exception {
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
SubscriptionHandling<ParseObject> subscriptionHandling = createSubscription(parseQuery,
mock(SubscriptionHandling.HandleSubscribeCallback.class));
SubscriptionHandling.HandleEventsCallback<ParseObject> eventsMockCallback = mock(SubscriptionHandling.HandleEventsCallback.class);
subscriptionHandling.handleEvents(eventsMockCallback);
ParseObject parseObject = new ParseObject("Test");
parseObject.setObjectId("testId");
webSocketClientCallback.onMessage(createObjectCreateMessage(subscriptionHandling.getRequestId(), parseObject).toString());
ArgumentCaptor<ParseObject> objectCaptor = ArgumentCaptor.forClass(ParseObject.class);
verify(eventsMockCallback, times(1)).onEvents(eq(parseQuery), eq(SubscriptionHandling.Event.CREATE), objectCaptor.capture());
ParseObject newParseObject = objectCaptor.getValue();
assertEquals(parseObject.getObjectId(), newParseObject.getObjectId());
}
@Test
public void testSubscriptionStoppedAfterUnsubscribe() throws Exception {
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
SubscriptionHandling<ParseObject> subscriptionHandling = createSubscription(parseQuery,
mock(SubscriptionHandling.HandleSubscribeCallback.class));
SubscriptionHandling.HandleEventCallback<ParseObject> eventMockCallback = mock(SubscriptionHandling.HandleEventCallback.class);
subscriptionHandling.handleEvent(SubscriptionHandling.Event.CREATE, eventMockCallback);
SubscriptionHandling.HandleUnsubscribeCallback<ParseObject> unsubscribeMockCallback = mock(
SubscriptionHandling.HandleUnsubscribeCallback.class);
subscriptionHandling.handleUnsubscribe(unsubscribeMockCallback);
parseLiveQueryClient.unsubscribe(parseQuery);
verify(webSocketClient, times(1)).send(any(String.class));
webSocketClientCallback.onMessage(createUnsubscribedMessage(subscriptionHandling.getRequestId()).toString());
verify(unsubscribeMockCallback, times(1)).onUnsubscribe(parseQuery);
ParseObject parseObject = new ParseObject("Test");
parseObject.setObjectId("testId");
webSocketClientCallback.onMessage(createObjectCreateMessage(subscriptionHandling.getRequestId(), parseObject).toString());
ArgumentCaptor<ParseObject> objectCaptor = ArgumentCaptor.forClass(ParseObject.class);
verify(eventMockCallback, times(0)).onEvent(eq(parseQuery), objectCaptor.capture());
}
@Test
public void testSubscriptionReplayedAfterReconnect() throws Exception {
SubscriptionHandling.HandleSubscribeCallback<ParseObject> subscribeMockCallback = mock(SubscriptionHandling.HandleSubscribeCallback.class);
ParseQuery<ParseObject> parseQuery = new ParseQuery<>("test");
createSubscription(parseQuery, subscribeMockCallback);
parseLiveQueryClient.disconnect();
reconnect();
verify(webSocketClient, times(2)).send(any(String.class));
}
@Test
public void testSessionTokenSentOnConnect() {
when(mockUser.getSessionToken()).thenReturn("the token");
parseLiveQueryClient.reconnect();
webSocketClientCallback.onOpen();
verify(webSocketClient, times(1)).send(contains("\"sessionToken\":\"the token\""));
}
@Test
public void testEmptySessionTokenOnConnect() {
parseLiveQueryClient.reconnect();
webSocketClientCallback.onOpen();
verify(webSocketClient, times(1)).send(not(contains("\"sessionToken\":")));
}
@Test
public void testSessionTokenSentOnSubscribe() {
when(mockUser.getSessionToken()).thenReturn("the token");
when(webSocketClient.getState()).thenReturn(WebSocketClient.State.CONNECTED);
parseLiveQueryClient.subscribe(ParseQuery.getQuery("Test"));
verify(webSocketClient, times(1)).send(and(
contains("\"op\":\"subscribe\""),
contains("\"sessionToken\":\"the token\"")));
}
@Test
public void testEmptySessionTokenOnSubscribe() {
when(mockUser.getSessionToken()).thenReturn("the token");
when(webSocketClient.getState()).thenReturn(WebSocketClient.State.CONNECTED);
parseLiveQueryClient.subscribe(ParseQuery.getQuery("Test"));
verify(webSocketClient, times(1)).send(contains("\"op\":\"connect\""));
verify(webSocketClient, times(1)).send(and(
contains("\"op\":\"subscribe\""),
contains("\"sessionToken\":\"the token\"")));
}
@Test
public void testCallbackNotifiedOnUnexpectedDisconnect() throws Exception {
LoggingCallbacks callbacks = new LoggingCallbacks();
parseLiveQueryClient.registerListener(callbacks);
callbacks.transcript.assertNoEventsSoFar();
// Unexpected close from the server:
webSocketClientCallback.onClose();
callbacks.transcript.assertEventsSoFar("onLiveQueryClientDisconnected: false");
}
@Test
public void testCallbackNotifiedOnExpectedDisconnect() throws Exception {
LoggingCallbacks callbacks = new LoggingCallbacks();
parseLiveQueryClient.registerListener(callbacks);
callbacks.transcript.assertNoEventsSoFar();
parseLiveQueryClient.disconnect();
verify(webSocketClient, times(1)).close();
callbacks.transcript.assertNoEventsSoFar();
// the client is a mock, so it won't actually invoke the callback automatically
webSocketClientCallback.onClose();
callbacks.transcript.assertEventsSoFar("onLiveQueryClientDisconnected: true");
}
@Test
public void testCallbackNotifiedOnConnect() throws Exception {
LoggingCallbacks callbacks = new LoggingCallbacks();
parseLiveQueryClient.registerListener(callbacks);
callbacks.transcript.assertNoEventsSoFar();
reconnect();
callbacks.transcript.assertEventsSoFar("onLiveQueryClientConnected");
}
@Test
public void testCallbackNotifiedOnSocketError() throws Exception {
LoggingCallbacks callbacks = new LoggingCallbacks();
parseLiveQueryClient.registerListener(callbacks);
callbacks.transcript.assertNoEventsSoFar();
webSocketClientCallback.onError(new IOException("bad things happened"));
callbacks.transcript.assertEventsSoFar("onSocketError: java.io.IOException: bad things happened",
"onLiveQueryClientDisconnected: false");
}
@Test
public void testCallbackNotifiedOnServerError() throws Exception {
LoggingCallbacks callbacks = new LoggingCallbacks();
parseLiveQueryClient.registerListener(callbacks);
callbacks.transcript.assertNoEventsSoFar();
webSocketClientCallback.onMessage(createErrorMessage(1).toString());
callbacks.transcript.assertEventsSoFar("onLiveQueryError: com.parse.LiveQueryException$ServerReportedException: Server reported error; code: 1, error: testError, reconnect: true");
}
private SubscriptionHandling<ParseObject> createSubscription(ParseQuery<ParseObject> parseQuery,
SubscriptionHandling.HandleSubscribeCallback<ParseObject> subscribeMockCallback) throws Exception {
SubscriptionHandling<ParseObject> subscriptionHandling = parseLiveQueryClient.subscribe(parseQuery).handleSubscribe(subscribeMockCallback);
webSocketClientCallback.onMessage(createSubscribedMessage(subscriptionHandling.getRequestId()).toString());
return subscriptionHandling;
}
private void validateSameObject(SubscriptionHandling.HandleEventCallback<ParseObject> eventMockCallback,
ParseQuery<ParseObject> parseQuery,
ParseObject originalParseObject) {
ArgumentCaptor<ParseObject> objectCaptor = ArgumentCaptor.forClass(ParseObject.class);
verify(eventMockCallback, times(1)).onEvent(eq(parseQuery), objectCaptor.capture());
ParseObject newParseObject = objectCaptor.getValue();
assertEquals(originalParseObject.getClassName(), newParseObject.getClassName());
assertEquals(originalParseObject.getObjectId(), newParseObject.getObjectId());
}
private void clearConnection() {
webSocketClient = null;
webSocketClientCallback = null;
}
private void reconnect() {
parseLiveQueryClient.reconnect();
webSocketClientCallback.onOpen();
try {
webSocketClientCallback.onMessage(createConnectedMessage().toString());
} catch (JSONException e) {
e.printStackTrace();
}
}
private static JSONObject createConnectedMessage() throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "connected");
return jsonObject;
}
private static JSONObject createSubscribedMessage(int requestId) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "subscribed");
jsonObject.put("clientId", 1);
jsonObject.put("requestId", requestId);
return jsonObject;
}
private static JSONObject createUnsubscribedMessage(int requestId) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "unsubscribed");
jsonObject.put("requestId", requestId);
return jsonObject;
}
private static JSONObject createErrorMessage(int requestId) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "error");
jsonObject.put("requestId", requestId);
jsonObject.put("code", 1);
jsonObject.put("error", "testError");
jsonObject.put("reconnect", true);
return jsonObject;
}
private static JSONObject createObjectCreateMessage(int requestId, ParseObject parseObject) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "create");
jsonObject.put("requestId", requestId);
jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject));
return jsonObject;
}
private static JSONObject createObjectEnterMessage(int requestId, ParseObject parseObject) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "enter");
jsonObject.put("requestId", requestId);
jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject));
return jsonObject;
}
private static JSONObject createObjectUpdateMessage(int requestId, ParseObject parseObject) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "update");
jsonObject.put("requestId", requestId);
jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject));
return jsonObject;
}
private static JSONObject createObjectLeaveMessage(int requestId, ParseObject parseObject) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "leave");
jsonObject.put("requestId", requestId);
jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject));
return jsonObject;
}
private static JSONObject createObjectDeleteMessage(int requestId, ParseObject parseObject) throws JSONException {
JSONObject jsonObject = new JSONObject();
jsonObject.put("op", "delete");
jsonObject.put("requestId", requestId);
jsonObject.put("object", PointerEncoder.get().encodeRelatedObject(parseObject));
return jsonObject;
}
private static class LoggingCallbacks implements ParseLiveQueryClientCallbacks {
final Transcript transcript = new Transcript();
@Override
public void onLiveQueryClientConnected(ParseLiveQueryClient client) {
transcript.add("onLiveQueryClientConnected");
}
@Override
public void onLiveQueryClientDisconnected(ParseLiveQueryClient client, boolean userInitiated) {
transcript.add("onLiveQueryClientDisconnected: " + userInitiated);
}
@Override
public void onLiveQueryError(ParseLiveQueryClient client, LiveQueryException reason) {
transcript.add("onLiveQueryError: " + reason);
}
@Override
public void onSocketError(ParseLiveQueryClient client, Throwable reason) {
transcript.add("onSocketError: " + reason);
}
}
@ParseClassName("MockA")
static class MockClassA extends ParseObject {
}
@ParseClassName("MockB")
static class MockClassB extends ParseObject {
}
}