Java tutorial
/* * Copyright (c) 2009 Jens Scheffler (appenginefan.com) * * 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 * * http://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. */ package com.appenginefan.toolkit.common; import java.net.URL; import java.util.ConcurrentModificationException; import java.util.Queue; import java.util.concurrent.ConcurrentLinkedQueue; import java.util.logging.Level; import java.util.logging.Logger; import org.json.JSONArray; import org.json.JSONException; import org.json.JSONObject; /** * A client than can connect to a WebConnection. * WebConnection objects can be used in App Engine applications * to replace socket-based services. This class can be used as * basis of communicating with such an end point on the web. */ public class WebConnectionClient { private static final Logger LOG = Logger.getLogger(WebConnectionClient.class.getName()); static final String META = "meta"; static final String CLOSED = "closed"; /** * Represents the receiving end of the communication */ public static interface Receiver { public void receive(String message); } /** * Abstraction of everything that depends on the runtime environment, * like system clock, threading, or network. Easy to replace with * mocks for unit tests. */ public static interface Environment { /** * Performs an http request to the server * @param data the data to be transmitted * @return the response payload, or null if the connection failed. */ public String fetch(String data); /** * Controls the execution of the client's run-method in an independent * thread. Similar to the Executor interface, just not for generic runnables, * and it would also work in Java 1.3 */ public void execute(WebConnectionClient client); /** * Holds the current thread for a certain amount of milliseconds */ public void sleep(long millis) throws InterruptedException; /** * Gets the current time in milliseconds */ public long currentTimeMillis(); } private final Queue<String> outqueue = new ConcurrentLinkedQueue<String>(); private final Environment env; private final int silencePeriodInMillis; private final int maxMessages; private boolean isInterrupted = false; private boolean isStarted = false; private Receiver receiver; /** * Constructor. Uses a simple environment implementation that opens the socket in a new * thread and uses URL.openStream() to connect to the WebSocket * @param url the URL to connect to * @param silencePeriodInMillis the time the thread should wait between each http call * @param maxMessages the maximum amount of messages that should be transported in one http request */ public WebConnectionClient(final URL url, int silencePeriodInMillis, int maxMessages) { super(); this.env = new HttpClientEnvironment(url.toString()); this.maxMessages = maxMessages; this.silencePeriodInMillis = silencePeriodInMillis; } /** * Constructor * @param env the Environment that this client runs in * @param silencePeriodInMillis the time the thread should wait between each http call * @param maxMessages the maximum amount of messages that should be transported in one http request */ public WebConnectionClient(Environment env, int silencePeriodInMillis, int maxMessages) { super(); this.env = env; this.maxMessages = maxMessages; this.silencePeriodInMillis = silencePeriodInMillis; } /** * Checks if the interrupted-flag is set (using synchronization on this object) */ private synchronized void checkForInterruption() throws InterruptedException { if (isInterrupted) { throw new InterruptedException(); } } /** * Sleeps for a while. */ private void sleep(long lastComm) throws InterruptedException { long diff = silencePeriodInMillis - (env.currentTimeMillis() - lastComm); if (diff > 0) { env.sleep(diff); } } /** * Signals a running executing thread to cease its work. */ public synchronized void close() { isInterrupted = true; } /** * Starts a thread (through the executor) that connects to the server on a regular base */ public synchronized void open(Receiver receiver) { if (isStarted) { throw new ConcurrentModificationException("Cannot call open more than once!"); } isStarted = true; this.receiver = receiver; env.execute(this); } /** * Enqueues an object for transmission. */ public void send(String message) { outqueue.add(message); } /** * For unit tests, retrieve the queue that internally stores the XML snippets. */ Queue<String> getQueue() { return outqueue; } /** * Performs the communication loop. Visible for unit tests and the "Environment" implementation. */ void run() throws InterruptedException { // Some tool variables we will need throughout this method final PayloadBuilder payload = new PayloadBuilder(); // constructs JSON messages String lastMeta = null; // The last "meta"-tag that was sent from the server long lastCommunication = 0; // the last time we tried to contact the server // Initial connection: an empty payload is sent. All we expect the response to be // at this point is parseable xml with a meta-element that we can begin conversation with while (lastMeta == null) { checkForInterruption(); lastCommunication = env.currentTimeMillis(); String hello = env.fetch(payload.toString()); try { if (hello != null) { JSONObject parsed = new JSONObject(hello); lastMeta = parsed.getString(META); if (parsed.has(CLOSED)) { close(); break; } } } catch (JSONException parseFailed) { LOG.severe("Could not parse http response" + hello); } if (lastMeta == null) { sleep(lastCommunication); } } payload.reset(); payload.setProperty(META, lastMeta); // Payload exchange while (true) { // First, check if we should terminate, or at least sleep a little while checkForInterruption(); sleep(lastCommunication); // Fill the payload while (payload.size() < maxMessages && !outqueue.isEmpty()) { payload.addPayload(outqueue.poll()); } // Submit the data as a request and evaluate the response lastCommunication = env.currentTimeMillis(); final String response = env.fetch(payload.toString()); if (response != null) { // Parse the xml element and extract the last meta. If there is // no meta, something went wrong, and we should try again later JSONArray responseMessages = null; try { JSONObject parsed = new JSONObject(response); final String newMeta = parsed.getString(META); if (lastMeta == null) { continue; } else { lastMeta = newMeta; } responseMessages = parsed.getJSONArray(PayloadBuilder.TAG); if (parsed.has(CLOSED)) { close(); } } catch (JSONException parseFailed) { LOG.severe("Could not parse http response" + response); } // Iterate through the children and submit them to the parser. If a message fails, // we log the issue but continue for (int i = 0; i < responseMessages.length(); i++) { checkForInterruption(); try { String message = responseMessages.getString(i); receiver.receive(message); } catch (JSONException e) { LOG.log(Level.WARNING, "JSON exception for index " + i, e); } catch (RuntimeException e) { LOG.log(Level.WARNING, "Runtime Exception for index " + i, e); } catch (Error e) { LOG.log(Level.WARNING, "Runtime Error for index " + i, e); } } // Now that we successfully transmitted the messages, let's reset the buffer to make space // for new messages from the queue payload.reset(); payload.setProperty(META, lastMeta); } } } }