All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.glassfish.tyrus.core.TyrusSession Maven / Gradle / Ivy

The newest version!
/*
 * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
 *
 * Copyright (c) 2011-2014 Oracle and/or its affiliates. All rights reserved.
 *
 * The contents of this file are subject to the terms of either the GNU
 * General Public License Version 2 only ("GPL") or the Common Development
 * and Distribution License("CDDL") (collectively, the "License").  You
 * may not use this file except in compliance with the License.  You can
 * obtain a copy of the License at
 * http://glassfish.java.net/public/CDDL+GPL_1_1.html
 * or packager/legal/LICENSE.txt.  See the License for the specific
 * language governing permissions and limitations under the License.
 *
 * When distributing the software, include this License Header Notice in each
 * file and include the License file at packager/legal/LICENSE.txt.
 *
 * GPL Classpath Exception:
 * Oracle designates this particular file as subject to the "Classpath"
 * exception as provided by Oracle in the GPL Version 2 section of the License
 * file that accompanied this code.
 *
 * Modifications:
 * If applicable, add the following below the License Header, with the fields
 * enclosed by brackets [] replaced by your own identifying information:
 * "Portions Copyright [year] [name of copyright owner]"
 *
 * Contributor(s):
 * If you wish your version of this file to be governed by only the CDDL or
 * only the GPL Version 2, indicate your decision by adding "[Contributor]
 * elects to include this software in this distribution under the [CDDL or GPL
 * Version 2] license."  If you don't indicate a single choice of license, a
 * recipient has the option to distribute your version of this file under
 * either the CDDL, the GPL Version 2 or to extend the choice of license to
 * its licensees as provided above.  However, if you add GPL Version 2 code
 * and therefore, elected the GPL Version 2 license, then the option applies
 * only if the new code is made subject to such option by the copyright
 * holder.
 */
package org.glassfish.tyrus.core;


import java.io.IOException;
import java.io.Serializable;
import java.net.URI;
import java.nio.ByteBuffer;
import java.nio.charset.Charset;
import java.security.Principal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.Future;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.ScheduledFuture;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;
import java.util.logging.Level;
import java.util.logging.Logger;

import javax.websocket.CloseReason;
import javax.websocket.DecodeException;
import javax.websocket.Decoder;
import javax.websocket.Extension;
import javax.websocket.MessageHandler;
import javax.websocket.PongMessage;
import javax.websocket.Session;
import javax.websocket.WebSocketContainer;

/**
 * Implementation of the {@link Session}.
 *
 * @author Danny Coward (danny.coward at oracle.com)
 * @author Stepan Kopriva (stepan.kopriva at oracle.com)
 * @author Martin Matula (martin.matula at oracle.com)
 * @author Pavel Bucek (pavel.bucek at oracle.com)
 */
public class TyrusSession implements Session {

    private static final Logger LOGGER = Logger.getLogger(TyrusSession.class.getName());
    private static final String SESSION_CLOSED = "The connection has been closed.";
    private final WebSocketContainer container;
    private final TyrusEndpointWrapper endpoint;
    private final RemoteEndpointWrapper.Basic basicRemote;
    private final RemoteEndpointWrapper.Async asyncRemote;
    private final boolean isSecure;
    private final URI uri;
    private final String queryString;
    private final Map pathParameters;
    private final Principal userPrincipal;
    private final Map> requestParameterMap;
    private final Object idleTimeoutLock = new Object();
    private final String id = UUID.randomUUID().toString();
    private final Map userProperties = new HashMap();
    private final MessageHandlerManager handlerManager;
    private final AtomicReference state = new AtomicReference(State.RUNNING);
    private final TextBuffer textBuffer = new TextBuffer();
    private final BinaryBuffer binaryBuffer = new BinaryBuffer();
    private final List negotiatedExtensions;
    private final String negotiatedSubprotocol;

    private volatile long maxIdleTimeout = 0;
    private volatile ScheduledFuture idleTimeoutFuture = null;
    private int maxBinaryMessageBufferSize = Integer.MAX_VALUE;
    private int maxTextMessageBufferSize = Integer.MAX_VALUE;
    private ScheduledExecutorService service;
    private ReaderBuffer readerBuffer;
    private InputStreamBuffer inputStreamBuffer;

    TyrusSession(WebSocketContainer container, TyrusRemoteEndpoint remoteEndpoint, TyrusEndpointWrapper tyrusEndpointWrapper,
                 String subprotocol, List extensions, boolean isSecure,
                 URI uri, String queryString, Map pathParameters, Principal principal,
                 Map> requestParameterMap) {
        this.container = container;
        this.endpoint = tyrusEndpointWrapper;
        this.negotiatedExtensions = extensions == null ? Collections.emptyList() : Collections.unmodifiableList(extensions);
        this.negotiatedSubprotocol = subprotocol == null ? "" : subprotocol;
        this.isSecure = isSecure;
        this.uri = uri;
        this.queryString = queryString;
        this.pathParameters = pathParameters == null ? Collections.emptyMap() : Collections.unmodifiableMap(new HashMap(pathParameters));
        this.basicRemote = new RemoteEndpointWrapper.Basic(this, remoteEndpoint, tyrusEndpointWrapper);
        this.asyncRemote = new RemoteEndpointWrapper.Async(this, remoteEndpoint, tyrusEndpointWrapper);
        this.handlerManager = MessageHandlerManager.fromDecoderInstances(tyrusEndpointWrapper.getDecoders());
        this.userPrincipal = principal;
        this.requestParameterMap = requestParameterMap == null ? Collections.>emptyMap() : Collections.unmodifiableMap(new HashMap>(requestParameterMap));

        if (container != null) {
            maxTextMessageBufferSize = container.getDefaultMaxTextMessageBufferSize();
            maxBinaryMessageBufferSize = container.getDefaultMaxBinaryMessageBufferSize();
            service = ((ExecutorServiceProvider) container).getScheduledExecutorService();
            setMaxIdleTimeout(container.getDefaultMaxSessionIdleTimeout());
        }
    }

    /**
     * Web Socket protocol version used.
     *
     * @return protocol version
     */
    @Override
    public String getProtocolVersion() {
        return "13"; // TODO
    }

    @Override
    public String getNegotiatedSubprotocol() {
        return negotiatedSubprotocol;
    }

    @Override
    public javax.websocket.RemoteEndpoint.Async getAsyncRemote() {
        checkConnectionState(State.CLOSED, State.CLOSING);
        return asyncRemote;
    }

    @Override
    public javax.websocket.RemoteEndpoint.Basic getBasicRemote() {
        checkConnectionState(State.CLOSED, State.CLOSING);
        return basicRemote;
    }

    @Override
    public boolean isOpen() {
        return (!(state.get() == State.CLOSED || state.get() == State.CLOSING));
    }

    @Override
    public void close() throws IOException {
        changeStateToClosing();
        basicRemote.close(new CloseReason(CloseReason.CloseCodes.NORMAL_CLOSURE, "no reason given"));
    }

    /**
     * Closes the underlying connection this session is based upon.
     */
    @Override
    public void close(CloseReason closeReason) throws IOException {
        checkConnectionState(State.CLOSED);
        changeStateToClosing();
        basicRemote.close(closeReason);
    }

    @Override
    public int getMaxBinaryMessageBufferSize() {
        return maxBinaryMessageBufferSize;
    }

    @Override
    public void setMaxBinaryMessageBufferSize(int maxBinaryMessageBufferSize) {
        checkConnectionState(State.CLOSED);
        this.maxBinaryMessageBufferSize = maxBinaryMessageBufferSize;
    }

    @Override
    public int getMaxTextMessageBufferSize() {
        return maxTextMessageBufferSize;
    }

    @Override
    public void setMaxTextMessageBufferSize(int maxTextMessageBufferSize) {
        checkConnectionState(State.CLOSED);
        this.maxTextMessageBufferSize = maxTextMessageBufferSize;
    }

    @Override
    public Set getOpenSessions() {
        checkConnectionState(State.CLOSED);
        return endpoint.getOpenSessions();
    }

    @Override
    public List getNegotiatedExtensions() {
        return negotiatedExtensions;
    }

    @Override
    public long getMaxIdleTimeout() {
        return maxIdleTimeout;
    }

    @Override
    public void setMaxIdleTimeout(long maxIdleTimeout) {
        checkConnectionState(State.CLOSED);
        this.maxIdleTimeout = maxIdleTimeout;
        restartIdleTimeoutExecutor();
    }

    @Override
    public boolean isSecure() {
        return isSecure;
    }

    @Override
    public WebSocketContainer getContainer() {
        return this.container;
    }

    @Override
    public void addMessageHandler(MessageHandler handler) {
        checkConnectionState(State.CLOSED);
        synchronized (handlerManager) {
            handlerManager.addMessageHandler(handler);
        }
    }

    @Override
    public Set getMessageHandlers() {
        synchronized (handlerManager) {
            return handlerManager.getMessageHandlers();
        }
    }

    @Override
    public void removeMessageHandler(MessageHandler handler) {
        checkConnectionState(State.CLOSED);
        synchronized (handlerManager) {
            handlerManager.removeMessageHandler(handler);
        }
    }

    @Override
    public URI getRequestURI() {
        return uri;
    }

    // TODO: this method should be deleted?
    @Override
    public Map> getRequestParameterMap() {
        return requestParameterMap;
    }

    @Override
    public Map getPathParameters() {
        return pathParameters;
    }

    @Override
    public Map getUserProperties() {
        return userProperties;
    }

    @Override
    public String getQueryString() {
        return queryString;
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public Principal getUserPrincipal() {
        return userPrincipal;
    }

    /**
     * Broadcasts text message to all connected clients.
     *
     * @param message message to be broadcasted.
     * @return map of sessions and futures for user to get the information about status of the message.
     */
    public Map> broadcast(String message) {
        return endpoint.broadcast(message);
    }

    /**
     * Broadcasts binary message to all connected clients.
     *
     * @param message message to be broadcasted.
     * @return map of sessions and futures for user to get the information about status of the message.
     */
    public Map> broadcast(ByteBuffer message) {
        return endpoint.broadcast(message);
    }


    void restartIdleTimeoutExecutor() {
        if (this.maxIdleTimeout < 1) {
            synchronized (idleTimeoutLock) {
                if (idleTimeoutFuture != null) {
                    idleTimeoutFuture.cancel(true);
                } else {
                    return;
                }
            }
        }

        synchronized (idleTimeoutLock) {
            if (idleTimeoutFuture != null) {
                idleTimeoutFuture.cancel(false);
            }

            idleTimeoutFuture = service.schedule(new IdleTimeoutCommand(), this.getMaxIdleTimeout(), TimeUnit.MILLISECONDS);
        }
    }

    private void checkConnectionState(State... states) {
        final State sessionState = state.get();
        for (State s : states) {
            if (sessionState == s) {
                throw new IllegalStateException(SESSION_CLOSED);
            }
        }
    }

    private void checkMessageSize(Object message, long maxMessageSize) {
        if (maxMessageSize != -1) {
            final long messageSize = (message instanceof String ? ((String) message).getBytes(Charset.defaultCharset()).length :
                    ((ByteBuffer) message).remaining());

            if (messageSize > maxMessageSize) {
                throw new MessageTooBigException(String.format("Message too long; allowed message size is %d bytes. (Current message length is %d bytes).", maxMessageSize, messageSize));
            }
        }
    }

    void notifyMessageHandlers(Object message, List> availableDecoders) throws DecodeException, IOException {
        boolean decoded = false;

        if (availableDecoders.isEmpty()) {
            LOGGER.severe("No decoder found");
        }

        for (CoderWrapper decoder : availableDecoders) {
            for (MessageHandler mh : getOrderedMessageHandlers()) {
                Class type;
                if ((mh instanceof MessageHandler.Whole)
                        && (type = MessageHandlerManager.getHandlerType(mh)).isAssignableFrom(decoder.getType())) {

                    if (mh instanceof BasicMessageHandler) {
                        checkMessageSize(message, ((BasicMessageHandler) mh).getMaxMessageSize());
                    }

                    Object object = endpoint.decodeCompleteMessage(this, message, type, decoder);
                    if (object != null) {
                        final State currentState = state.get();
                        if (currentState != State.CLOSING && currentState != State.CLOSED) {
                            //noinspection unchecked
                            ((MessageHandler.Whole) mh).onMessage(object);
                        }
                        decoded = true;
                        break;
                    }
                }
            }
            if (decoded) {
                break;
            }
        }
    }

     MessageHandler.Whole getMessageHandler(Class c) {
        for (MessageHandler mh : this.getOrderedMessageHandlers()) {
            if (MessageHandlerManager.getHandlerType(mh) == c) {
                return (MessageHandler.Whole) mh;
            }
        }

        return null;
    }

    void notifyMessageHandlers(Object message, boolean last) {
        boolean handled = false;

        for (MessageHandler handler : getMessageHandlers()) {
            if ((handler instanceof MessageHandler.Partial) &&
                    MessageHandlerManager.getHandlerType(handler).isAssignableFrom(message.getClass())) {

                if (handler instanceof AsyncMessageHandler) {
                    checkMessageSize(message, ((AsyncMessageHandler) handler).getMaxMessageSize());
                }

                final State currentState = state.get();
                if (currentState != State.CLOSING && currentState != State.CLOSED) {
                    //noinspection unchecked
                    ((MessageHandler.Partial) handler).onMessage(message, last);
                }
                handled = true;
                break;
            }
        }

        if (!handled) {
            if (message instanceof ByteBuffer) {
                notifyMessageHandlers(((ByteBuffer) message).array(), last);
            } else {
                LOGGER.severe("Unhandled text message in EndpointWrapper");
            }
        }
    }

    void notifyPongHandler(PongMessage pongMessage) {
        final Set messageHandlers = getMessageHandlers();
        for (MessageHandler handler : messageHandlers) {
            if (MessageHandlerManager.getHandlerType(handler).equals(PongMessage.class)) {
                ((MessageHandler.Whole) handler).onMessage(pongMessage);
            }
        }
    }

    boolean isWholeTextHandlerPresent() {
        return handlerManager.isWholeTextHandlerPresent();
    }

    boolean isWholeBinaryHandlerPresent() {
        return handlerManager.isWholeBinaryHandlerPresent();
    }

    boolean isPartialTextHandlerPresent() {
        return handlerManager.isPartialTextHandlerPresent();
    }

    boolean isPartialBinaryHandlerPresent() {
        return handlerManager.isPartialBinaryHandlerPresent();
    }

    boolean isReaderHandlerPresent() {
        return handlerManager.isReaderHandlerPresent();
    }

    boolean isInputStreamHandlerPresent() {
        return handlerManager.isInputStreamHandlerPresent();
    }

    boolean isPongHandlerPreset() {
        return handlerManager.isPongHandlerPresent();
    }

    private List getOrderedMessageHandlers() {
        Set handlers = this.getMessageHandlers();
        ArrayList result = new ArrayList();

        result.addAll(handlers);
        Collections.sort(result, new MessageHandlerComparator());

        return result;
    }

    State getState() {
        return state.get();
    }

    /**
     * Set the state of the {@link Session}.
     *
     * @param state the newly set state.
     */
    void setState(State state) {
        if (!state.equals(this.state.get())) {
            checkConnectionState(State.CLOSED);
            this.state.set(state);
        }
    }

    TextBuffer getTextBuffer() {
        return textBuffer;
    }

    BinaryBuffer getBinaryBuffer() {
        return binaryBuffer;
    }

    ReaderBuffer getReaderBuffer() {
        return readerBuffer;
    }

    void setReaderBuffer(ReaderBuffer readerBuffer) {
        this.readerBuffer = readerBuffer;
    }

    InputStreamBuffer getInputStreamBuffer() {
        return inputStreamBuffer;
    }

    void setInputStreamBuffer(InputStreamBuffer inputStreamBuffer) {
        this.inputStreamBuffer = inputStreamBuffer;
    }

    @Override
    public String toString() {
        final StringBuilder sb = new StringBuilder();
        sb.append("SessionImpl");
        sb.append("{uri=").append(uri);
        sb.append(", id='").append(id).append('\'');
        sb.append(", endpoint=").append(endpoint);
        sb.append('}');
        return sb.toString();
    }

    private void changeStateToClosing() {
        state.compareAndSet(State.RUNNING, State.CLOSING);
        state.compareAndSet(State.RECEIVING_BINARY, State.CLOSING);
        state.compareAndSet(State.RECEIVING_TEXT, State.CLOSING);
    }

    /**
     * Session state.
     */
    enum State {

        /**
         * {@link Session} is running and is not receiving partial messages on registered {@link MessageHandler.Whole}.
         */
        RUNNING,

        /**
         * {@link Session} is currently receiving text partial message on registered {@link MessageHandler.Whole}.
         */
        RECEIVING_TEXT,

        /**
         * {@link Session} is currently receiving binary partial message on registered {@link MessageHandler.Whole}.
         */
        RECEIVING_BINARY,

        /**
         * {@link Session} is being closed.
         */
        CLOSING,

        /**
         * {@link Session} has been already closed.
         */
        CLOSED
    }

    private static class MessageHandlerComparator implements Comparator, Serializable {

        @Override
        public int compare(MessageHandler o1, MessageHandler o2) {
            if (o1 instanceof MessageHandler.Whole) {
                if (o2 instanceof MessageHandler.Whole) {
                    Class type1 = MessageHandlerManager.getHandlerType(o1);
                    Class type2 = MessageHandlerManager.getHandlerType(o2);

                    if (type1.isAssignableFrom(type2)) {
                        return 1;
                    } else if (type2.isAssignableFrom(type1)) {
                        return -1;
                    } else {
                        return 0;
                    }
                } else {
                    return 1;
                }
            } else if (o2 instanceof MessageHandler.Whole) {
                return 1;
            }
            return 0;
        }
    }

    private class IdleTimeoutCommand implements Runnable {

        @Override
        public void run() {
            TyrusSession session = TyrusSession.this;

            // condition is required because scheduled task can be (for some reason) run even when it is cancelled.
            if (session.getMaxIdleTimeout() > 0 && session.isOpen()) {
                try {
                    session.close(new CloseReason(CloseReason.CloseCodes.CLOSED_ABNORMALLY, "Session closed by the container because of the idle timeout."));
                } catch (IOException e) {
                    LOGGER.log(Level.FINE, "Session could not been closed. " + e.getMessage());
                }
            }
        }
    }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy