com.relayrides.pushy.apns.MockApnsServer.java Source code

Java tutorial

Introduction

Here is the source code for com.relayrides.pushy.apns.MockApnsServer.java

Source

/* Copyright (c) 2013 RelayRides
 *
 * 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.relayrides.pushy.apns;

import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFutureListener;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelOption;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.handler.codec.DecoderException;
import io.netty.handler.codec.MessageToByteEncoder;
import io.netty.handler.codec.ReplayingDecoder;
import io.netty.handler.ssl.SslHandler;

import java.nio.charset.Charset;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.concurrent.CountDownLatch;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import com.relayrides.pushy.apns.util.SimpleApnsPushNotification;

public class MockApnsServer {

    private final int port;
    private final NioEventLoopGroup eventLoopGroup;

    private final ArrayList<CountDownLatch> countdownLatches = new ArrayList<CountDownLatch>();

    private Channel channel;

    public static final int EXPECTED_TOKEN_SIZE = 32;
    public static final int MAX_PAYLOAD_SIZE = 256;

    private static final Logger log = LoggerFactory.getLogger(MockApnsServer.class);

    private class ApnsDecoderException extends Exception {
        private static final long serialVersionUID = 1L;

        final int sequenceNumber;
        final RejectedNotificationReason reason;

        public ApnsDecoderException(final int sequenceNumber, final RejectedNotificationReason reason) {
            this.sequenceNumber = sequenceNumber;
            this.reason = reason;
        }
    };

    private enum ApnsPushNotificationDecoderState {
        OPCODE, SEQUENCE_NUMBER, EXPIRATION, TOKEN_LENGTH, TOKEN, PAYLOAD_LENGTH, PAYLOAD
    }

    private class ApnsPushNotificationDecoder extends ReplayingDecoder<ApnsPushNotificationDecoderState> {

        private int sequenceNumber;
        private Date expiration;
        private byte[] token;
        private byte[] payloadBytes;

        private static final byte EXPECTED_OPCODE = 1;

        public ApnsPushNotificationDecoder() {
            super(ApnsPushNotificationDecoderState.OPCODE);
        }

        @Override
        protected void decode(final ChannelHandlerContext context, final ByteBuf in, final List<Object> out)
                throws ApnsDecoderException {
            switch (this.state()) {
            case OPCODE: {
                final byte opcode = in.readByte();

                if (opcode != EXPECTED_OPCODE) {
                    throw new ApnsDecoderException(0, RejectedNotificationReason.UNKNOWN);
                }

                this.checkpoint(ApnsPushNotificationDecoderState.SEQUENCE_NUMBER);

                break;
            }

            case SEQUENCE_NUMBER: {
                this.sequenceNumber = in.readInt();
                this.checkpoint(ApnsPushNotificationDecoderState.EXPIRATION);

                break;
            }

            case EXPIRATION: {
                final long timestamp = (in.readInt() & 0xFFFFFFFFL) * 1000L;
                this.expiration = new Date(timestamp);

                this.checkpoint(ApnsPushNotificationDecoderState.TOKEN_LENGTH);

                break;
            }

            case TOKEN_LENGTH: {

                this.token = new byte[in.readShort() & 0x0000FFFF];

                if (this.token.length == 0) {
                    throw new ApnsDecoderException(this.sequenceNumber, RejectedNotificationReason.MISSING_TOKEN);
                } else if (this.token.length != EXPECTED_TOKEN_SIZE) {
                    throw new ApnsDecoderException(this.sequenceNumber,
                            RejectedNotificationReason.INVALID_TOKEN_SIZE);
                }

                this.checkpoint(ApnsPushNotificationDecoderState.TOKEN);

                break;
            }

            case TOKEN: {
                in.readBytes(this.token);
                this.checkpoint(ApnsPushNotificationDecoderState.PAYLOAD_LENGTH);

                break;
            }

            case PAYLOAD_LENGTH: {
                final int payloadSize = in.readShort() & 0x0000FFFF;

                if (payloadSize == 0) {
                    throw new ApnsDecoderException(this.sequenceNumber, RejectedNotificationReason.MISSING_PAYLOAD);
                } else if (payloadSize > MAX_PAYLOAD_SIZE) {
                    throw new ApnsDecoderException(this.sequenceNumber,
                            RejectedNotificationReason.INVALID_PAYLOAD_SIZE);
                }

                this.payloadBytes = new byte[payloadSize];
                this.checkpoint(ApnsPushNotificationDecoderState.PAYLOAD);

                break;
            }

            case PAYLOAD: {
                in.readBytes(this.payloadBytes);

                final String payloadString = new String(this.payloadBytes, Charset.forName("UTF-8"));

                out.add(new SendableApnsPushNotification<SimpleApnsPushNotification>(
                        new SimpleApnsPushNotification(this.token, payloadString, this.expiration),
                        this.sequenceNumber));

                this.checkpoint(ApnsPushNotificationDecoderState.OPCODE);

                break;
            }
            }
        }
    }

    private class ApnsErrorEncoder extends MessageToByteEncoder<RejectedNotification> {

        private static final byte ERROR_COMMAND = 8;

        @Override
        protected void encode(final ChannelHandlerContext context, final RejectedNotification rejectedNotification,
                final ByteBuf out) {
            out.writeByte(ERROR_COMMAND);
            out.writeByte(rejectedNotification.getReason().getErrorCode());
            out.writeInt(rejectedNotification.getSequenceNumber());
        }
    }

    private class MockApnsServerHandler
            extends SimpleChannelInboundHandler<SendableApnsPushNotification<SimpleApnsPushNotification>> {

        private final MockApnsServer server;

        private boolean rejectFutureNotifications = false;

        public MockApnsServerHandler(final MockApnsServer server) {
            this.server = server;
        }

        @Override
        protected void channelRead0(final ChannelHandlerContext context,
                final SendableApnsPushNotification<SimpleApnsPushNotification> receivedNotification) {
            if (!this.rejectFutureNotifications) {
                this.server.acceptNotification(receivedNotification);
            }
        }

        @Override
        public void exceptionCaught(final ChannelHandlerContext context, final Throwable cause) {
            this.rejectFutureNotifications = true;

            if (cause instanceof DecoderException) {
                final DecoderException decoderException = (DecoderException) cause;

                if (decoderException.getCause() instanceof ApnsDecoderException) {
                    final ApnsDecoderException apnsDecoderException = (ApnsDecoderException) decoderException
                            .getCause();
                    final RejectedNotification rejectedNotification = new RejectedNotification(
                            apnsDecoderException.sequenceNumber, apnsDecoderException.reason);

                    context.writeAndFlush(rejectedNotification).addListener(ChannelFutureListener.CLOSE);
                }
            } else {
                log.warn("Caught an unexpected exception; closing connection.", cause);
                context.close();
            }
        }
    }

    public MockApnsServer(final int port, final NioEventLoopGroup eventLoopGroup) {
        this.port = port;
        this.eventLoopGroup = eventLoopGroup;

    }

    public synchronized void start() throws InterruptedException {
        final ServerBootstrap bootstrap = new ServerBootstrap();

        bootstrap.group(this.eventLoopGroup);
        bootstrap.channel(NioServerSocketChannel.class);
        bootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

        final MockApnsServer server = this;

        bootstrap.childHandler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(final SocketChannel channel) throws Exception {
                channel.pipeline().addLast("ssl", new SslHandler(SSLTestUtil.createSSLEngineForMockServer()));
                channel.pipeline().addLast("encoder", new ApnsErrorEncoder());
                channel.pipeline().addLast("decoder", new ApnsPushNotificationDecoder());
                channel.pipeline().addLast("handler", new MockApnsServerHandler(server));
            }
        });

        this.channel = bootstrap.bind(this.port).await().channel();
    }

    public synchronized void shutdown() throws InterruptedException {
        if (this.channel != null) {
            this.channel.close().await();
        }

        this.channel = null;
    }

    protected void acceptNotification(
            final SendableApnsPushNotification<SimpleApnsPushNotification> receivedNotification) {
        synchronized (this.countdownLatches) {
            for (final CountDownLatch latch : this.countdownLatches) {
                latch.countDown();
            }
        }
    }

    public CountDownLatch getAcceptedNotificationCountDownLatch(final int acceptedNotificationCount) {
        synchronized (this.countdownLatches) {
            final CountDownLatch latch = new CountDownLatch(acceptedNotificationCount);
            this.countdownLatches.add(latch);

            return latch;
        }
    }
}