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

Java tutorial

Introduction

Here is the source code for com.relayrides.pushy.apns.FeedbackServiceClient.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.Bootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.channel.Channel;
import io.netty.channel.ChannelFuture;
import io.netty.channel.ChannelHandlerContext;
import io.netty.channel.ChannelInitializer;
import io.netty.channel.ChannelPipeline;
import io.netty.channel.SimpleChannelInboundHandler;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.ReplayingDecoder;
import io.netty.handler.ssl.SslHandler;
import io.netty.handler.timeout.ReadTimeoutException;
import io.netty.handler.timeout.ReadTimeoutHandler;
import io.netty.util.concurrent.Future;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Vector;
import java.util.concurrent.TimeUnit;

import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLEngine;

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

/**
 * <p>A client that communicates with the APNs feedback to retrieve expired device tokens. According to Apple's
 * documentation:</p>
 *
 * <blockquote><p>The Apple Push Notification Service includes a feedback service to give you information about failed
 * push notifications. When a push notification cannot be delivered because the intended app does not exist on the
 * device, the feedback service adds that device's token to its list. Push notifications that expire before being
 * delivered are not considered a failed delivery and don't impact the feedback service...</p>
 *
 * <p>Query the feedback service daily to get the list of device tokens. Use the timestamp to verify that the device
 * tokens haven't been reregistered since the feedback entry was generated. For each device that has not been
 * reregistered, stop sending notifications.</p></blockquote>
 *
 * <p>Generally, users of Pushy should <em>not</em> instantiate a {@code FeedbackServiceClient} directly, but should
 * instead call {@link com.relayrides.pushy.apns.PushManager#getExpiredTokens()}, which will manage the creation
 * and configuration of a {@code FeedbackServiceClient} internally.</p>
 *
 * @author <a href="mailto:jon@relayrides.com">Jon Chambers</a>
 *
 * @see <a href="http://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/CommunicatingWIthAPS.html#//apple_ref/doc/uid/TP40008194-CH101-SW3">
 * Local and Push Notification Programming Guide - Provider Communication with Apple Push Notification Service - The
 * Feedback Service</a>
 */
class FeedbackServiceClient {

    private final ApnsEnvironment environment;
    private final SSLContext sslContext;
    private final NioEventLoopGroup eventLoopGroup;

    private final Vector<ExpiredToken> expiredTokens;

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

    private enum ExpiredTokenDecoderState {
        EXPIRATION, TOKEN_LENGTH, TOKEN
    }

    private class ExpiredTokenDecoder extends ReplayingDecoder<ExpiredTokenDecoderState> {

        private Date expiration;
        private byte[] token;

        public ExpiredTokenDecoder() {
            super(ExpiredTokenDecoderState.EXPIRATION);
        }

        @Override
        protected void decode(final ChannelHandlerContext context, final ByteBuf in, final List<Object> out) {
            switch (this.state()) {
            case EXPIRATION: {
                final long timestamp = (in.readInt() & 0xFFFFFFFFL) * 1000L;
                this.expiration = new Date(timestamp);

                this.checkpoint(ExpiredTokenDecoderState.TOKEN_LENGTH);

                break;
            }

            case TOKEN_LENGTH: {
                this.token = new byte[in.readShort() & 0x0000FFFF];
                this.checkpoint(ExpiredTokenDecoderState.TOKEN);

                break;
            }

            case TOKEN: {
                in.readBytes(this.token);
                out.add(new ExpiredToken(this.token, this.expiration));

                this.checkpoint(ExpiredTokenDecoderState.EXPIRATION);

                break;
            }
            }
        }
    }

    private class FeedbackClientHandler extends SimpleChannelInboundHandler<ExpiredToken> {

        private final FeedbackServiceClient feedbackClient;

        public FeedbackClientHandler(final FeedbackServiceClient feedbackClient) {
            this.feedbackClient = feedbackClient;
        }

        @Override
        protected void channelRead0(final ChannelHandlerContext context, final ExpiredToken expiredToken) {
            this.feedbackClient.expiredTokens.add(expiredToken);
        }

        @Override
        public void exceptionCaught(final ChannelHandlerContext context, final Throwable cause) {

            if (!(cause instanceof ReadTimeoutException)) {
                log.debug("Caught an unexpected exception while waiting for feedback.", cause);
            }

            context.close();
        }
    }

    /**
     * <p>Constructs a new feedback client that connects to the feedback service in the given environment with the
     * credentials and key/trust managers in the given SSL context.</p>
        
     * @param environment the environment in which this feedback client will operate
     * @param sslContext an SSL context with the keys/certificates and trust managers this client should use when
     * communicating with the APNs feedback service
     * @param eventLoopGroup the event loop group this client should use for asynchronous network operations
     */
    public FeedbackServiceClient(final ApnsEnvironment environment, final SSLContext sslContext,
            final NioEventLoopGroup eventLoopGroup) {
        this.environment = environment;
        this.sslContext = sslContext;
        this.eventLoopGroup = eventLoopGroup;

        this.expiredTokens = new Vector<ExpiredToken>();
    }

    /**
     * <p>Retrieves a list of expired tokens from the APNs feedback service. Be warned that this is a
     * <strong>destructive operation</strong>. According to Apple's documentation:</p>
     *
     * <blockquote>The feedback service's list is cleared after you read it. Each time you connect to the feedback
     * service, the information it returns lists only the failures that have happened since you last
     * connected.</blockquote>
     *
     * @param timeout the time after the last received data after which the connection to the feedback service should
     * be closed
     * @param timeoutUnit the unit of time in which the given {@code timeout} is measured
     *
     * @return a list of tokens that have expired since the last connection to the feedback service
     *
     * @throws InterruptedException if interrupted while waiting for a response from the feedback service
     * @throws FeedbackConnectionException if the connection to the feedback service failed for any reason
     */
    public synchronized List<ExpiredToken> getExpiredTokens(final long timeout, final TimeUnit timeoutUnit)
            throws InterruptedException, FeedbackConnectionException {

        this.expiredTokens.clear();

        final Bootstrap bootstrap = new Bootstrap();
        bootstrap.group(this.eventLoopGroup);
        bootstrap.channel(NioSocketChannel.class);

        final FeedbackServiceClient feedbackClient = this;
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {

            @Override
            protected void initChannel(final SocketChannel channel) throws Exception {
                final ChannelPipeline pipeline = channel.pipeline();

                final SSLEngine sslEngine = feedbackClient.sslContext.createSSLEngine();
                sslEngine.setUseClientMode(true);

                pipeline.addLast("ssl", new SslHandler(sslEngine));
                pipeline.addLast("readTimeoutHandler", new ReadTimeoutHandler(timeout, timeoutUnit));
                pipeline.addLast("decoder", new ExpiredTokenDecoder());
                pipeline.addLast("handler", new FeedbackClientHandler(feedbackClient));
            }

        });

        final ChannelFuture connectFuture = bootstrap
                .connect(this.environment.getFeedbackHost(), this.environment.getFeedbackPort()).await();

        if (connectFuture.isSuccess()) {
            log.debug("Connected to feedback service.");

            final SslHandler sslHandler = connectFuture.channel().pipeline().get(SslHandler.class);

            if (sslHandler != null) {
                final Future<Channel> handshakeFuture = sslHandler.handshakeFuture().await();

                if (handshakeFuture.isSuccess()) {
                    log.debug("Completed TLS handshake with feedback service.");

                    // The feedback service will send us a list of device tokens as soon as we complete the SSL
                    // handshake, then hang up. While we're waiting to sync with the connection closure, we'll be
                    // receiving messages from the feedback service from another thread.
                    connectFuture.channel().closeFuture().await();
                } else {
                    log.debug("Failed to complete TLS handshake with feedback service.", handshakeFuture.cause());

                    connectFuture.channel().close().await();
                    throw new FeedbackConnectionException(handshakeFuture.cause());
                }
            } else {
                log.warn("Feedback client failed to get SSL handler and could not wait for TLS handshake.");

                connectFuture.channel().close().await();
                throw new FeedbackConnectionException(null);
            }
        } else {
            log.debug("Failed to connect to feedback service.", connectFuture.cause());
            throw new FeedbackConnectionException(connectFuture.cause());
        }

        return new ArrayList<ExpiredToken>(this.expiredTokens);
    }
}