Java tutorial
// -*- mode: java; c-basic-offset: 2; -*- // Copyright 2009-2011 Google, All Rights reserved // Copyright 2011-2012 MIT, All rights reserved // Released under the Apache License, Version 2.0 // http://www.apache.org/licenses/LICENSE-2.0 package com.google.appinventor.client.jsonp; import com.google.common.annotations.VisibleForTesting; import com.google.common.base.Function; import com.google.common.base.Functions; import com.google.common.base.Preconditions; import com.google.appinventor.common.jsonp.JsonpConstants; import com.google.appinventor.shared.jsonp.JsonpConnectionInfo; import com.google.appinventor.shared.properties.json.JSONArray; import com.google.appinventor.shared.properties.json.JSONParser; import com.google.appinventor.shared.properties.json.JSONValue; import com.google.gwt.dom.client.Document; import com.google.gwt.dom.client.Element; import com.google.gwt.dom.client.ScriptElement; import com.google.gwt.user.client.Timer; import com.google.gwt.user.client.rpc.AsyncCallback; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Class used for communicating with an HTTP server via JSONP requests. * * @author lizlooney@google.com (Liz Looney) */ public class JsonpConnection { private static final int CONTACT_REQUEST_TIMER_DELAY = 1000; private static final int MINIMUM_UNRESPONSIVE_TIME_LIMIT = 2000; private static int jsonpRequestCounter = 0; private static final Map<String, AsyncCallback<String>> CALLBACKS = new HashMap<String, AsyncCallback<String>>(); // Functions that will be used to transform the server's responses to the appropriate types. // Those that don't need the jsonParser can be static. protected static final Function<String, String> stringResponseDecoder = Functions.<String>identity(); protected static final Function<String, Void> voidResponseDecoder = new Function<String, Void>() { public Void apply(String response) { return null; } }; @VisibleForTesting public static final Function<String, Boolean> booleanResponseDecoder = new Function<String, Boolean>() { public Boolean apply(String response) { return Boolean.valueOf(response); } }; @VisibleForTesting public static final Function<String, Integer> integerResponseDecoder = new Function<String, Integer>() { public Integer apply(String response) { return Integer.valueOf(response); } }; @VisibleForTesting public final Function<String, String[]> stringArrayResponseDecoder; /** * The connection information for the JSONP server. */ private final JsonpConnectionInfo connInfo; /** * The list of {@link ConnectivityListener}. */ private final List<ConnectivityListener> connectivityListeners; /** * A timer for sending a contact request periodically to see if the server * is still alive. */ private Timer contactTimer; /** * The time that last contact request was sent. */ private long contactRequestTime; /** * The time limit (in milliseconds) within which we expect to have received a * response to the contact request. */ private int unresponsiveTimeLimit; /** * A timer used to recognize that the server is unresponsive. */ private Timer unresponsiveConnectionTimer; /** * The connectivity status. If false, the server might be dead. */ private boolean connectivityStatus; /** * The JSON parser used to decode responses. */ private final JSONParser jsonParser; /** * A {@link Function} used to escape URL query parameters. */ private final Function<String, String> escapeQueryParameterFunction; /** * Creates a JsonpConnection object with the given connection information, * JSON parser, and escape function. * * @param connInfo the JSONP connection information * @param jsonParser the JSON parser * @param escapeQueryParameterFunction the escape function */ public JsonpConnection(JsonpConnectionInfo connInfo, final JSONParser jsonParser, Function<String, String> escapeQueryParameterFunction) { this.connInfo = connInfo; this.jsonParser = jsonParser; this.escapeQueryParameterFunction = escapeQueryParameterFunction; stringArrayResponseDecoder = new Function<String, String[]>() { public String[] apply(String response) { if (response != null) { JSONValue jsonValue = jsonParser.parse(response); if (jsonValue != null) { JSONArray jsonArray = jsonValue.asArray(); int size = jsonArray.size(); String[] strings = new String[size]; for (int i = 0; i < size; i++) { JSONValue element = jsonArray.get(i); strings[i] = (element != null) ? element.asString().getString() : null; } return strings; } } return null; } }; connectivityListeners = new ArrayList<ConnectivityListener>(); connectivityStatus = true; unresponsiveTimeLimit = MINIMUM_UNRESPONSIVE_TIME_LIMIT; } /** * Sends a JSONP request by embedding a script tag into the document. * Generates a new id for the request. * * @param request the request to be made * @param parameters the parameters for the request * @param function a function that transforms the response into the type * that the callback needs * @param callback the callback that should be called with the transformed * response */ public <T> void sendJsonpRequest(String request, Map<String, Object> parameters, Function<String, ? extends T> function, AsyncCallback<T> callback) { String id = getNextRequestId(); sendJsonpRequest(id, request, parameters, function, callback); } /** * Returns the next request id; */ @VisibleForTesting public String getNextRequestId() { return "jr_" + (jsonpRequestCounter++); } /** * Sends a JSONP request by embedding a SCRIPT tag into the document. * * @param id the id used for the script tag and to identify the callback * @param request the request to be made * @param parameters the parameters for the request * @param function a function that transforms the response into the type * that the callback needs * @param callback the callback that should be called with the transformed * response */ private <T> void sendJsonpRequest(String id, String request, Map<String, Object> parameters, final Function<String, ? extends T> function, final AsyncCallback<T> callback) { Preconditions.checkNotNull(id); // Prepare an intermediate callback that converts the String result to T. if (callback != null) { Preconditions.checkNotNull(function); CALLBACKS.put(id, new AsyncCallback<String>() { @Override public void onSuccess(String jsonResult) { T result; try { result = function.apply(jsonResult); } catch (RuntimeException e) { callback.onFailure(e); return; } callback.onSuccess(result); } @Override public void onFailure(Throwable caught) { callback.onFailure(caught); } }); } // Insert a script tag into the document. Document document = Document.get(); ScriptElement script = document.createScriptElement(); String uri = makeURI(request, parameters, id); script.setSrc(uri); script.setId(id); Element bodyElement = document.getElementsByTagName("body").getItem(0); Element previous = document.getElementById(id); if (previous != null) { bodyElement.replaceChild(script, previous); } else { bodyElement.appendChild(script); } } /** * Sends a JSONP request by embedding a SCRIPT tag into the document and * then polls for the result by resending the request with the parameter * {@link JsonpConstants#POLLING}. We continue polling until we receive a * response that is not {@link JsonpConstants#NOT_FINISHED_YET}. * * @param request the request to be made * @param p the parameters for the request * @param function a function that transforms the response into the type * that the callback needs * @param callback the callback that should be called with the transformed * response */ public <T> void sendJsonpRequestAndPoll(final String request, Map<String, Object> p, final Function<String, ? extends T> function, final AsyncCallback<T> callback) { // Make a new parameters Map so we can add the POLLING parameter below. final Map<String, Object> parameters = new HashMap<String, Object>(); if (p != null) { parameters.putAll(p); } final String initialRequestId = getNextRequestId(); AsyncCallback<String> pollingCallback = new AsyncCallback<String>() { @Override public void onSuccess(String response) { if (JsonpConstants.NOT_FINISHED_YET.equals(response)) { // No response is available yet, create a timer to try again in a second. parameters.put(JsonpConstants.POLLING, initialRequestId); final AsyncCallback<String> pollingCallback = this; Timer timer = new Timer() { @Override public void run() { sendJsonpRequest(request, parameters, stringResponseDecoder, pollingCallback); } }; timer.schedule(1000); // schedule the timer } else { // The response is available, convert it to the correct type T. T result; try { result = function.apply(response); } catch (RuntimeException e) { callback.onFailure(e); return; } callback.onSuccess(result); } } @Override public void onFailure(Throwable caught) { callback.onFailure(caught); } }; sendJsonpRequest(initialRequestId, request, parameters, stringResponseDecoder, pollingCallback); } /** * Returns the URI for a JSONP request. * * @param request the request to be made * @param parameters the parameters for the request * @param id the id for the request */ @VisibleForTesting public String makeURI(String request, Map<String, Object> parameters, String id) { StringBuilder sb = new StringBuilder(); sb.append("http://127.0.0.1:").append(connInfo.getPort()).append("/").append(request); sb.append("?").append(JsonpConstants.OUTPUT).append("=").append(JsonpConstants.REQUIRED_OUTPUT_VALUE); sb.append("&").append(JsonpConstants.CALLBACK).append("=").append(JsonpConstants.REQUIRED_CALLBACK_VALUE); sb.append("&").append(JsonpConstants.ID).append("=").append(escapeQueryParameterFunction.apply(id)); sb.append("&").append(JsonpConstants.SECRET).append("=").append(connInfo.getSecret()); if (parameters != null) { for (Map.Entry<String, Object> parameter : parameters.entrySet()) { sb.append("&").append(parameter.getKey()).append("=") .append(escapeQueryParameterFunction.apply(parameter.getValue().toString())); } } return sb.toString(); } /** * Define the jsonpcb method that is called from javascript for all JSONP * requests that we make. */ public static native void defineBridgeMethod() /*-{ $wnd.jsonpcb = function(id, success, s) { if (s != null) { s = decodeURIComponent(s); } @com.google.appinventor.client.jsonp.JsonpConnection::jsonpcb(Ljava/lang/String;ZLjava/lang/String;) (id, success, s); } }-*/; /** * Called from the native jsonpcb method defined above, for all JSONP * requests that we make. * * @param id the id of the script tag that made the JSONP request * @param success whether the request was successful or not * @param response the response produced by the JSONP request if successful, * or the exception message if not successful */ public static void jsonpcb(String id, boolean success, String response) { // Remove the script tag from the document. removeScriptTag(id); // Remove the callback from the CALLBACKS map. AsyncCallback<String> callback = CALLBACKS.remove(id); // Call the callback's onSuccess or onFailure method. if (callback != null) { if (success) { callback.onSuccess(response); } else { callback.onFailure(new RuntimeException(response)); } } } /** * Removes the script tag with the given id from the document. */ private static void removeScriptTag(String id) { Document document = Document.get(); Element element = document.getElementById(id); if (element != null) { document.getElementsByTagName("body").getItem(0).removeChild(element); } } /** * Removes the JSONP request with the given id. */ public void removeJsonpRequest(String id) { removeScriptTag(id); CALLBACKS.remove(id); } /** * Sends a JSONP request to the HTTP server telling it to quit. */ public void quit() { sendJsonpRequest(JsonpConstants.QUIT, null, null, null); } /** * Registers the given @{link ConnectivityListener}. */ public void addConnectivityListener(ConnectivityListener listener) { connectivityListeners.add(listener); if (connectivityListeners.size() == 1) { createConnectivityTimers(); contactTimer.schedule(CONTACT_REQUEST_TIMER_DELAY); } } /** * Unregisters the given @{link ConnectivityListener}. */ public void removeConnectivityListener(ConnectivityListener listener) { connectivityListeners.remove(listener); if (connectivityListeners.size() == 0) { contactTimer.cancel(); } } private void createConnectivityTimers() { // We create these GWT timers lazily. // We must not create them during a java test because they will cause test failures. if (contactTimer == null) { // Create (but don't schedule) a timer to send a contact request. contactTimer = new Timer() { @Override public void run() { sendContactRequest(); } }; } if (unresponsiveConnectionTimer == null) { unresponsiveConnectionTimer = new Timer() { @Override public void run() { handleUnresponsiveConnection(); } }; } } private void sendContactRequest() { contactRequestTime = System.currentTimeMillis(); unresponsiveConnectionTimer.schedule(unresponsiveTimeLimit); sendJsonpRequest(JsonpConstants.CONTACT, null, voidResponseDecoder, new AsyncCallback<Void>() { @Override public void onSuccess(Void response) { receivedContactResponse(); } @Override public void onFailure(Throwable caught) { // We got a response, even if it is a failure response. receivedContactResponse(); } }); } private void receivedContactResponse() { int elapsedTime = (int) (System.currentTimeMillis() - contactRequestTime); contactRequestTime = 0; unresponsiveConnectionTimer.cancel(); setConnectivityStatus(true); // Adjust the unresponsiveTimeLimit. unresponsiveTimeLimit = Math.max(MINIMUM_UNRESPONSIVE_TIME_LIMIT, elapsedTime); if (connectivityListeners.size() >= 1) { contactTimer.schedule(CONTACT_REQUEST_TIMER_DELAY); } } private void handleUnresponsiveConnection() { // Just in case the unresponsiveConnectionTimer still goes off *after* we've canceled it, check // that contactRequestTime is not 0 before calling setConnectivityStatus. if (contactRequestTime != 0) { setConnectivityStatus(false); } } private void setConnectivityStatus(boolean newConnectivityStatus) { // This method may be called repeatedly with true (or false). We only need to do something when // the connectivity status changes. if (connectivityStatus != newConnectivityStatus) { connectivityStatus = newConnectivityStatus; fireConnectivityStatusChange(); } } private void fireConnectivityStatusChange() { // Since listeners may choose to remove themselves during their callback, we use a copy in the // for loop here. List<ConnectivityListener> listenersCopy = new ArrayList<ConnectivityListener>(connectivityListeners); for (ConnectivityListener listener : listenersCopy) { listener.onConnectivityStatusChange(this, connectivityStatus); } } }