com.dbay.apns4j.impl.ApnsConnectionImpl.java Source code

Java tutorial

Introduction

Here is the source code for com.dbay.apns4j.impl.ApnsConnectionImpl.java

Source

/*
 * Copyright 2013 DiscoveryBay Inc.
 * 
 * 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.dbay.apns4j.impl;

import static com.dbay.apns4j.model.ApnsConstants.CHARSET_ENCODING;
import static com.dbay.apns4j.model.ApnsConstants.ERROR_RESPONSE_BYTES_LENGTH;
import static com.dbay.apns4j.model.ApnsConstants.PAY_LOAD_MAX_LENGTH;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.UnsupportedEncodingException;
import java.net.Socket;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.atomic.AtomicInteger;

import javax.net.SocketFactory;

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

import com.dbay.apns4j.ErrorProcessHandler;
import com.dbay.apns4j.IApnsConnection;
import com.dbay.apns4j.model.Command;
import com.dbay.apns4j.model.ErrorResponse;
import com.dbay.apns4j.model.Payload;
import com.dbay.apns4j.model.PushNotification;
import com.dbay.apns4j.tools.ApnsTools;

/**
 * @author RamosLi
 * 
 */
public class ApnsConnectionImpl implements IApnsConnection {

    private static AtomicInteger IDENTIFIER = new AtomicInteger(100);

    private final static Log logger = LogFactory.getLog(ApnsConnectionImpl.class);

    // never expire
    private int EXPIRE = Integer.MAX_VALUE;

    private SocketFactory factory;
    /**
     * EN: There are two threads using the socket, one for writing, one for
     * reading. CN: socket
     */
    private Socket socket;
    /**
     * When a notification is sent, cache it into this queue. It may be resent.
     */
    private Queue<PushNotification> notificationCachedQueue = new LinkedList<PushNotification>();

    /**
     * Whether find error in the last connection
     */
    private boolean errorHappendedLastConn = false;

    /**
     * Whether first write data in the connection
     */
    private boolean isFirstWrite = false;

    private int maxRetries;
    private int maxCacheLength;

    private int readTimeOut;

    private String host;
    private int port;

    /**
     * You can find the properly ApnsService to resend notifications by this
     * name
     */
    private String name;

    /**
     * connection name
     */
    private String connName;
    private int intervalTime;
    private long lastSuccessfulTime = 0;
    private AtomicInteger notificaionSentCount = new AtomicInteger(0);
    private Object lock = new Object();
    private String serviceName = "";
    private ErrorProcessHandler errorProcessHandler;

    public ApnsConnectionImpl(SocketFactory factory, String host, int port, int maxRetries, int maxCacheLength,
            String name, String connName, int intervalTime, int timeout, String serviceName,
            ErrorProcessHandler errorProcessHandler) {
        this.factory = factory;
        this.host = host;
        this.port = port;
        this.maxRetries = maxRetries;
        this.maxCacheLength = maxCacheLength;
        this.name = name;
        this.connName = connName;
        this.intervalTime = intervalTime;
        this.readTimeOut = timeout;
        this.serviceName = serviceName;
        this.errorProcessHandler = errorProcessHandler;
    }

    @Override
    public void sendNotification(String token, Payload payload) {
        PushNotification notification = new PushNotification();
        notification.setId(IDENTIFIER.incrementAndGet());
        notification.setExpire(EXPIRE);
        notification.setToken(token);
        notification.setPayload(payload);
        sendNotification(notification);
    }

    @Override
    public void sendNotification(PushNotification notification) {
        byte[] plBytes = null;
        String payload = notification.getPayload().toString();
        try {
            plBytes = payload.getBytes(CHARSET_ENCODING);
            if (plBytes.length > PAY_LOAD_MAX_LENGTH) {
                logger.error("Payload execeed limit, the maximum size allowed is 256 bytes. " + payload);
                return;
            }
        } catch (UnsupportedEncodingException e) {
            logger.error(e.getMessage(), e);
            return;
        }

        /**
         * EN: If error happened before, just wait until the resending work
         * finishes by another thread and close the current socket CN:
         * ??error-response?????????
         */
        synchronized (lock) {
            if (errorHappendedLastConn) {
                closeSocket(socket);
                socket = null;
            }
            byte[] data = notification.generateData(plBytes);
            boolean isSuccessful = false;
            int retries = 0;
            while (retries < maxRetries) {
                try {
                    boolean exceedIntervalTime = lastSuccessfulTime > 0
                            && (System.currentTimeMillis() - lastSuccessfulTime) > intervalTime;
                    if (exceedIntervalTime) {
                        closeSocket(socket);
                        socket = null;
                    }

                    if (socket == null || socket.isClosed()) {
                        socket = createNewSocket();
                    }

                    OutputStream socketOs = socket.getOutputStream();
                    socketOs.write(data);
                    socketOs.flush();
                    isSuccessful = true;
                    break;
                } catch (Exception e) {
                    logger.error(serviceName + "," + connName + " " + e.getMessage(), e);
                    closeSocket(socket);
                    socket = null;
                }
                retries++;
            }
            if (!isSuccessful) {
                logger.error(
                        String.format("%s, %s Notification send failed. %s", serviceName, connName, notification));
                return;
            } else {
                logger.info(String.format("%s, %s Send success. count: %s, notificaion: %s", serviceName, connName,
                        notificaionSentCount.incrementAndGet(), notification));

                notificationCachedQueue.add(notification);
                lastSuccessfulTime = System.currentTimeMillis();

                /**
                 * TODO there is a bug, maybe, theoretically. CN:
                 * ?????? maxCacheLength ?APNS?
                 * ??error-response??
                 * ??100?????APNS??
                 */
                if (notificationCachedQueue.size() > maxCacheLength) {
                    notificationCachedQueue.poll();
                }
            }
        }

        if (isFirstWrite) {
            isFirstWrite = false;
            /**
             * EN: When we create a socket, just a TCP/IP connection created.
             * After we wrote data to the stream, the SSL connection had been
             * created. Now, it's time to read data from InputStream CN:
             * createSocket?TCPSSL???????
             * ?InputStream
             */
            startErrorWorker();
        }
    }

    private Socket createNewSocket() throws IOException, UnknownHostException {
        if (logger.isDebugEnabled()) {
            logger.debug(connName + " create a new socket.");
        }
        isFirstWrite = true;
        errorHappendedLastConn = false;
        Socket socket = factory.createSocket(host, port);
        socket.setSoTimeout(readTimeOut);
        // enable tcp_nodelay, any data will be sent immediately.
        socket.setTcpNoDelay(true);

        return socket;
    }

    private void closeSocket(Socket socket) {
        try {
            if (socket != null) {
                socket.close();
            }
        } catch (Exception e) {
            logger.error(e.getMessage(), e);
        }
    }

    private boolean isSocketAlive(Socket socket) {
        if (socket != null && socket.isConnected()) {
            return true;
        }
        return false;
    }

    @Override
    public void close() throws IOException {
        closeSocket(socket);
    }

    private void startErrorWorker() {
        Thread thread = new Thread(new Runnable() {

            @Override
            public void run() {
                Socket curSocket = socket;
                try {
                    if (!isSocketAlive(curSocket)) {
                        return;
                    }
                    InputStream socketIs = curSocket.getInputStream();
                    byte[] res = new byte[ERROR_RESPONSE_BYTES_LENGTH];
                    int size = 0;

                    while (true) {
                        try {
                            size = socketIs.read(res);
                            if (size > 0 || size == -1) {
                                // break, when something was read or there is no
                                // data any more
                                break;
                            }
                        } catch (SocketTimeoutException e) {
                            // There is no data. Keep reading.
                            Thread.sleep(10);
                        }
                    }

                    int command = res[0];
                    /**
                     * EN: error-response,close the socket and resent
                     * notifications CN: ???????
                     */
                    if (size == res.length && command == Command.ERROR) {
                        int status = res[1];
                        int errorId = ApnsTools.parse4ByteInt(res[2], res[3], res[4], res[5]);

                        String token = ErrorResponse.desc(status);

                        // callback error token?
                        if (null != errorProcessHandler) {
                            errorProcessHandler.process(errorId, status, token);
                        }

                        if (logger.isInfoEnabled()) {
                            logger.info(String.format(
                                    "%s, %s Received error response. status: %s, id: %s, error-desc: %s",
                                    serviceName, connName, status, errorId, token));
                        }

                        Queue<PushNotification> resentQueue = new LinkedList<PushNotification>();

                        synchronized (lock) {
                            boolean found = false;
                            errorHappendedLastConn = true;
                            while (!notificationCachedQueue.isEmpty()) {
                                PushNotification pn = notificationCachedQueue.poll();
                                if (pn.getId() == errorId) {
                                    found = true;
                                } else {
                                    /**
                                     * https://developer.apple.com/library/ios/
                                     * documentation
                                     * /NetworkingInternet/Conceptual
                                     * /RemoteNotificationsPG
                                     * /Chapters/CommunicatingWIthAPS.html As
                                     * the document said, add the notifications
                                     * which need be resent to the queue. Igonre
                                     * the error one
                                     */
                                    if (found) {
                                        resentQueue.add(pn);
                                    }
                                }
                            }
                            if (!found) {
                                logger.warn(connName
                                        + " Didn't find error-notification in the queue. Maybe it's time to adjust cache length. id: "
                                        + errorId);
                            }
                        }
                        // resend notifications
                        if (!resentQueue.isEmpty()) {
                            ApnsResender.getInstance().resend(name, resentQueue);
                        }
                    } else {
                        // ignore and continue reading
                        logger.error(
                                connName + " Unexpected command or size. commend: " + command + " , size: " + size);
                    }
                } catch (Exception e) {
                    // logger.error(connName + " " + e.getMessage(), e);
                    logger.error(connName + " " + e.getMessage());
                } finally {
                    /**
                     * EN: close the old socket although it may be closed once
                     * before. CN: ???
                     */
                    closeSocket(curSocket);
                }
            }
        });

        thread.start();
    }
}