com.github.spapageo.jannel.client.ClientSession.java Source code

Java tutorial

Introduction

Here is the source code for com.github.spapageo.jannel.client.ClientSession.java

Source

/*
 * The MIT License (MIT)
 * Copyright (c) 2016 Spyros Papageorgiou
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to
 * deal in the Software without restriction, including without limitation the
 * rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
 * sell copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package com.github.spapageo.jannel.client;

import com.github.spapageo.jannel.exception.BadMessageException;
import com.github.spapageo.jannel.msg.*;
import com.github.spapageo.jannel.windowing.Window;
import com.github.spapageo.jannel.windowing.WindowFuture;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.util.Timer;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.annotation.Nonnull;
import javax.annotation.Nullable;
import java.net.SocketAddress;
import java.nio.channels.ClosedChannelException;
import java.util.UUID;
import java.util.concurrent.ExecutionException;

/**
 * A client session to a remote bearer-box which can be used to send messages using it member
 * functions and receive message using a registered {@link SessionCallbackHandler}.
 */
public class ClientSession implements SessionCallbackHandler {

    /**
     * The possible Session states
     */
    private enum State {
        /**
         * Connection open
         */
        OPEN,

        /**
         * Connection open and identified to the remote bearer-box
         */
        IDENTIFIED,

        /**
         * Connection closed
         */
        CLOSED
    }

    private static final Logger LOGGER = LoggerFactory.getLogger(ClientSession.class);

    private volatile State state;

    private final ClientSessionConfiguration configuration;

    private final Channel channel;

    private SessionHandler sessionHandler;

    private final Window<UUID, Sms, Ack> sendWindow;

    /**
     * Construct a new client session to a remote bearer-box
     * @param configuration the client configuration to use for this session
     * @param channel the connected channel to the remote bearer-box
     * @param sessionHandler the session that will be called every time a specific event occurs
     * @param timer the timer used by the window for expiring requests
     */
    public ClientSession(ClientSessionConfiguration configuration, Channel channel, Timer timer,
            @Nullable SessionHandler sessionHandler) {
        this.configuration = configuration;
        this.channel = channel;
        this.sessionHandler = sessionHandler == null ? new DefaultSessionHandler() : sessionHandler;
        this.state = State.OPEN;
        this.sendWindow = new Window<UUID, Sms, Ack>(configuration.getWindowSize(), timer);
    }

    /**
     * Informs the session that an message was received
     * @param msg the message
     * @throws InterruptedException when the window operation is interrupted
     */
    @Override
    public void fireInboundMessage(Message msg) throws InterruptedException {
        MessageType messageType = msg.getType();

        switch (messageType) {
        case HEARTBEAT:
            this.sessionHandler.fireHeartBeatReceived((HeartBeat) msg);
            break;
        case ADMIN:
            this.sessionHandler.fireAdminCommandReceived((Admin) msg);
            break;
        case SMS:
            this.sessionHandler.fireSmsReceived((Sms) msg);
            break;
        case ACK:
            Ack ack = (Ack) msg;
            handleSmsAckResponse(ack, ack.getId());
            break;
        case DATAGRAM:
            LOGGER.warn("Unsupported datagram message received");
            break;
        default:
            LOGGER.warn("Unsupported message received");
            break;
        }
    }

    /**
     * Informs the session that an exception was caught
     * @param throwable the exception
     */
    @Override
    public void fireExceptionCaught(Throwable throwable) {
        if (throwable instanceof BadMessageException) {
            this.sessionHandler.fireBadMessageException((BadMessageException) throwable);
        } else {
            if (isClosed()) {
                LOGGER.debug("Unbind/close was requested, ignoring exception thrown: {}", throwable);
            } else {
                sessionHandler.fireUnknownThrowable(throwable);
            }
        }
    }

    /**
     * Informs the session that the connection was closed
     */
    @Override
    public void fireConnectionClosed() {
        Throwable cause = new ClosedChannelException();

        this.sendWindow.failAll(cause);

        if (isClosed()) {
            LOGGER.debug("Unbind/close was requested, ignoring channelClosed event");
        } else {
            sessionHandler.fireChannelUnexpectedlyClosed();
        }
    }

    /**
     * Synchronously identify to the remote bearer-box
     * @param identifyCommand the identify command
     */
    public void identify(Admin identifyCommand) {
        if (identifyCommand.getAdminCommand() != AdminCommand.IDENTIFY)
            throw new IllegalStateException("The command must be an IDENTIFY");

        try {
            sendMessage(identifyCommand).syncUninterruptibly();
            state = State.IDENTIFIED;
            sessionHandler.fireSessionInitialized(this);
        } catch (Exception throwable) {
            LOGGER.error("Exception thrown while trying to identify to the bearer-box.", throwable);
            close();
            Throwables.propagate(throwable);
        }
    }

    /**
     * @return true if the session is identified to the remote bearer-box
     */
    public boolean isIdentified() {
        return State.IDENTIFIED.equals(state);
    }

    /**
     * Closed the session
     */
    public void close() {
        close(5000);
    }

    /**
     * Closes the channel with the specified timeout
     * @param timeoutInMillis the timeout in milliseconds
     */
    public void close(long timeoutInMillis) {
        if (channel.isActive()) {
            channel.close().awaitUninterruptibly(timeoutInMillis);
        }
        this.state = State.CLOSED;
    }

    /**
     * @return whether the session is closed
     */
    public boolean isClosed() {
        return State.CLOSED.equals(state);
    }

    /**
     * Close and destroy this session
     */
    public void destroy() {
        close();
        sendWindow.destroy();
        sessionHandler = null;
    }

    /**
     * Synchronously sends an sms and waits for its response
     * @param sms             the sms to send
     * @param timeoutInMillis the wait timeout until the request if completed
     * @return the response the response
     * @throws InterruptedException   when the operation was interrupted
     * @throws ExecutionException     when the operation failed
     */
    @Nonnull
    public Ack sendSmsAndWait(Sms sms, long timeoutInMillis) throws InterruptedException, ExecutionException {
        return sendSms(sms, timeoutInMillis).get();
    }

    /**
     * Asynchronously sends an sms
     * @param sms           the sms to send
     * @param timeoutMillis the timeout for an open window slot to appear
     * @return the future on the operation
     * @throws InterruptedException   when the operation was interrupted
     */
    @SuppressWarnings("unchecked")
    @Nonnull
    public WindowFuture<Sms, Ack> sendSms(final Sms sms, final long timeoutMillis) throws InterruptedException {

        // Generate UUID if null
        if (sms.getId() == null) {
            sms.setId(UUID.randomUUID());
        }

        // Apply the current client id if null
        if (sms.getBoxId() == null)
            sms.setBoxId(configuration.getClientId());

        WindowFuture future = sendWindow.offer(sms.getId(), sms, timeoutMillis,
                configuration.getRequestExpiryTimeout());

        sendMessage(sms).addListener(new GenericFutureListener<Future<? super Void>>() {
            @Override
            public void operationComplete(Future<? super Void> channelFuture) throws Exception {
                if (!channelFuture.isSuccess() && !channelFuture.isCancelled()) {
                    sendWindow.fail(sms.getId(), channelFuture.cause());
                } else if (channelFuture.isCancelled()) {
                    sendWindow.cancel(sms.getId(), true);
                }
            }
        });

        return future;
    }

    /**
     * Send an heartbeat message to the remote server
     * @param heartBeat the heartbeat message
     * @return the channel future of this operation
     */
    @Nonnull
    public Future sendHeartBeat(HeartBeat heartBeat) {
        return sendMessage(heartBeat);
    }

    /**
     * Send an ack message to the remote server
     * @param ack the ack message
     * @return the channel future of this operation
     */
    @Nonnull
    public Future sendAck(Ack ack) {
        return sendMessage(ack);
    }

    private void handleSmsAckResponse(Ack ack, UUID receivedMsgUUID) throws InterruptedException {
        final WindowFuture<Sms, Ack> future = this.sendWindow.complete(receivedMsgUUID, ack);

        if (future == null) {
            sessionHandler.fireUnexpectedAckReceived(ack);
            return;
        }

        LOGGER.trace("Found a future in the window for id [{}]", ack.getId());
    }

    private ChannelFuture sendMessage(Message message) {
        return this.channel.writeAndFlush(message);
    }

    /**
     * @return the local address
     */
    @Nonnull
    public Optional<SocketAddress> getLocalAddress() {
        return Optional.fromNullable(this.channel.localAddress());
    }

    /**
     * @return the remote address
     */
    @Nonnull
    public Optional<SocketAddress> getRemoteAddress() {
        return Optional.fromNullable(this.channel.remoteAddress());
    }

    /**
     * @return the session configuration
     */
    @Nonnull
    public ClientSessionConfiguration getConfiguration() {
        return this.configuration;
    }

    /**
     * @return the session channel
     */
    @Nonnull
    public Channel getChannel() {
        return this.channel;
    }

    /**
     * @return the windows of this session
     */
    @Nonnull
    public Window<UUID, Sms, Ack> getWindow() {
        return this.sendWindow;
    }

    /**
     * @return the maximum window size
     */
    @Nonnull
    public int getMaxWindowSize() {
        return sendWindow.getMaxSize();
    }

    /**
     * @return the current window size
     */
    @Nonnull
    public int getWindowSize() {
        return sendWindow.getSize();
    }

    /**
     * @return the handler for this session
     */
    @Nonnull
    public SessionHandler getSessionHandler() {
        return sessionHandler;
    }
}