/* * Copyright 2000-2016 Vaadin Ltd. * * 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.vaadin.server.communication; import java.io.IOException; import java.io.ObjectInputStream; import java.io.Reader; import java.io.Serializable; import java.io.StringReader; import java.io.StringWriter; import java.io.Writer; import java.util.concurrent.Future; import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeoutException; import java.util.logging.ConsoleHandler; import java.util.logging.Level; import java.util.logging.LogRecord; import java.util.logging.Logger; import org.atmosphere.cpr.AtmosphereResource; import org.atmosphere.cpr.AtmosphereResource.TRANSPORT; import org.atmosphere.util.Version; import com.vaadin.shared.communication.PushConstants; import com.vaadin.ui.UI; /** * A {@link PushConnection} implementation using the Atmosphere push support * that is by default included in Vaadin. * * @author Vaadin Ltd * @since 7.1 */ public class AtmospherePushConnection implements PushConnection { public static String getAtmosphereVersion() { try { String v = Version.getRawVersion(); assert v != null; return v; } catch (NoClassDefFoundError e) { return null; } } /** * Represents a message that can arrive as multiple fragments. */ protected static class FragmentedMessage implements Serializable { private final StringBuilder message = new StringBuilder(); private final int messageLength; public FragmentedMessage(Reader reader) throws IOException { // Messages are prefixed by the total message length plus a // delimiter String length = ""; int c; while ((c = reader.read()) != -1 && c != PushConstants.MESSAGE_DELIMITER) { length += (char) c; } try { messageLength = Integer.parseInt(length); } catch (NumberFormatException e) { throw new IOException("Invalid message length " + length, e); } } /** * Appends all the data from the given Reader to this message and * returns whether the message was completed. * * @param reader * The Reader from which to read. * @return true if this message is complete, false otherwise. * @throws IOException */ public boolean append(Reader reader) throws IOException { char[] buffer = new char[PushConstants.WEBSOCKET_BUFFER_SIZE]; int read; while ((read = reader.read(buffer)) != -1) { message.append(buffer, 0, read); assert message.length() <= messageLength : "Received message " + message.length() + "chars, expected " + messageLength; } return message.length() == messageLength; } public Reader getReader() { return new StringReader(message.toString()); } } protected enum State { /** * Not connected. Trying to push will set the connection state to * PUSH_PENDING or RESPONSE_PENDING and defer sending the message until * a connection is established. */ DISCONNECTED, /** * Not connected. An asynchronous push is pending the opening of the * connection. */ PUSH_PENDING, /** * Not connected. A response to a client request is pending the opening * of the connection. */ RESPONSE_PENDING, /** * Connected. Messages can be sent through the connection. */ CONNECTED; } private final UI ui; private transient State state = State.DISCONNECTED; private transient AtmosphereResource resource; private transient FragmentedMessage incomingMessage; private transient Future<Object> outgoingMessage; public AtmospherePushConnection(UI ui) { this.ui = ui; } @Override public void push() { push(true); } /** * Pushes pending state changes and client RPC calls to the client. If * {@code isConnected()} is false, defers the push until a connection is * established. * * @param async * True if this push asynchronously originates from the server, * false if it is a response to a client request. */ public void push(boolean async) { if (!isConnected()) { if (async && state != State.RESPONSE_PENDING) { state = State.PUSH_PENDING; } else { state = State.RESPONSE_PENDING; } } else { try { Writer writer = new StringWriter(); new UidlWriter().write(getUI(), writer, async); sendMessage("for(;;);[{" + writer.toString() + "}]"); } catch (Exception e) { throw new RuntimeException("Push failed", e); } } } /** * Sends the given message to the current client. Cannot be called if * {@isConnected()} is false. * * @param message * The message to send */ void sendMessage(String message) { assert (isConnected()); // "Broadcast" the changes to the single client only outgoingMessage = getResource().getBroadcaster().broadcast(message, getResource()); } /** * Reads and buffers a (possibly partial) message. If a complete message was * received, or if the call resulted in the completion of a partially * received message, returns a {@link Reader} yielding the complete message. * Otherwise, returns null. * * @param reader * A Reader from which to read the (partial) message * @return A Reader yielding a complete message or null if the message is * not yet complete. * @throws IOException */ protected Reader receiveMessage(Reader reader) throws IOException { if (resource == null || resource.transport() != TRANSPORT.WEBSOCKET) { return reader; } if (incomingMessage == null) { // No existing partially received message incomingMessage = new FragmentedMessage(reader); } if (incomingMessage.append(reader)) { // Message is complete Reader completeReader = incomingMessage.getReader(); incomingMessage = null; return completeReader; } else { // Only received a partial message return null; } } @Override public boolean isConnected() { assert state != null; assert (state == State.CONNECTED) ^ (resource == null); return state == State.CONNECTED; } /** * Associates this {@link AtmospherePushConnection} with the given * {@link AtmosphereResource} representing an established push connection. If * already connected, calls {@link #disconnect()} first. If there is a * deferred push, carries it out via the new connection. * * @since 7.2 */ public void connect(AtmosphereResource resource) { assert resource != null; assert resource != this.resource; if (isConnected()) { disconnect(); } this.resource = resource; State oldState = state; state = State.CONNECTED; if (oldState == State.PUSH_PENDING || oldState == State.RESPONSE_PENDING) { // Sending a "response" message (async=false) also takes care of a // pending push, but not vice versa push(oldState == State.PUSH_PENDING); } } /** * Gets the UI this push connection is associated with. * * @return the UI associated with this connection */ public UI getUI() { return ui; } /** * Gets the atmosphere resource associated with this connection. * * @return The AtmosphereResource associated with this connection or * <code>null</code> if the connection is not open. */ public AtmosphereResource getResource() { return resource; } @Override public void disconnect() { assert isConnected(); if (resource == null) { // Already disconnected. Should not happen but if it does, we don't // want to cause NPEs getLogger().fine( "AtmospherePushConnection.disconnect() called twice, this should not happen"); return; } if (resource.isResumed()) { // This can happen for long polling because of // http://dev.vaadin.com/ticket/16919 // Once that is fixed, this should never happen connectionLost(); return; } if (outgoingMessage != null) { // Wait for the last message to be sent before closing the // connection (assumes that futures are completed in order) try { outgoingMessage.get(1000, TimeUnit.MILLISECONDS); } catch (TimeoutException e) { getLogger().log(Level.INFO, "Timeout waiting for messages to be sent to client before disconnect"); } catch (Exception e) { getLogger().log(Level.INFO, "Error waiting for messages to be sent to client before disconnect"); } outgoingMessage = null; } try { resource.close(); } catch (IOException e) { getLogger().log(Level.INFO, "Error when closing push connection", e); } connectionLost(); } /** * Called when the connection to the client has been lost. * * @since 7.4.1 */ public void connectionLost() { resource = null; if (state == State.CONNECTED) { // Guard against connectionLost being (incorrectly) called when // state is PUSH_PENDING or RESPONSE_PENDING // (http://dev.vaadin.com/ticket/16919) state = State.DISCONNECTED; } } /** * Returns the state of this connection. */ protected State getState() { return state; } /** * Reinitializes this PushConnection after deserialization. The connection * is initially in disconnected state; the client will handle the * reconnecting. */ private void readObject(ObjectInputStream stream) throws IOException, ClassNotFoundException { stream.defaultReadObject(); state = State.DISCONNECTED; } private static Logger getLogger() { return Logger.getLogger(AtmospherePushConnection.class.getName()); } /** * Internal method used for reconfiguring loggers to show all Atmosphere log * messages in the console. * * @since 7.6 */ public static void enableAtmosphereDebugLogging() { Level level = Level.FINEST; Logger atmosphereLogger = Logger.getLogger("org.atmosphere"); if (atmosphereLogger.getLevel() == level) { // Already enabled return; } atmosphereLogger.setLevel(level); // Without this logging, we will have a ClassCircularityError LogRecord record = new LogRecord(Level.INFO, "Enabling Atmosphere debug logging"); atmosphereLogger.log(record); ConsoleHandler ch = new ConsoleHandler(); ch.setLevel(Level.ALL); atmosphereLogger.addHandler(ch); } }