Browse Source

commit push server

heavyrain.lee 6 years ago
parent
commit
cc513446b9
63 changed files with 5896 additions and 0 deletions
  1. 1 0
      .gitignore
  2. BIN
      apns/develop.p12
  3. BIN
      apns/product.p12
  4. BIN
      apns/voip.p12
  5. BIN
      apns/wfdemo_apns.p12
  6. BIN
      apns/wfdemo_voip.p12
  7. 225 0
      mvnw
  8. 143 0
      mvnw.cmd
  9. 147 0
      pom.xml
  10. 11 0
      src/main/java/cn/wildfirechat/push/PushApplication.java
  11. 28 0
      src/main/java/cn/wildfirechat/push/PushController.java
  12. 161 0
      src/main/java/cn/wildfirechat/push/PushMessage.java
  13. 7 0
      src/main/java/cn/wildfirechat/push/PushMessageType.java
  14. 7 0
      src/main/java/cn/wildfirechat/push/android/AndroidPushService.java
  15. 45 0
      src/main/java/cn/wildfirechat/push/android/AndroidPushServiceImpl.java
  16. 7 0
      src/main/java/cn/wildfirechat/push/android/AndroidPushType.java
  17. 29 0
      src/main/java/cn/wildfirechat/push/android/hms/HMSConfig.java
  18. 136 0
      src/main/java/cn/wildfirechat/push/android/hms/HMSPush.java
  19. 29 0
      src/main/java/cn/wildfirechat/push/android/meizu/MeiZuConfig.java
  20. 68 0
      src/main/java/cn/wildfirechat/push/android/meizu/MeiZuPush.java
  21. 20 0
      src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiConfig.java
  22. 71 0
      src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiPush.java
  23. 86 0
      src/main/java/cn/wildfirechat/push/ios/ApnsConfig.java
  24. 187 0
      src/main/java/cn/wildfirechat/push/ios/ApnsServer.java
  25. 7 0
      src/main/java/cn/wildfirechat/push/ios/IOSPushService.java
  26. 22 0
      src/main/java/cn/wildfirechat/push/ios/IOSPushServiceImpl.java
  27. 6 0
      src/main/java/cn/wildfirechat/push/ios/IOSPushType.java
  28. 57 0
      src/main/java/com/notnoop/apns/APNS.java
  29. 92 0
      src/main/java/com/notnoop/apns/ApnsDelegate.java
  30. 52 0
      src/main/java/com/notnoop/apns/ApnsDelegateAdapter.java
  31. 76 0
      src/main/java/com/notnoop/apns/ApnsNotification.java
  32. 140 0
      src/main/java/com/notnoop/apns/ApnsService.java
  33. 760 0
      src/main/java/com/notnoop/apns/ApnsServiceBuilder.java
  34. 81 0
      src/main/java/com/notnoop/apns/DeliveryError.java
  35. 191 0
      src/main/java/com/notnoop/apns/EnhancedApnsNotification.java
  36. 535 0
      src/main/java/com/notnoop/apns/PayloadBuilder.java
  37. 118 0
      src/main/java/com/notnoop/apns/ReconnectPolicy.java
  38. 172 0
      src/main/java/com/notnoop/apns/SimpleApnsNotification.java
  39. 47 0
      src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java
  40. 90 0
      src/main/java/com/notnoop/apns/internal/AbstractApnsService.java
  41. 52 0
      src/main/java/com/notnoop/apns/internal/ApnsConnection.java
  42. 412 0
      src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java
  43. 121 0
      src/main/java/com/notnoop/apns/internal/ApnsFeedbackConnection.java
  44. 121 0
      src/main/java/com/notnoop/apns/internal/ApnsPooledConnection.java
  45. 59 0
      src/main/java/com/notnoop/apns/internal/ApnsServiceImpl.java
  46. 143 0
      src/main/java/com/notnoop/apns/internal/BatchApnsService.java
  47. 126 0
      src/main/java/com/notnoop/apns/internal/QueuedApnsService.java
  48. 67 0
      src/main/java/com/notnoop/apns/internal/ReconnectPolicies.java
  49. 179 0
      src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java
  50. 147 0
      src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java
  51. 296 0
      src/main/java/com/notnoop/apns/internal/Utilities.java
  52. 61 0
      src/main/java/com/notnoop/exceptions/ApnsDeliveryErrorException.java
  53. 44 0
      src/main/java/com/notnoop/exceptions/ApnsException.java
  54. 64 0
      src/main/java/com/notnoop/exceptions/InvalidSSLConfig.java
  55. 69 0
      src/main/java/com/notnoop/exceptions/NetworkIOException.java
  56. 50 0
      src/main/java/com/notnoop/exceptions/RuntimeIOException.java
  57. BIN
      src/main/libs/MiPush_SDK_Server_2_2_19.jar
  58. 9 0
      src/main/resources/apns.properties
  59. 1 0
      src/main/resources/application.properties
  60. 2 0
      src/main/resources/hms.properties
  61. 2 0
      src/main/resources/meizu.properties
  62. 1 0
      src/main/resources/xiaomi.properties
  63. 16 0
      src/test/java/cn/wildfirechat/push/PushApplicationTests.java

+ 1 - 0
.gitignore

@@ -0,0 +1 @@
+target

BIN
apns/develop.p12


BIN
apns/product.p12


BIN
apns/voip.p12


BIN
apns/wfdemo_apns.p12


BIN
apns/wfdemo_voip.p12


+ 225 - 0
mvnw

@@ -0,0 +1,225 @@
+#!/bin/sh
+# ----------------------------------------------------------------------------
+# Licensed to the Apache Software Foundation (ASF) under one
+# or more contributor license agreements.  See the NOTICE file
+# distributed with this work for additional information
+# regarding copyright ownership.  The ASF licenses this file
+# to you 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.
+# ----------------------------------------------------------------------------
+
+# ----------------------------------------------------------------------------
+# Maven2 Start Up Batch script
+#
+# Required ENV vars:
+# ------------------
+#   JAVA_HOME - location of a JDK home dir
+#
+# Optional ENV vars
+# -----------------
+#   M2_HOME - location of maven2's installed home dir
+#   MAVEN_OPTS - parameters passed to the Java VM when running Maven
+#     e.g. to debug Maven itself, use
+#       set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+#   MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+# ----------------------------------------------------------------------------
+
+if [ -z "$MAVEN_SKIP_RC" ] ; then
+
+  if [ -f /etc/mavenrc ] ; then
+    . /etc/mavenrc
+  fi
+
+  if [ -f "$HOME/.mavenrc" ] ; then
+    . "$HOME/.mavenrc"
+  fi
+
+fi
+
+# OS specific support.  $var _must_ be set to either true or false.
+cygwin=false;
+darwin=false;
+mingw=false
+case "`uname`" in
+  CYGWIN*) cygwin=true ;;
+  MINGW*) mingw=true;;
+  Darwin*) darwin=true
+    # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
+    # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
+    if [ -z "$JAVA_HOME" ]; then
+      if [ -x "/usr/libexec/java_home" ]; then
+        export JAVA_HOME="`/usr/libexec/java_home`"
+      else
+        export JAVA_HOME="/Library/Java/Home"
+      fi
+    fi
+    ;;
+esac
+
+if [ -z "$JAVA_HOME" ] ; then
+  if [ -r /etc/gentoo-release ] ; then
+    JAVA_HOME=`java-config --jre-home`
+  fi
+fi
+
+if [ -z "$M2_HOME" ] ; then
+  ## resolve links - $0 may be a link to maven's home
+  PRG="$0"
+
+  # need this for relative symlinks
+  while [ -h "$PRG" ] ; do
+    ls=`ls -ld "$PRG"`
+    link=`expr "$ls" : '.*-> \(.*\)$'`
+    if expr "$link" : '/.*' > /dev/null; then
+      PRG="$link"
+    else
+      PRG="`dirname "$PRG"`/$link"
+    fi
+  done
+
+  saveddir=`pwd`
+
+  M2_HOME=`dirname "$PRG"`/..
+
+  # make it fully qualified
+  M2_HOME=`cd "$M2_HOME" && pwd`
+
+  cd "$saveddir"
+  # echo Using m2 at $M2_HOME
+fi
+
+# For Cygwin, ensure paths are in UNIX format before anything is touched
+if $cygwin ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --unix "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
+fi
+
+# For Migwn, ensure paths are in UNIX format before anything is touched
+if $mingw ; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME="`(cd "$M2_HOME"; pwd)`"
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
+  # TODO classpath?
+fi
+
+if [ -z "$JAVA_HOME" ]; then
+  javaExecutable="`which javac`"
+  if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
+    # readlink(1) is not available as standard on Solaris 10.
+    readLink=`which readlink`
+    if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
+      if $darwin ; then
+        javaHome="`dirname \"$javaExecutable\"`"
+        javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
+      else
+        javaExecutable="`readlink -f \"$javaExecutable\"`"
+      fi
+      javaHome="`dirname \"$javaExecutable\"`"
+      javaHome=`expr "$javaHome" : '\(.*\)/bin'`
+      JAVA_HOME="$javaHome"
+      export JAVA_HOME
+    fi
+  fi
+fi
+
+if [ -z "$JAVACMD" ] ; then
+  if [ -n "$JAVA_HOME"  ] ; then
+    if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+      # IBM's JDK on AIX uses strange locations for the executables
+      JAVACMD="$JAVA_HOME/jre/sh/java"
+    else
+      JAVACMD="$JAVA_HOME/bin/java"
+    fi
+  else
+    JAVACMD="`which java`"
+  fi
+fi
+
+if [ ! -x "$JAVACMD" ] ; then
+  echo "Error: JAVA_HOME is not defined correctly." >&2
+  echo "  We cannot execute $JAVACMD" >&2
+  exit 1
+fi
+
+if [ -z "$JAVA_HOME" ] ; then
+  echo "Warning: JAVA_HOME environment variable is not set."
+fi
+
+CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
+
+# traverses directory structure from process work directory to filesystem root
+# first directory with .mvn subdirectory is considered project base directory
+find_maven_basedir() {
+
+  if [ -z "$1" ]
+  then
+    echo "Path not specified to find_maven_basedir"
+    return 1
+  fi
+
+  basedir="$1"
+  wdir="$1"
+  while [ "$wdir" != '/' ] ; do
+    if [ -d "$wdir"/.mvn ] ; then
+      basedir=$wdir
+      break
+    fi
+    # workaround for JBEAP-8937 (on Solaris 10/Sparc)
+    if [ -d "${wdir}" ]; then
+      wdir=`cd "$wdir/.."; pwd`
+    fi
+    # end of workaround
+  done
+  echo "${basedir}"
+}
+
+# concatenates all lines of a file
+concat_lines() {
+  if [ -f "$1" ]; then
+    echo "$(tr -s '\n' ' ' < "$1")"
+  fi
+}
+
+BASE_DIR=`find_maven_basedir "$(pwd)"`
+if [ -z "$BASE_DIR" ]; then
+  exit 1;
+fi
+
+export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
+echo $MAVEN_PROJECTBASEDIR
+MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
+
+# For Cygwin, switch paths to Windows format before running java
+if $cygwin; then
+  [ -n "$M2_HOME" ] &&
+    M2_HOME=`cygpath --path --windows "$M2_HOME"`
+  [ -n "$JAVA_HOME" ] &&
+    JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
+  [ -n "$CLASSPATH" ] &&
+    CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
+  [ -n "$MAVEN_PROJECTBASEDIR" ] &&
+    MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
+fi
+
+WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+exec "$JAVACMD" \
+  $MAVEN_OPTS \
+  -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
+  "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
+  ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"

+ 143 - 0
mvnw.cmd

@@ -0,0 +1,143 @@
+@REM ----------------------------------------------------------------------------
+@REM Licensed to the Apache Software Foundation (ASF) under one
+@REM or more contributor license agreements.  See the NOTICE file
+@REM distributed with this work for additional information
+@REM regarding copyright ownership.  The ASF licenses this file
+@REM to you under the Apache License, Version 2.0 (the
+@REM "License"); you may not use this file except in compliance
+@REM with the License.  You may obtain a copy of the License at
+@REM
+@REM    http://www.apache.org/licenses/LICENSE-2.0
+@REM
+@REM Unless required by applicable law or agreed to in writing,
+@REM software distributed under the License is distributed on an
+@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+@REM KIND, either express or implied.  See the License for the
+@REM specific language governing permissions and limitations
+@REM under the License.
+@REM ----------------------------------------------------------------------------
+
+@REM ----------------------------------------------------------------------------
+@REM Maven2 Start Up Batch script
+@REM
+@REM Required ENV vars:
+@REM JAVA_HOME - location of a JDK home dir
+@REM
+@REM Optional ENV vars
+@REM M2_HOME - location of maven2's installed home dir
+@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
+@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a key stroke before ending
+@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
+@REM     e.g. to debug Maven itself, use
+@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
+@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
+@REM ----------------------------------------------------------------------------
+
+@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
+@echo off
+@REM enable echoing my setting MAVEN_BATCH_ECHO to 'on'
+@if "%MAVEN_BATCH_ECHO%" == "on"  echo %MAVEN_BATCH_ECHO%
+
+@REM set %HOME% to equivalent of $HOME
+if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
+
+@REM Execute a user defined script before this one
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
+@REM check for pre script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat"
+if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd"
+:skipRcPre
+
+@setlocal
+
+set ERROR_CODE=0
+
+@REM To isolate internal variables from possible post scripts, we use another setlocal
+@setlocal
+
+@REM ==== START VALIDATION ====
+if not "%JAVA_HOME%" == "" goto OkJHome
+
+echo.
+echo Error: JAVA_HOME not found in your environment. >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+:OkJHome
+if exist "%JAVA_HOME%\bin\java.exe" goto init
+
+echo.
+echo Error: JAVA_HOME is set to an invalid directory. >&2
+echo JAVA_HOME = "%JAVA_HOME%" >&2
+echo Please set the JAVA_HOME variable in your environment to match the >&2
+echo location of your Java installation. >&2
+echo.
+goto error
+
+@REM ==== END VALIDATION ====
+
+:init
+
+@REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
+@REM Fallback to current working directory if not found.
+
+set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
+IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
+
+set EXEC_DIR=%CD%
+set WDIR=%EXEC_DIR%
+:findBaseDir
+IF EXIST "%WDIR%"\.mvn goto baseDirFound
+cd ..
+IF "%WDIR%"=="%CD%" goto baseDirNotFound
+set WDIR=%CD%
+goto findBaseDir
+
+:baseDirFound
+set MAVEN_PROJECTBASEDIR=%WDIR%
+cd "%EXEC_DIR%"
+goto endDetectBaseDir
+
+:baseDirNotFound
+set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
+cd "%EXEC_DIR%"
+
+:endDetectBaseDir
+
+IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
+
+@setlocal EnableExtensions EnableDelayedExpansion
+for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
+@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
+
+:endReadAdditionalConfig
+
+SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
+
+set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
+set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
+
+%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
+if ERRORLEVEL 1 goto error
+goto end
+
+:error
+set ERROR_CODE=1
+
+:end
+@endlocal & set ERROR_CODE=%ERROR_CODE%
+
+if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost
+@REM check for post script, once with legacy .bat ending and once with .cmd ending
+if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat"
+if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd"
+:skipRcPost
+
+@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
+if "%MAVEN_BATCH_PAUSE%" == "on" pause
+
+if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE%
+
+exit /B %ERROR_CODE%

+ 147 - 0
pom.xml

@@ -0,0 +1,147 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
+	<modelVersion>4.0.0</modelVersion>
+
+	<groupId>cn.wildfirechat</groupId>
+	<artifactId>push</artifactId>
+	<version>0.0.1-SNAPSHOT</version>
+	<packaging>jar</packaging>
+
+	<name>push</name>
+	<description>Demo project for Spring Boot</description>
+
+	<parent>
+		<groupId>org.springframework.boot</groupId>
+		<artifactId>spring-boot-starter-parent</artifactId>
+		<version>2.0.6.RELEASE</version>
+		<relativePath/> <!-- lookup parent from repository -->
+	</parent>
+
+	<properties>
+		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
+		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
+		<java.version>1.8</java.version>
+	</properties>
+
+	<dependencies>
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-web</artifactId>
+		</dependency>
+
+		<dependency>
+			<groupId>org.springframework.boot</groupId>
+			<artifactId>spring-boot-starter-test</artifactId>
+			<scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>com.xiaomi.push</groupId>
+			<artifactId>mipush-sdk-server</artifactId>
+			<version>2.2.18</version>
+			<scope>system</scope>
+			<systemPath>${project.basedir}/src/main/libs/MiPush_SDK_Server_2_2_19.jar
+			</systemPath>
+		</dependency>
+
+		<dependency>
+			<groupId>com.meizu.flyme</groupId>
+			<artifactId>push-server-sdk</artifactId>
+			<version>1.2.7.20180307_release</version>
+		</dependency>
+
+		<!-- https://mvnrepository.com/artifact/com.google.code.gson/gson -->
+		<dependency>
+			<groupId>com.google.code.gson</groupId>
+			<artifactId>gson</artifactId>
+			<version>2.8.2</version>
+		</dependency>
+
+		<dependency>
+			<groupId>commons-io</groupId>
+			<artifactId>commons-io</artifactId>
+			<version>2.5</version>
+		</dependency>
+
+		<dependency>
+			<groupId>com.googlecode.json-simple</groupId>
+			<artifactId>json-simple</artifactId>
+			<version>1.1.1</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-api</artifactId>
+			<version>1.7.5</version>
+		</dependency>
+
+		<dependency>
+			<groupId>org.slf4j</groupId>
+			<artifactId>slf4j-log4j12</artifactId>
+			<version>1.7.5</version>
+		</dependency>
+
+
+		<dependency>
+			<groupId>commons-httpclient</groupId>
+			<artifactId>commons-httpclient</artifactId>
+			<version>3.1</version>
+		</dependency>
+
+		<dependency>
+			<groupId>uk.org.lidalia</groupId>
+			<artifactId>slf4j-test</artifactId>
+			<version>1.0.0-jdk6</version>
+			<scope>test</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>com.google.code.findbugs</groupId>
+			<artifactId>annotations</artifactId>
+			<version>2.0.3</version>
+			<scope>provided</scope>
+		</dependency>
+
+		<dependency>
+			<groupId>org.mockito</groupId>
+			<artifactId>mockito-all</artifactId>
+			<version>1.9.5</version>
+			<type>jar</type>
+			<scope>test</scope>
+		</dependency>
+
+
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-core</artifactId>
+			<version>2.9.4</version>
+		</dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-databind</artifactId>
+			<version>2.9.4</version>
+		</dependency>
+		<dependency>
+			<groupId>com.fasterxml.jackson.core</groupId>
+			<artifactId>jackson-annotations</artifactId>
+			<version>2.9.4</version>
+		</dependency>
+	</dependencies>
+
+	<build>
+		<plugins>
+			<plugin>
+				<groupId>org.springframework.boot</groupId>
+				<artifactId>spring-boot-maven-plugin</artifactId>
+			</plugin>
+		</plugins>
+	</build>
+
+
+</project>

+ 11 - 0
src/main/java/cn/wildfirechat/push/PushApplication.java

@@ -0,0 +1,11 @@
+package cn.wildfirechat.push;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class PushApplication {
+	public static void main(String[] args) {
+		SpringApplication.run(PushApplication.class, args);
+	}
+}

+ 28 - 0
src/main/java/cn/wildfirechat/push/PushController.java

@@ -0,0 +1,28 @@
+package cn.wildfirechat.push;
+
+import cn.wildfirechat.push.android.AndroidPushService;
+import cn.wildfirechat.push.ios.IOSPushService;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class PushController {
+
+    @Autowired
+    private AndroidPushService mAndroidPushService;
+
+    @Autowired
+    private IOSPushService mIOSPushService;
+
+    @PostMapping(value = "/android/push", produces = "application/json;charset=UTF-8"   )
+    public Object androidPush(@RequestBody PushMessage pushMessage) {
+        return mAndroidPushService.push(pushMessage);
+    }
+
+    @PostMapping(value = "/ios/push", produces = "application/json;charset=UTF-8"   )
+    public Object iOSPush(@RequestBody PushMessage pushMessage) {
+        return mIOSPushService.push(pushMessage);
+    }
+}

+ 161 - 0
src/main/java/cn/wildfirechat/push/PushMessage.java

@@ -0,0 +1,161 @@
+package cn.wildfirechat.push;
+
+
+public class PushMessage {
+    public String sender;
+    public String senderName;
+    public int convType;
+    public String target;
+    public String targetName;
+    public int line;
+    public int cntType;
+    public long serverTime;
+    //消息的类型,普通消息通知栏;voip要透传。
+    public int pushMessageType;
+    //推送类型,android推送分为小米/华为/魅族等。ios分别为开发和发布。
+    public int pushType;
+    public String pushContent;
+    public int unReceivedMsg;
+    public int mentionedType;
+    public String packageName;
+    public String deviceToken;
+    public String voipDeviceToken;
+    public String language;
+
+
+    public String getSender() {
+        return sender;
+    }
+
+    public void setSender(String sender) {
+        this.sender = sender;
+    }
+
+    public String getSenderName() {
+        return senderName;
+    }
+
+    public void setSenderName(String senderName) {
+        this.senderName = senderName;
+    }
+
+    public int getConvType() {
+        return convType;
+    }
+
+    public void setConvType(int convType) {
+        this.convType = convType;
+    }
+
+    public String getTarget() {
+        return target;
+    }
+
+    public void setTarget(String target) {
+        this.target = target;
+    }
+
+    public String getTargetName() {
+        return targetName;
+    }
+
+    public void setTargetName(String targetName) {
+        this.targetName = targetName;
+    }
+
+    public int getLine() {
+        return line;
+    }
+
+    public void setLine(int line) {
+        this.line = line;
+    }
+
+    public int getCntType() {
+        return cntType;
+    }
+
+    public void setCntType(int cntType) {
+        this.cntType = cntType;
+    }
+
+    public long getServerTime() {
+        return serverTime;
+    }
+
+    public void setServerTime(long serverTime) {
+        this.serverTime = serverTime;
+    }
+
+    public int getPushMessageType() {
+        return pushMessageType;
+    }
+
+    public void setPushMessageType(int pushMessageType) {
+        this.pushMessageType = pushMessageType;
+    }
+
+    public int getPushType() {
+        return pushType;
+    }
+
+    public void setPushType(int pushType) {
+        this.pushType = pushType;
+    }
+
+    public String getPushContent() {
+        return pushContent;
+    }
+
+    public void setPushContent(String pushContent) {
+        this.pushContent = pushContent;
+    }
+
+    public int getUnReceivedMsg() {
+        return unReceivedMsg;
+    }
+
+    public void setUnReceivedMsg(int unReceivedMsg) {
+        this.unReceivedMsg = unReceivedMsg;
+    }
+
+    public int getMentionedType() {
+        return mentionedType;
+    }
+
+    public void setMentionedType(int mentionedType) {
+        this.mentionedType = mentionedType;
+    }
+
+    public String getPackageName() {
+        return packageName;
+    }
+
+    public void setPackageName(String packageName) {
+        this.packageName = packageName;
+    }
+
+    public String getDeviceToken() {
+        return deviceToken;
+    }
+
+    public void setDeviceToken(String deviceToken) {
+        this.deviceToken = deviceToken;
+    }
+
+    public String getVoipDeviceToken() {
+        return voipDeviceToken;
+    }
+
+    public void setVoipDeviceToken(String voipDeviceToken) {
+        this.voipDeviceToken = voipDeviceToken;
+    }
+
+    public String getLanguage() {
+        return language;
+    }
+
+    public void setLanguage(String language) {
+        this.language = language;
+    }
+}

+ 7 - 0
src/main/java/cn/wildfirechat/push/PushMessageType.java

@@ -0,0 +1,7 @@
+package cn.wildfirechat.push;
+
+public interface PushMessageType {
+    int PUSH_MESSAGE_TYPE_NORMAL = 0;
+    int PUSH_MESSAGE_TYPE_VOIP_INVITE = 1;
+    int PUSH_MESSAGE_TYPE_VOIP_BYE = 2;
+}

+ 7 - 0
src/main/java/cn/wildfirechat/push/android/AndroidPushService.java

@@ -0,0 +1,7 @@
+package cn.wildfirechat.push.android;
+
+import cn.wildfirechat.push.PushMessage;
+
+public interface AndroidPushService {
+    Object push(PushMessage pushMessage);
+}

+ 45 - 0
src/main/java/cn/wildfirechat/push/android/AndroidPushServiceImpl.java

@@ -0,0 +1,45 @@
+package cn.wildfirechat.push.android;
+
+import cn.wildfirechat.push.PushMessage;
+import cn.wildfirechat.push.android.hms.HMSPush;
+import cn.wildfirechat.push.android.meizu.MeiZuPush;
+import cn.wildfirechat.push.android.xiaomi.XiaomiPush;
+import com.google.gson.Gson;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class AndroidPushServiceImpl implements AndroidPushService {
+    private static final Logger LOG = LoggerFactory.getLogger(AndroidPushServiceImpl.class);
+    @Autowired
+    private HMSPush hmsPush;
+
+    @Autowired
+    private MeiZuPush meiZuPush;
+
+    @Autowired
+    private XiaomiPush xiaomiPush;
+
+
+    @Override
+    public Object push(PushMessage pushMessage) {
+        LOG.info("Android push {}", new Gson().toJson(pushMessage));
+        switch (pushMessage.getPushType()) {
+            case AndroidPushType.ANDROID_PUSH_TYPE_XIAOMI:
+                xiaomiPush.push(pushMessage);
+                break;
+            case AndroidPushType.ANDROID_PUSH_TYPE_HUAWEI:
+                hmsPush.push(pushMessage);
+                break;
+            case AndroidPushType.ANDROID_PUSH_TYPE_MEIZU:
+                meiZuPush.push(pushMessage);
+                break;
+            default:
+                LOG.info("unknown push type");
+                break;
+        }
+        return "ok";
+    }
+}

+ 7 - 0
src/main/java/cn/wildfirechat/push/android/AndroidPushType.java

@@ -0,0 +1,7 @@
+package cn.wildfirechat.push.android;
+
+public interface AndroidPushType {
+    int ANDROID_PUSH_TYPE_XIAOMI = 1;
+    int ANDROID_PUSH_TYPE_HUAWEI = 2;
+    int ANDROID_PUSH_TYPE_MEIZU = 3;
+}

+ 29 - 0
src/main/java/cn/wildfirechat/push/android/hms/HMSConfig.java

@@ -0,0 +1,29 @@
+package cn.wildfirechat.push.android.hms;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+@Configuration
+@ConfigurationProperties(prefix="hms")
+@PropertySource(value = "classpath:hms.properties")
+public class HMSConfig {
+    private String appSecret;
+    private String appId;
+
+    public String getAppSecret() {
+        return appSecret;
+    }
+
+    public void setAppSecret(String appSecret) {
+        this.appSecret = appSecret;
+    }
+
+    public String getAppId() {
+        return appId;
+    }
+
+    public void setAppId(String appId) {
+        this.appId = appId;
+    }
+}

+ 136 - 0
src/main/java/cn/wildfirechat/push/android/hms/HMSPush.java

@@ -0,0 +1,136 @@
+package cn.wildfirechat.push.android.hms;
+
+
+import cn.wildfirechat.push.PushMessage;
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.google.gson.Gson;
+import org.apache.commons.io.IOUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLEncoder;
+import java.text.MessageFormat;
+import java.util.List;
+
+@Component
+public class HMSPush {
+    private static final Logger LOG = LoggerFactory.getLogger(HMSPush.class);
+    private static final String tokenUrl = "https://login.vmall.com/oauth2/token"; //获取认证Token的URL
+    private static final String apiUrl = "https://api.push.hicloud.com/pushsend.do"; //应用级消息下发API
+    private String accessToken;//下发通知消息的认证Token
+    private long tokenExpiredTime;  //accessToken的过期时间
+
+    @Autowired
+    private HMSConfig mConfig;
+
+    //获取下发通知消息的认证Token
+    private void refreshToken() throws IOException {
+        LOG.info("hms refresh token");
+        String msgBody = MessageFormat.format(
+            "grant_type=client_credentials&client_secret={0}&client_id={1}",
+            URLEncoder.encode(mConfig.getAppSecret(), "UTF-8"), mConfig.getAppId());
+        String response = httpPost(tokenUrl, msgBody, 5000, 5000);
+        JSONObject obj = JSONObject.parseObject(response);
+        accessToken = obj.getString("access_token");
+        tokenExpiredTime = System.currentTimeMillis() + obj.getLong("expires_in") - 5*60*1000;
+        LOG.info("hms refresh token with result {}", response);
+    }
+
+    //发送Push消息
+    public void push(PushMessage pushMessage) {
+        if (tokenExpiredTime <= System.currentTimeMillis()) {
+            try {
+                refreshToken();
+            } catch (IOException e) {
+                e.printStackTrace();
+            }
+        }
+        /*PushManager.requestToken为客户端申请token的方法,可以调用多次以防止申请token失败*/
+        /*PushToken不支持手动编写,需使用客户端的onToken方法获取*/
+        JSONArray deviceTokens = new JSONArray();//目标设备Token
+        deviceTokens.add(pushMessage.getDeviceToken());
+
+
+        JSONObject msg = new JSONObject();
+        msg.put("type", 1);//3: 通知栏消息,异步透传消息请根据接口文档设置
+        msg.put("body", new Gson().toJson(pushMessage));//通知栏消息body内容
+
+        JSONObject hps = new JSONObject();//华为PUSH消息总结构体
+        hps.put("msg", msg);
+
+        JSONObject payload = new JSONObject();
+        payload.put("hps", hps);
+
+        LOG.info("send push to HMS {}", payload);
+
+        try {
+            String postBody = MessageFormat.format(
+                "access_token={0}&nsp_svc={1}&nsp_ts={2}&device_token_list={3}&payload={4}",
+                URLEncoder.encode(accessToken,"UTF-8"),
+                URLEncoder.encode("openpush.message.api.send","UTF-8"),
+                URLEncoder.encode(String.valueOf(System.currentTimeMillis() / 1000),"UTF-8"),
+                URLEncoder.encode(deviceTokens.toString(),"UTF-8"),
+                URLEncoder.encode(payload.toString(),"UTF-8"));
+
+            String postUrl = apiUrl + "?nsp_ctx=" + URLEncoder.encode("{\"ver\":\"1\", \"appId\":\"" + mConfig.getAppId() + "\"}", "UTF-8");
+            String response = httpPost(postUrl, postBody, 5000, 5000);
+            LOG.info("Push to {} response {}", pushMessage.getDeviceToken(), response);
+        } catch (IOException e) {
+            e.printStackTrace();
+            LOG.info("Push to {} with exception", pushMessage.getDeviceToken(), e);
+        }
+    }
+
+    public String httpPost(String httpUrl, String data, int connectTimeout, int readTimeout) throws IOException {
+        OutputStream outPut = null;
+        HttpURLConnection urlConnection = null;
+        InputStream in = null;
+
+        try {
+            URL url = new URL(httpUrl);
+            urlConnection = (HttpURLConnection)url.openConnection();
+            urlConnection.setRequestMethod("POST");
+            urlConnection.setDoOutput(true);
+            urlConnection.setDoInput(true);
+            urlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded; charset=UTF-8");
+            urlConnection.setConnectTimeout(connectTimeout);
+            urlConnection.setReadTimeout(readTimeout);
+            urlConnection.connect();
+
+            // POST data
+            outPut = urlConnection.getOutputStream();
+            outPut.write(data.getBytes("UTF-8"));
+            outPut.flush();
+
+            // read response
+            if (urlConnection.getResponseCode() < 400) {
+                in = urlConnection.getInputStream();
+            } else {
+                in = urlConnection.getErrorStream();
+            }
+
+            List<String> lines = IOUtils.readLines(in, urlConnection.getContentEncoding());
+            StringBuffer strBuf = new StringBuffer();
+            for (String line : lines) {
+                strBuf.append(line);
+            }
+            LOG.info(strBuf.toString());
+            return strBuf.toString();
+        }
+        finally {
+            IOUtils.closeQuietly(outPut);
+            IOUtils.closeQuietly(in);
+            if (urlConnection != null) {
+                urlConnection.disconnect();
+            }
+        }
+    }
+}

+ 29 - 0
src/main/java/cn/wildfirechat/push/android/meizu/MeiZuConfig.java

@@ -0,0 +1,29 @@
+package cn.wildfirechat.push.android.meizu;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+@Configuration
+@ConfigurationProperties(prefix="meizu")
+@PropertySource(value = "classpath:meizu.properties")
+public class MeiZuConfig {
+    private String appSecret;
+    private long appId;
+
+    public String getAppSecret() {
+        return appSecret;
+    }
+
+    public void setAppSecret(String appSecret) {
+        this.appSecret = appSecret;
+    }
+
+    public long getAppId() {
+        return appId;
+    }
+
+    public void setAppId(long appId) {
+        this.appId = appId;
+    }
+}

+ 68 - 0
src/main/java/cn/wildfirechat/push/android/meizu/MeiZuPush.java

@@ -0,0 +1,68 @@
+package cn.wildfirechat.push.android.meizu;
+
+import cn.wildfirechat.push.PushMessage;
+import com.meizu.push.sdk.server.IFlymePush;
+import com.meizu.push.sdk.server.constant.ResultPack;
+import com.meizu.push.sdk.server.model.push.PushResult;
+import com.meizu.push.sdk.server.model.push.VarnishedMessage;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+
+import javax.annotation.PostConstruct;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+@Component
+public class MeiZuPush {
+    private static final Logger LOG = LoggerFactory.getLogger(MeiZuPush.class);
+    private IFlymePush flymePush;
+
+    @PostConstruct
+    public void init() {
+        this.flymePush = new IFlymePush(mConfig.getAppSecret());
+    }
+
+    @Autowired
+    private MeiZuConfig mConfig;
+
+    public void push(PushMessage pushMessage) {
+        //组装透传消息
+        VarnishedMessage message = new VarnishedMessage.Builder()
+            .appId(mConfig.getAppId())
+            .title("WildfireChat")
+            .content(pushMessage.pushContent)
+            .validTime(1)
+            .build();
+
+        //目标用户
+        List<String> pushIds = new ArrayList<String>();
+        pushIds.add(pushMessage.getDeviceToken());
+
+        try {
+            // 1 调用推送服务
+            ResultPack<PushResult> result = flymePush.pushMessage(message, pushIds);
+            if (result.isSucceed()) {
+                // 2 调用推送服务成功 (其中map为设备的具体推送结果,一般业务针对超速的code类型做处理)
+                PushResult pushResult = result.value();
+                String msgId = pushResult.getMsgId();//推送消息ID,用于推送流程明细排查
+                Map<String, List<String>> targetResultMap = pushResult.getRespTarget();//推送结果,全部推送成功,则map为empty
+                LOG.info("push result:" + pushResult);
+                if (targetResultMap != null && !targetResultMap.isEmpty()) {
+                    System.err.println("push fail token:" + targetResultMap);
+                }
+            } else {
+                // 调用推送接口服务异常 eg: appId、appKey非法、推送消息非法.....
+                // result.code(); //服务异常码
+                // result.comment();//服务异常描述
+                LOG.info(String.format("pushMessage error code:%s comment:%s", result.code(), result.comment()));
+            }
+        } catch (IOException e) {
+            e.printStackTrace();
+        }
+    }
+
+}

+ 20 - 0
src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiConfig.java

@@ -0,0 +1,20 @@
+package cn.wildfirechat.push.android.xiaomi;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+@Configuration
+@ConfigurationProperties(prefix="xiaomi")
+@PropertySource(value = "classpath:xiaomi.properties")
+public class XiaomiConfig {
+    private String appSecret;
+
+    public String getAppSecret() {
+        return appSecret;
+    }
+
+    public void setAppSecret(String appSecret) {
+        this.appSecret = appSecret;
+    }
+}

+ 71 - 0
src/main/java/cn/wildfirechat/push/android/xiaomi/XiaomiPush.java

@@ -0,0 +1,71 @@
+package cn.wildfirechat.push.android.xiaomi;
+
+
+import cn.wildfirechat.push.PushMessage;
+import cn.wildfirechat.push.PushMessageType;
+import com.google.gson.Gson;
+import com.xiaomi.xmpush.server.Constants;
+import com.xiaomi.xmpush.server.Message;
+import com.xiaomi.xmpush.server.Result;
+import com.xiaomi.xmpush.server.Sender;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.json.simple.parser.ParseException;
+
+
+import java.io.IOException;
+
+import static com.xiaomi.xmpush.server.Message.NOTIFY_TYPE_ALL;
+
+@Component
+public class XiaomiPush {
+    private static final Logger LOG = LoggerFactory.getLogger(XiaomiPush.class);
+    @Autowired
+    private XiaomiConfig mConfig;
+
+
+    public void push(PushMessage pushMessage) {
+        Constants.useOfficial();
+        Sender sender = new Sender(mConfig.getAppSecret());
+
+        Message message;
+        if(pushMessage.pushMessageType != PushMessageType.PUSH_MESSAGE_TYPE_NORMAL) {
+            //voip
+            long timeToLive = 60 * 1000; // 1 min
+            message = new Message.Builder()
+                    .payload(new Gson().toJson(pushMessage))
+                    .restrictedPackageName(pushMessage.getPackageName())
+                    .passThrough(1)  //透传
+                    .timeToLive(timeToLive)
+                    .enableFlowControl(false)
+                    .build();
+        } else {
+            long timeToLive = 600 * 1000;//10 min
+            message = new Message.Builder()
+                    .payload(new Gson().toJson(pushMessage))
+                    .title("新消息提醒")
+                    .description(pushMessage.pushContent)
+                    .notifyType(NOTIFY_TYPE_ALL)
+                    .restrictedPackageName(pushMessage.getPackageName())
+                    .passThrough(0)
+                    .timeToLive(timeToLive)
+                    .enableFlowControl(true)
+                    .build();
+        }
+
+        Result result = null;
+        try {
+            result = sender.send(message, pushMessage.getDeviceToken(), 3);
+        } catch (IOException e) {
+            e.printStackTrace();
+        } catch (ParseException e) {
+            e.printStackTrace();
+        }
+
+        LOG.info("Server response: MessageId: " + result.getMessageId()
+            + " ErrorCode: " + result.getErrorCode().toString()
+            + " Reason: " + result.getReason());
+    }
+}

+ 86 - 0
src/main/java/cn/wildfirechat/push/ios/ApnsConfig.java

@@ -0,0 +1,86 @@
+package cn.wildfirechat.push.ios;
+
+import org.springframework.boot.context.properties.ConfigurationProperties;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.PropertySource;
+
+@Configuration
+@ConfigurationProperties(prefix="apns")
+@PropertySource(value = "classpath:apns.properties")
+public class ApnsConfig {
+    String productCerPath;
+    String productCerPwd;
+
+    String developCerPath;
+    String developCerPwd;
+
+    String voipCerPath;
+    String voipCerPwd;
+
+    String alert;
+    String voipAlert;
+
+    public String getProductCerPath() {
+        return productCerPath;
+    }
+
+    public void setProductCerPath(String productCerPath) {
+        this.productCerPath = productCerPath;
+    }
+
+    public String getProductCerPwd() {
+        return productCerPwd;
+    }
+
+    public void setProductCerPwd(String productCerPwd) {
+        this.productCerPwd = productCerPwd;
+    }
+
+    public String getDevelopCerPath() {
+        return developCerPath;
+    }
+
+    public void setDevelopCerPath(String developCerPath) {
+        this.developCerPath = developCerPath;
+    }
+
+    public String getDevelopCerPwd() {
+        return developCerPwd;
+    }
+
+    public void setDevelopCerPwd(String developCerPwd) {
+        this.developCerPwd = developCerPwd;
+    }
+
+    public String getVoipCerPath() {
+        return voipCerPath;
+    }
+
+    public void setVoipCerPath(String voipCerPath) {
+        this.voipCerPath = voipCerPath;
+    }
+
+    public String getVoipCerPwd() {
+        return voipCerPwd;
+    }
+
+    public void setVoipCerPwd(String voipCerPwd) {
+        this.voipCerPwd = voipCerPwd;
+    }
+
+    public String getAlert() {
+        return alert;
+    }
+
+    public void setAlert(String alert) {
+        this.alert = alert;
+    }
+
+    public String getVoipAlert() {
+        return voipAlert;
+    }
+
+    public void setVoipAlert(String voipAlert) {
+        this.voipAlert = voipAlert;
+    }
+}

+ 187 - 0
src/main/java/cn/wildfirechat/push/ios/ApnsServer.java

@@ -0,0 +1,187 @@
+package cn.wildfirechat.push.ios;
+
+import cn.wildfirechat.push.PushMessage;
+import cn.wildfirechat.push.PushMessageType;
+import com.notnoop.apns.*;
+import com.notnoop.exceptions.ApnsDeliveryErrorException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import javax.annotation.PostConstruct;
+import java.util.Date;
+import java.util.Map;
+
+import static com.notnoop.apns.DeliveryError.INVALID_TOKEN;
+
+@Component
+public class ApnsServer implements ApnsDelegate {
+    private static final Logger LOG = LoggerFactory.getLogger(ApnsServer.class);
+    @Override
+    public void messageSent(ApnsNotification message, boolean resent) {
+        LOG.info("APNS push sent:{}", message.getDeviceToken());
+    }
+
+    @Override
+    public void messageSendFailed(ApnsNotification message, Throwable e) {
+        LOG.info("APNS push failure:{}", e.getMessage());
+        if(e instanceof ApnsDeliveryErrorException) {
+            ApnsDeliveryErrorException apnsDeliveryErrorException = (ApnsDeliveryErrorException)e;
+            LOG.info("APNS error code:{}", apnsDeliveryErrorException.getDeliveryError());
+            if (apnsDeliveryErrorException.getDeliveryError() == INVALID_TOKEN) {
+                if (message.getDeviceId() != null) {
+                    LOG.error("Invalide token!!!");
+                } else {
+                    LOG.error("APNS ERROR without deviceId:{}", message);
+                }
+            }
+
+        }
+    }
+
+    @Override
+    public void connectionClosed(DeliveryError e, int messageIdentifier) {
+        LOG.info("111");
+    }
+
+    @Override
+    public void cacheLengthExceeded(int newCacheLength) {
+        LOG.info("111");
+    }
+
+    @Override
+    public void notificationsResent(int resendCount) {
+        LOG.info("111");
+    }
+
+    ApnsService productSvc;
+    ApnsService developSvc;
+    ApnsService voipSvc;
+
+    @Autowired
+    private ApnsConfig mConfig;
+
+    @PostConstruct
+    private void init() {
+        if (StringUtils.isEmpty(mConfig.alert)) {
+            mConfig.alert = "default";
+        }
+
+        if (StringUtils.isEmpty(mConfig.voipAlert)) {
+            mConfig.alert = "default";
+        }
+
+        productSvc = APNS.newService()
+                .asBatched(3, 10)
+                .withAppleDestination(true)
+                .withCert(mConfig.productCerPath, mConfig.productCerPwd)
+                .withDelegate(this)
+                .build();
+
+        developSvc = APNS.newService()
+                .asBatched(3, 10)
+                .withAppleDestination(false)
+                .withCert(mConfig.developCerPath, mConfig.developCerPwd)
+                .withDelegate(this)
+                .build();
+
+        voipSvc = APNS.newService()
+                .withAppleDestination(true)
+                .withCert(mConfig.voipCerPath, mConfig.voipCerPwd)
+                .withDelegate(this)
+                .build();
+
+        productSvc.start();
+        developSvc.start();
+        voipSvc.start();
+    }
+
+
+    public void pushMessage(PushMessage pushMessage) {
+
+        ApnsService service = developSvc;
+        if (pushMessage.getPushType() == IOSPushType.IOS_PUSH_TYPE_DISTRIBUTION) {
+            if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_NORMAL || StringUtils.isEmpty(pushMessage.getVoipDeviceToken())) {
+                service = productSvc;
+            } else {
+                service = voipSvc;
+            }
+        }
+
+
+        if (service == null) {
+            LOG.error("Service not exist!!!!");
+            return;
+        }
+        String sound = mConfig.alert;
+
+        String pushContent = pushMessage.getPushContent();
+        if (pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_INVITE) {
+            pushContent = "通话邀请";
+            sound = mConfig.voipAlert;
+        } else if(pushMessage.pushMessageType == PushMessageType.PUSH_MESSAGE_TYPE_VOIP_BYE) {
+            pushContent = "通话结束";
+            sound = null;
+        }
+
+        int badge = pushMessage.getUnReceivedMsg();
+        if (badge <= 0) {
+            badge = 1;
+        }
+
+        String title;
+        String body;
+        //todo 这里需要加上语言的处理,客户端会上报自己的语言,在DeviceInfo那个类中
+//        if (pushMessage.language == "zh_CN") {
+//
+//        } else if(pushMessage.language == "US_EN") {
+//
+//        }
+        if (pushMessage.convType == 1) {
+            title = pushMessage.targetName;
+            if (StringUtils.isEmpty(title)) {
+                title = "群聊";
+            }
+
+            if (StringUtils.isEmpty(pushMessage.senderName)) {
+                body = pushContent;
+            } else {
+                body = pushMessage.senderName + ":" + pushContent;
+            }
+
+            if (pushMessage.mentionedType == 1) {
+                if (StringUtils.isEmpty(pushMessage.senderName)) {
+                    body = "有人在群里@了你";
+                } else {
+                    body = pushMessage.senderName + "在群里@了你";
+                }
+            } else if(pushMessage.mentionedType == 2) {
+                if (StringUtils.isEmpty(pushMessage.senderName)) {
+                    body = "有人在群里@了大家";
+                } else {
+                    body = pushMessage.senderName + "在群里@了大家";
+                }
+            }
+        } else {
+            if (StringUtils.isEmpty(pushMessage.senderName)) {
+                title = "消息";
+            } else {
+                title = pushMessage.senderName;
+            }
+            body = pushContent;
+        }
+
+        final String payload = APNS.newPayload().alertBody(body).badge(badge).alertTitle(title).sound(sound).build();
+        final ApnsNotification goodMsg = service.push(service == voipSvc ? pushMessage.getVoipDeviceToken() : pushMessage.getDeviceToken(), payload, null);
+        LOG.info("Message id: " + goodMsg.getIdentifier());
+
+
+        //检查key到期日期
+        final Map<String, Date> inactiveDevices = service.getInactiveDevices();
+        for (final Map.Entry<String, Date> ent : inactiveDevices.entrySet()) {
+            LOG.info("Inactive " + ent.getKey() + " at date " + ent.getValue());
+        }
+    }
+}

+ 7 - 0
src/main/java/cn/wildfirechat/push/ios/IOSPushService.java

@@ -0,0 +1,7 @@
+package cn.wildfirechat.push.ios;
+
+import cn.wildfirechat.push.PushMessage;
+
+public interface IOSPushService {
+    Object push(PushMessage pushMessage);
+}

+ 22 - 0
src/main/java/cn/wildfirechat/push/ios/IOSPushServiceImpl.java

@@ -0,0 +1,22 @@
+package cn.wildfirechat.push.ios;
+
+import cn.wildfirechat.push.PushMessage;
+import com.google.gson.Gson;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+@Service
+public class IOSPushServiceImpl implements IOSPushService {
+    private static final Logger LOG = LoggerFactory.getLogger(IOSPushServiceImpl.class);
+    @Autowired
+    public ApnsServer apnsServer;
+
+    @Override
+    public Object push(PushMessage pushMessage) {
+        LOG.info("iOS push {}", new Gson().toJson(pushMessage));
+        apnsServer.pushMessage(pushMessage);
+        return "OK";
+    }
+}

+ 6 - 0
src/main/java/cn/wildfirechat/push/ios/IOSPushType.java

@@ -0,0 +1,6 @@
+package cn.wildfirechat.push.ios;
+
+public interface IOSPushType {
+    int IOS_PUSH_TYPE_DISTRIBUTION = 0;
+    int IOS_PUSH_TYPE_DEVELOPEMENT = 1;
+}

+ 57 - 0
src/main/java/com/notnoop/apns/APNS.java

@@ -0,0 +1,57 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+/**
+ * The main class to interact with the APNS Service.
+ *
+ * Provides an interface to create the {@link ApnsServiceBuilder} and
+ * {@code ApnsNotification} payload.
+ *
+ */
+public final class APNS {
+
+    private APNS() { throw new AssertionError("Uninstantiable class"); }
+
+    /**
+     * Returns a new Payload builder
+     */
+    public static PayloadBuilder newPayload() {
+        return new PayloadBuilder();
+    }
+
+    /**
+     * Returns a new APNS Service for sending iPhone notifications
+     */
+    public static ApnsServiceBuilder newService() {
+        return new ApnsServiceBuilder();
+    }
+}

+ 92 - 0
src/main/java/com/notnoop/apns/ApnsDelegate.java

@@ -0,0 +1,92 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+/**
+ * A delegate that gets notified of the status of notification delivery to the
+ * Apple Server.
+ *
+ * The delegate doesn't get notified when the notification actually arrives at
+ * the phone.
+ */
+public interface ApnsDelegate {
+
+    /**
+     * Called when message was successfully sent to the Apple servers
+     *
+     * @param message the notification that was sent
+     * @param resent whether the notification was resent after an error
+     */
+    public void messageSent(ApnsNotification message, boolean resent);
+
+    /**
+     * Called when the delivery of the message failed for any reason
+     *
+     * If message is null, then your notification has been rejected by Apple but
+     * it has been removed from the cache so it is not possible to identify
+     * which notification caused the error. In this case subsequent
+     * notifications may be lost. If this happens you should consider increasing
+     * your cacheLength value to prevent data loss.
+     *
+     * @param message the notification that was attempted to be sent
+     * @param e the cause and description of the failure
+     */
+    public void messageSendFailed(ApnsNotification message, Throwable e);
+
+    /**
+     * The connection was closed and/or an error packet was received while
+     * monitoring was turned on.
+     *
+     * @param e the delivery error
+     * @param messageIdentifier  id of the message that failed
+     */
+    public void connectionClosed(DeliveryError e, int messageIdentifier);
+
+    /**
+     * The resend cache needed a bigger size (while resending messages)
+     *
+     * @param newCacheLength new size of the resend cache.
+     */
+    public void cacheLengthExceeded(int newCacheLength);
+
+    /**
+     * A number of notifications has been queued for resending due to a error-response
+     * packet being received.
+     *
+     * @param resendCount the number of messages being queued for resend
+     */
+    public void notificationsResent(int resendCount);
+    
+    /**
+     * A no operation delegate that does nothing!
+     */
+    public final static ApnsDelegate EMPTY = new ApnsDelegateAdapter();
+}

+ 52 - 0
src/main/java/com/notnoop/apns/ApnsDelegateAdapter.java

@@ -0,0 +1,52 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+/**
+ * A no operation delegate that does nothing!
+ */
+public class ApnsDelegateAdapter implements ApnsDelegate {
+
+    public void messageSent(ApnsNotification message, boolean resent) {
+    }
+
+    public void messageSendFailed(ApnsNotification message, Throwable e) {
+    }
+
+    public void connectionClosed(DeliveryError e, int messageIdentifier) {
+    }
+
+    public void cacheLengthExceeded(int newCacheLength) {
+    }
+
+    public void notificationsResent(int resendCount) {
+    }
+}

+ 76 - 0
src/main/java/com/notnoop/apns/ApnsNotification.java

@@ -0,0 +1,76 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+/**
+ * Represents an APNS notification to be sent to Apple service.
+ */
+public interface ApnsNotification {
+
+    /**
+     * Returns the binary representation of the device token.
+     */
+    public byte[] getDeviceToken();
+
+    /**
+     * Returns the binary representation of the payload.
+     *
+     */
+    public byte[] getPayload();
+
+    /**
+     * Returns the identifier of the current message.  The
+     * identifier is an application generated identifier.
+     *
+     * @return the notification identifier
+     */
+    public int getIdentifier();
+
+    /**
+     * Returns the expiry date of the notification, a fixed UNIX
+     * epoch date expressed in seconds
+     *
+     * @return the expiry date of the notification
+     */
+    public int getExpiry();
+
+    /**
+     * Returns the binary representation of the message as expected by the
+     * APNS server.
+     *
+     * The returned array can be used to sent directly to the APNS server
+     * (on the wire/socket) without any modification.
+     */
+    public byte[] marshall();
+
+
+    public String getDeviceId();
+}

+ 140 - 0
src/main/java/com/notnoop/apns/ApnsService.java

@@ -0,0 +1,140 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+import java.util.Collection;
+import java.util.Date;
+import java.util.Map;
+
+import com.notnoop.exceptions.NetworkIOException;
+
+/**
+ * Represents the connection and interface to the Apple APNS servers.
+ *
+ * The service is created by {@link ApnsServiceBuilder} like:
+ *
+ * <pre>
+ *   ApnsService = APNS.newService()
+ *                  .withCert("/path/to/certificate.p12", "MyCertPassword")
+ *                  .withSandboxDestination()
+ *                  .build()
+ * </pre>
+ */
+public interface ApnsService {
+
+    /**
+     * Sends a push notification with the provided {@code payload} to the
+     * iPhone of {@code deviceToken}.
+     *
+     * The payload needs to be a valid JSON object, otherwise it may fail
+     * silently.  It is recommended to use {@link PayloadBuilder} to create
+     * one.
+     *
+     * @param deviceToken   the destination iPhone device token
+     * @param payload       The payload message
+     * @throws NetworkIOException if a network error occurred while
+     *      attempting to send the message
+     */
+    ApnsNotification push(String deviceToken, String payload, String deviceId) throws NetworkIOException;
+
+    EnhancedApnsNotification push(String deviceToken, String payload, Date expiry, String deviceId) throws NetworkIOException;
+
+    /**
+     * Sends a push notification with the provided {@code payload} to the
+     * iPhone of {@code deviceToken}.
+     *
+     * The payload needs to be a valid JSON object, otherwise it may fail
+     * silently.  It is recommended to use {@link PayloadBuilder} to create
+     * one.
+     *
+     * @param deviceToken   the destination iPhone device token
+     * @param payload       The payload message
+     * @throws NetworkIOException if a network error occurred while
+     *      attempting to send the message
+     */
+    ApnsNotification push(byte[] deviceToken, byte[] payload, String deviceId) throws NetworkIOException;
+
+    EnhancedApnsNotification push(byte[] deviceToken, byte[] payload, int expiry, String deviceId) throws NetworkIOException;
+
+    /**
+     * Sends the provided notification {@code message} to the desired
+     * destination.
+     * @throws NetworkIOException if a network error occurred while
+     *      attempting to send the message
+     */
+    void push(ApnsNotification message) throws NetworkIOException;
+
+    /**
+     * Starts the service.
+     *
+     * The underlying implementation may prepare its connections or
+     * data structures to be able to send the messages.
+     *
+     * This method is a blocking call, even if the service represents
+     * a Non-blocking push service.  Once the service is returned, it is ready
+     * to accept push requests.
+     *
+     * @throws NetworkIOException if a network error occurred while
+     *      starting the service
+     */
+    void start();
+
+    /**
+     * Stops the service and frees any allocated resources it created for this
+     * service.
+     *
+     * The underlying implementation should close all connections it created,
+     * and possibly stop any threads as well.
+     */
+    void stop();
+
+    /**
+     * Returns the list of devices that reported failed-delivery
+     * attempts to the Apple Feedback services.
+     *
+     * The result is map, mapping the device tokens as Hex Strings
+     * mapped to the timestamp when APNs determined that the
+     * application no longer exists on the device.
+     * @throws NetworkIOException if a network error occurred
+     *      while retrieving invalid device connection
+     */
+    Map<String, Date> getInactiveDevices() throws NetworkIOException;
+
+    /**
+     * Test that the service is setup properly and the Apple servers
+     * are reachable.
+     *
+     * @throws NetworkIOException   if the Apple servers aren't reachable
+     *      or the service cannot send notifications for now
+     */
+    void testConnection() throws NetworkIOException;
+    
+}

+ 760 - 0
src/main/java/com/notnoop/apns/ApnsServiceBuilder.java

@@ -0,0 +1,760 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+import com.notnoop.apns.internal.ApnsConnection;
+import com.notnoop.apns.internal.ApnsConnectionImpl;
+import com.notnoop.apns.internal.ApnsFeedbackConnection;
+import com.notnoop.apns.internal.ApnsPooledConnection;
+import com.notnoop.apns.internal.ApnsServiceImpl;
+import com.notnoop.apns.internal.BatchApnsService;
+import com.notnoop.apns.internal.QueuedApnsService;
+import com.notnoop.apns.internal.SSLContextBuilder;
+import com.notnoop.apns.internal.Utilities;
+import com.notnoop.exceptions.InvalidSSLConfig;
+import com.notnoop.exceptions.RuntimeIOException;
+
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.SSLSocketFactory;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.Socket;
+import java.security.KeyStore;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+
+import static com.notnoop.apns.internal.Utilities.PRODUCTION_FEEDBACK_HOST;
+import static com.notnoop.apns.internal.Utilities.PRODUCTION_FEEDBACK_PORT;
+import static com.notnoop.apns.internal.Utilities.PRODUCTION_GATEWAY_HOST;
+import static com.notnoop.apns.internal.Utilities.PRODUCTION_GATEWAY_PORT;
+import static com.notnoop.apns.internal.Utilities.SANDBOX_FEEDBACK_HOST;
+import static com.notnoop.apns.internal.Utilities.SANDBOX_FEEDBACK_PORT;
+import static com.notnoop.apns.internal.Utilities.SANDBOX_GATEWAY_HOST;
+import static com.notnoop.apns.internal.Utilities.SANDBOX_GATEWAY_PORT;
+import static java.util.concurrent.Executors.defaultThreadFactory;
+
+/**
+ * The class is used to create instances of {@link ApnsService}.
+ *
+ * Note that this class is not synchronized.  If multiple threads access a
+ * {@code ApnsServiceBuilder} instance concurrently, and at least on of the
+ * threads modifies one of the attributes structurally, it must be
+ * synchronized externally.
+ *
+ * Starting a new {@code ApnsService} is easy:
+ *
+ * <pre>
+ *   ApnsService = APNS.newService()
+ *    .withCert("/path/to/certificate.p12", "MyCertPassword")
+ *    .withSandboxDestination()
+ *    .build()
+ * </pre>
+ */
+public class ApnsServiceBuilder {
+    private static final String KEYSTORE_TYPE = "PKCS12";
+    private static final String KEY_ALGORITHM = ((java.security.Security.getProperty("ssl.KeyManagerFactory.algorithm") == null)? "sunx509" : java.security.Security.getProperty("ssl.KeyManagerFactory.algorithm"));
+
+    private SSLContext sslContext;
+
+    private int readTimeout;
+    private int connectTimeout;
+
+    private String gatewayHost;
+    private int gatewayPort = -1;
+
+    private String feedbackHost;
+    private int feedbackPort;
+    private int pooledMax = 1;
+    private int cacheLength = ApnsConnection.DEFAULT_CACHE_LENGTH;
+    private boolean autoAdjustCacheLength = true;
+    private ExecutorService executor;
+
+    private ReconnectPolicy reconnectPolicy = ReconnectPolicy.Provided.EVERY_HALF_HOUR.newObject();
+    private boolean isQueued;
+    private ThreadFactory queueThreadFactory;
+    
+    private boolean isBatched;
+    private int batchWaitTimeInSec;
+    private int batchMaxWaitTimeInSec;
+    private ScheduledExecutorService batchThreadPoolExecutor;
+    
+    private ApnsDelegate delegate = ApnsDelegate.EMPTY;
+    private Proxy proxy;
+    private String proxyUsername;
+    private String proxyPassword;
+    private boolean errorDetection = true;
+    private ThreadFactory errorDetectionThreadFactory;
+
+    /**
+     * Constructs a new instance of {@code ApnsServiceBuilder}
+     */
+    public ApnsServiceBuilder() { sslContext = null; }
+
+    /**
+     * Specify the certificate used to connect to Apple APNS
+     * servers.  This relies on the path (absolute or relative to
+     * working path) to the keystore (*.p12) containing the
+     * certificate, along with the given password.
+     *
+     * The keystore needs to be of PKCS12 and the keystore
+     * needs to be encrypted using the SunX509 algorithm.  Both
+     * of these settings are the default.
+     *
+     * This library does not support password-less p12 certificates, due to a
+     * Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
+     * Bug 6415637</a>.  There are three workarounds: use a password-protected
+     * certificate, use a different boot Java SDK implementation, or construct
+     * the `SSLContext` yourself!  Needless to say, the password-protected
+     * certificate is most recommended option.
+     *
+     * @param fileName  the path to the certificate
+     * @param password  the password of the keystore
+     * @return  this
+     * @throws RuntimeIOException if it {@code fileName} cannot be
+     *          found or read
+     * @throws InvalidSSLConfig if fileName is invalid Keystore
+     *  or the password is invalid
+     */
+    public ApnsServiceBuilder withCert(String fileName, String password)
+    throws RuntimeIOException, InvalidSSLConfig {
+        FileInputStream stream = null;
+        try {
+            stream = new FileInputStream(fileName);
+            return withCert(stream, password);
+        } catch (FileNotFoundException e) {
+            throw new RuntimeIOException(e);
+        } finally {
+            Utilities.close(stream);
+        }
+    }
+
+    /**
+     * Specify the certificate used to connect to Apple APNS
+     * servers.  This relies on the stream of keystore (*.p12)
+     * containing the certificate, along with the given password.
+     *
+     * The keystore needs to be of PKCS12 and the keystore
+     * needs to be encrypted using the SunX509 algorithm.  Both
+     * of these settings are the default.
+     *
+     * This library does not support password-less p12 certificates, due to a
+     * Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
+     * Bug 6415637</a>.  There are three workarounds: use a password-protected
+     * certificate, use a different boot Java SDK implementation, or constract
+     * the `SSLContext` yourself!  Needless to say, the password-protected
+     * certificate is most recommended option.
+     *
+     * @param stream    the keystore represented as input stream
+     * @param password  the password of the keystore
+     * @return  this
+     * @throws InvalidSSLConfig if stream is invalid Keystore
+     *  or the password is invalid
+     */
+    public ApnsServiceBuilder withCert(InputStream stream, String password)
+    throws InvalidSSLConfig {
+        assertPasswordNotEmpty(password);
+        return withSSLContext(new SSLContextBuilder()
+                .withAlgorithm(KEY_ALGORITHM)
+                .withCertificateKeyStore(stream, password, KEYSTORE_TYPE)
+                .withDefaultTrustKeyStore()
+                .build());
+    }
+
+    /**
+     * Specify the certificate used to connect to Apple APNS
+     * servers.  This relies on a keystore (*.p12)
+     * containing the certificate, along with the given password.
+     *
+     * This library does not support password-less p12 certificates, due to a
+     * Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
+     * Bug 6415637</a>.  There are three workarounds: use a password-protected
+     * certificate, use a different boot Java SDK implementation, or construct
+     * the `SSLContext` yourself!  Needless to say, the password-protected
+     * certificate is most recommended option.
+     *
+     * @param keyStore  the keystore
+     * @param password  the password of the keystore
+     * @return  this
+     * @throws InvalidSSLConfig if stream is invalid Keystore
+     *  or the password is invalid
+     */
+    public ApnsServiceBuilder withCert(KeyStore keyStore, String password)
+    throws InvalidSSLConfig {
+        assertPasswordNotEmpty(password);
+        return withSSLContext(new SSLContextBuilder()
+                .withAlgorithm(KEY_ALGORITHM)
+                .withCertificateKeyStore(keyStore, password)
+                .withDefaultTrustKeyStore()
+                .build());
+    }
+
+    /**
+     * Specify the certificate store used to connect to Apple APNS
+     * servers.  This relies on the stream of keystore (*.p12 | *.jks)
+     * containing the keys and certificates, along with the given
+     * password and alias.
+     *
+     * The keystore can be either PKCS12 or JKS and the keystore
+     * needs to be encrypted using the SunX509 algorithm.
+     *
+     * This library does not support password-less p12 certificates, due to a
+     * Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
+     * Bug 6415637</a>.  There are three workarounds: use a password-protected
+     * certificate, use a different boot Java SDK implementation, or constract
+     * the `SSLContext` yourself!  Needless to say, the password-protected
+     * certificate is most recommended option.
+     *
+     * @param stream    the keystore represented as input stream
+     * @param password  the password of the keystore
+     * @param alias     the alias identifing the key to be used
+     * @return  this
+     * @throws InvalidSSLConfig if stream is an invalid Keystore,
+     *  the password is invalid or the alias is not found
+     */
+    public ApnsServiceBuilder withCert(InputStream stream, String password, String alias)
+            throws InvalidSSLConfig {
+        assertPasswordNotEmpty(password);
+        return withSSLContext(new SSLContextBuilder()
+                .withAlgorithm(KEY_ALGORITHM)
+                .withCertificateKeyStore(stream, password, KEYSTORE_TYPE, alias)
+                .withDefaultTrustKeyStore()
+                .build());
+    }
+
+    /**
+     * Specify the certificate store used to connect to Apple APNS
+     * servers.  This relies on the stream of keystore (*.p12 | *.jks)
+     * containing the keys and certificates, along with the given
+     * password and alias.
+     *
+     * The keystore can be either PKCS12 or JKS and the keystore
+     * needs to be encrypted using the SunX509 algorithm.
+     *
+     * This library does not support password-less p12 certificates, due to a
+     * Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
+     * Bug 6415637</a>.  There are three workarounds: use a password-protected
+     * certificate, use a different boot Java SDK implementation, or constract
+     * the `SSLContext` yourself!  Needless to say, the password-protected
+     * certificate is most recommended option.
+     *
+     * @param keyStore  the keystore
+     * @param password  the password of the keystore
+     * @param alias     the alias identifing the key to be used
+     * @return  this
+     * @throws InvalidSSLConfig if stream is an invalid Keystore,
+     *  the password is invalid or the alias is not found
+     */
+    public ApnsServiceBuilder withCert(KeyStore keyStore, String password, String alias)
+            throws InvalidSSLConfig {
+        assertPasswordNotEmpty(password);
+        return withSSLContext(new SSLContextBuilder()
+                .withAlgorithm(KEY_ALGORITHM)
+                .withCertificateKeyStore(keyStore, password, alias)
+                .withDefaultTrustKeyStore()
+                .build());
+    }
+
+	private void assertPasswordNotEmpty(String password) {
+		if (password == null || password.length() == 0) {
+            throw new IllegalArgumentException("Passwords must be specified." +
+                    "Oracle Java SDK does not support passwordless p12 certificates");
+        }
+	}
+    
+    /**
+     * Specify the SSLContext that should be used to initiate the
+     * connection to Apple Server.
+     *
+     * Most clients would use {@link #withCert(InputStream, String)}
+     * or {@link #withCert(String, String)} instead.  But some
+     * clients may need to represent the Keystore in a different
+     * format than supported.
+     *
+     * @param sslContext    Context to be used to create secure connections
+     * @return  this
+     */
+    public ApnsServiceBuilder withSSLContext(SSLContext sslContext) {
+        this.sslContext = sslContext;
+        return this;
+    }
+    
+    /**
+     * Specify the timeout value to be set in new setSoTimeout in created
+     * sockets, for both feedback and push connections, in milliseconds.
+     * @param readTimeout timeout value to be set in new setSoTimeout
+     * @return this
+     */
+    public ApnsServiceBuilder withReadTimeout(int readTimeout) {
+    	this.readTimeout = readTimeout;
+    	return this;
+    }
+
+    /**
+     * Specify the timeout value to use for connectionTimeout in created
+     * sockets, for both feedback and push connections, in milliseconds.
+     * @param connectTimeout timeout value to use for connectionTimeout
+     * @return this
+     */
+    public ApnsServiceBuilder withConnectTimeout(int connectTimeout) {
+    	this.connectTimeout = connectTimeout;
+    	return this;
+    }
+
+    /**
+     * Specify the gateway server for sending Apple iPhone
+     * notifications.
+     *
+     * Most clients should use {@link #withSandboxDestination()}
+     * or {@link #withProductionDestination()}.  Clients may use
+     * this method to connect to mocking tests and such.
+     *
+     * @param host  hostname the notification gateway of Apple
+     * @param port  port of the notification gateway of Apple
+     * @return  this
+     */
+    public ApnsServiceBuilder withGatewayDestination(String host, int port) {
+        this.gatewayHost = host;
+        this.gatewayPort = port;
+        return this;
+    }
+
+    /**
+     * Specify the Feedback for getting failed devices from
+     * Apple iPhone Push servers.
+     *
+     * Most clients should use {@link #withSandboxDestination()}
+     * or {@link #withProductionDestination()}.  Clients may use
+     * this method to connect to mocking tests and such.
+     *
+     * @param host  hostname of the feedback server of Apple
+     * @param port  port of the feedback server of Apple
+     * @return this
+     */
+    public ApnsServiceBuilder withFeedbackDestination(String host, int port) {
+        this.feedbackHost = host;
+        this.feedbackPort = port;
+        return this;
+    }
+
+    /**
+     * Specify to use Apple servers as iPhone gateway and feedback servers.
+     *
+     * If the passed {@code isProduction} is true, then it connects to the
+     * production servers, otherwise, it connects to the sandbox servers
+     *
+     * @param isProduction  determines which Apple servers should be used:
+     *               production or sandbox
+     * @return this
+     */
+    public ApnsServiceBuilder withAppleDestination(boolean isProduction) {
+        if (isProduction) {
+            return withProductionDestination();
+        } else {
+            return withSandboxDestination();
+        }
+    }
+
+    /**
+     * Specify to use the Apple sandbox servers as iPhone gateway
+     * and feedback servers.
+     *
+     * This is desired when in testing and pushing notifications
+     * with a development provision.
+     *
+     * @return  this
+     */
+    public ApnsServiceBuilder withSandboxDestination() {
+        return withGatewayDestination(SANDBOX_GATEWAY_HOST, SANDBOX_GATEWAY_PORT)
+        .withFeedbackDestination(SANDBOX_FEEDBACK_HOST, SANDBOX_FEEDBACK_PORT);
+    }
+
+    /**
+     * Specify to use the Apple Production servers as iPhone gateway
+     * and feedback servers.
+     *
+     * This is desired when sending notifications to devices with
+     * a production provision (whether through App Store or Ad hoc
+     * distribution).
+     *
+     * @return  this
+     */
+    public ApnsServiceBuilder withProductionDestination() {
+        return withGatewayDestination(PRODUCTION_GATEWAY_HOST, PRODUCTION_GATEWAY_PORT)
+        .withFeedbackDestination(PRODUCTION_FEEDBACK_HOST, PRODUCTION_FEEDBACK_PORT);
+    }
+
+    /**
+     * Specify the reconnection policy for the socket connection.
+     *
+     * Note: This option has no effect when using non-blocking
+     * connections.
+     */
+    public ApnsServiceBuilder withReconnectPolicy(ReconnectPolicy rp) {
+        this.reconnectPolicy = rp;
+        return this;
+    }
+    
+    /**
+     * Specify if the notification cache should auto adjust.
+     * Default is true
+     * 
+     * @param autoAdjustCacheLength the notification cache should auto adjust.
+     * @return this
+     */
+    public ApnsServiceBuilder withAutoAdjustCacheLength(boolean autoAdjustCacheLength) {
+        this.autoAdjustCacheLength = autoAdjustCacheLength;
+        return this;
+    }
+
+    /**
+     * Specify the reconnection policy for the socket connection.
+     *
+     * Note: This option has no effect when using non-blocking
+     * connections.
+     */
+    public ApnsServiceBuilder withReconnectPolicy(ReconnectPolicy.Provided rp) {
+        this.reconnectPolicy = rp.newObject();
+        return this;
+    }
+
+    /**
+     * Specify the address of the SOCKS proxy the connection should
+     * use.
+     *
+     * <p>Read the <a href="http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html">
+     * Java Networking and Proxies</a> guide to understand the
+     * proxies complexity.
+     *
+     * <p>Be aware that this method only handles SOCKS proxies, not
+     * HTTPS proxies.  Use {@link #withProxy(Proxy)} instead.
+     *
+     * @param host  the hostname of the SOCKS proxy
+     * @param port  the port of the SOCKS proxy server
+     * @return  this
+     */
+    public ApnsServiceBuilder withSocksProxy(String host, int port) {
+        Proxy proxy = new Proxy(Proxy.Type.SOCKS,
+                new InetSocketAddress(host, port));
+        return withProxy(proxy);
+    }
+
+    /**
+     * Specify the proxy and the authentication parameters to be used
+     * to establish the connections to Apple Servers.
+     *
+     * <p>Read the <a href="http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html">
+     * Java Networking and Proxies</a> guide to understand the
+     * proxies complexity.
+     *
+     * @param proxy the proxy object to be used to create connections
+     * @param proxyUsername a String object representing the username of the proxy server
+     * @param proxyPassword a String object representing the password of the proxy server
+     * @return  this
+     */
+    public ApnsServiceBuilder withAuthProxy(Proxy proxy, String proxyUsername, String proxyPassword) {
+        this.proxy = proxy;
+        this.proxyUsername = proxyUsername;
+        this.proxyPassword = proxyPassword;
+        return this;
+    }
+    
+    /**
+     * Specify the proxy to be used to establish the connections
+     * to Apple Servers
+     *
+     * <p>Read the <a href="http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html">
+     * Java Networking and Proxies</a> guide to understand the
+     * proxies complexity.
+     *
+     * @param proxy the proxy object to be used to create connections
+     * @return  this
+     */
+    public ApnsServiceBuilder withProxy(Proxy proxy) {
+        this.proxy = proxy;
+        return this;
+    }
+    
+    /**
+     * Specify the number of notifications to cache for error purposes.
+     * Default is 100
+     * 
+     * @param cacheLength  Number of notifications to cache for error purposes
+     * @return  this
+     */
+    public ApnsServiceBuilder withCacheLength(int cacheLength) {
+        this.cacheLength = cacheLength;
+        return this;
+    }
+
+    /**
+     * Specify the socket to be used as underlying socket to connect
+     * to the APN service.
+     *
+     * This assumes that the socket connects to a SOCKS proxy.
+     *
+     * @deprecated use {@link ApnsServiceBuilder#withProxy(Proxy)} instead
+     * @param proxySocket   the underlying socket for connections
+     * @return  this
+     */
+    @Deprecated
+    public ApnsServiceBuilder withProxySocket(Socket proxySocket) {
+        return this.withProxy(new Proxy(Proxy.Type.SOCKS,
+                proxySocket.getRemoteSocketAddress()));
+    }
+
+    /**
+     * Constructs a pool of connections to the notification servers.
+     *
+     * Apple servers recommend using a pooled connection up to
+     * 15 concurrent persistent connections to the gateways.
+     *
+     * Note: This option has no effect when using non-blocking
+     * connections.
+     */
+    public ApnsServiceBuilder asPool(int maxConnections) {
+        return asPool(Executors.newFixedThreadPool(maxConnections), maxConnections);
+    }
+
+    /**
+     * Constructs a pool of connections to the notification servers.
+     *
+     * Apple servers recommend using a pooled connection up to
+     * 15 concurrent persistent connections to the gateways.
+     *
+     * Note: This option has no effect when using non-blocking
+     * connections.
+     *
+     * Note: The maxConnections here is used as a hint to how many connections
+     * get created.
+     */
+    public ApnsServiceBuilder asPool(ExecutorService executor, int maxConnections) {
+        this.pooledMax = maxConnections;
+        this.executor = executor;
+        return this;
+    }
+
+    /**
+     * Constructs a new thread with a processing queue to process
+     * notification requests.
+     *
+     * @return  this
+     */
+    public ApnsServiceBuilder asQueued() {
+        return asQueued(Executors.defaultThreadFactory());
+    }
+    
+    /**
+     * Constructs a new thread with a processing queue to process
+     * notification requests.
+     *
+     * @param threadFactory
+     *            thread factory to use for queue processing
+     * @return  this
+     */
+    public ApnsServiceBuilder asQueued(ThreadFactory threadFactory) {
+        this.isQueued = true;
+        this.queueThreadFactory = threadFactory;
+        return this;
+    }
+    
+    /**
+     * Construct service which will process notification requests in batch.
+     * After each request batch will wait <code>waitTimeInSec (set as 5sec)</code> for more request to come
+     * before executing but not more than <code>maxWaitTimeInSec (set as 10sec)</code>
+     * 
+     * Note: It is not recommended to use pooled connection
+     */
+    public ApnsServiceBuilder asBatched() {
+        return asBatched(5, 10);
+    }
+    
+    /**
+     * Construct service which will process notification requests in batch.
+     * After each request batch will wait <code>waitTimeInSec</code> for more request to come
+     * before executing but not more than <code>maxWaitTimeInSec</code>
+     * 
+     * Note: It is not recommended to use pooled connection
+     * 
+     * @param waitTimeInSec
+     *            time to wait for more notification request before executing
+     *            batch
+     * @param maxWaitTimeInSec
+     *            maximum wait time for batch before executing
+     */
+    public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec) {
+        return asBatched(waitTimeInSec, maxWaitTimeInSec, (ThreadFactory)null);
+    }
+    
+    /**
+     * Construct service which will process notification requests in batch.
+     * After each request batch will wait <code>waitTimeInSec</code> for more request to come
+     * before executing but not more than <code>maxWaitTimeInSec</code>
+     * 
+     * Each batch creates new connection and close it after finished.
+     * In case reconnect policy is specified it will be applied by batch processing. 
+     * E.g.: {@link ReconnectPolicy.Provided#EVERY_HALF_HOUR} will reconnect the connection in case batch is running for more than half an hour
+     * 
+     * Note: It is not recommended to use pooled connection
+     * 
+     * @param waitTimeInSec
+     *            time to wait for more notification request before executing
+     *            batch
+     * @param maxWaitTimeInSec
+     *            maximum wait time for batch before executing
+     * @param threadFactory
+     *            thread factory to use for batch processing
+     */
+    public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec, ThreadFactory threadFactory) {
+        return asBatched(waitTimeInSec, maxWaitTimeInSec, new ScheduledThreadPoolExecutor(1, threadFactory != null ? threadFactory : defaultThreadFactory()));
+    }
+
+    /**
+     * Construct service which will process notification requests in batch.
+     * After each request batch will wait <code>waitTimeInSec</code> for more request to come
+     * before executing but not more than <code>maxWaitTimeInSec</code>
+     * 
+     * Each batch creates new connection and close it after finished.
+     * In case reconnect policy is specified it will be applied by batch processing. 
+     * E.g.: {@link ReconnectPolicy.Provided#EVERY_HALF_HOUR} will reconnect the connection in case batch is running for more than half an hour
+     * 
+     * Note: It is not recommended to use pooled connection
+     * 
+     * @param waitTimeInSec
+     *            time to wait for more notification request before executing
+     *            batch
+     * @param maxWaitTimeInSec
+     *            maximum wait time for batch before executing
+     * @param batchThreadPoolExecutor
+     *            executor for batched processing (may be null)
+     */
+    public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec, ScheduledExecutorService batchThreadPoolExecutor) {
+        this.isBatched = true;
+        this.batchWaitTimeInSec = waitTimeInSec;
+        this.batchMaxWaitTimeInSec = maxWaitTimeInSec;
+        this.batchThreadPoolExecutor = batchThreadPoolExecutor;
+        return this;
+    }
+
+    /**
+     * Sets the delegate of the service, that gets notified of the
+     * status of message delivery.
+     *
+     * Note: This option has no effect when using non-blocking
+     * connections.
+     */
+    public ApnsServiceBuilder withDelegate(ApnsDelegate delegate) {
+        this.delegate = delegate == null ? ApnsDelegate.EMPTY : delegate;
+        return this;
+    }
+
+    /**
+     * Disables the enhanced error detection, enabled by the
+     * enhanced push notification interface.  Error detection is
+     * enabled by default.
+     *
+     * This setting is desired when the application shouldn't spawn
+     * new threads.
+     *
+     * @return  this
+     */
+    public ApnsServiceBuilder withNoErrorDetection() {
+        this.errorDetection = false;
+        return this;
+    }
+
+    /**
+     * Provide a custom source for threads used for monitoring connections.
+     *
+     * This setting is desired when the application must obtain threads from a
+     * controlled environment Google App Engine. 
+     * @param threadFactory
+     *            thread factory to use for error detection
+     * @return  this
+     */
+    public ApnsServiceBuilder withErrorDetectionThreadFactory(ThreadFactory threadFactory) {
+        this.errorDetectionThreadFactory = threadFactory;
+        return this;
+    }
+
+    /**
+     * Returns a fully initialized instance of {@link ApnsService},
+     * according to the requested settings.
+     *
+     * @return  a new instance of ApnsService
+     */
+    public ApnsService build() {
+        checkInitialization();
+        ApnsService service;
+
+        SSLSocketFactory sslFactory = sslContext.getSocketFactory();
+        ApnsFeedbackConnection feedback = new ApnsFeedbackConnection(sslFactory, feedbackHost, feedbackPort, proxy, readTimeout, connectTimeout, proxyUsername, proxyPassword);
+
+        ApnsConnection conn = new ApnsConnectionImpl(sslFactory, gatewayHost,
+            gatewayPort, proxy, proxyUsername, proxyPassword, reconnectPolicy,
+                delegate, errorDetection, errorDetectionThreadFactory, cacheLength,
+                autoAdjustCacheLength, readTimeout, connectTimeout);
+        if (pooledMax != 1) {
+            conn = new ApnsPooledConnection(conn, pooledMax, executor);
+        }
+
+        service = new ApnsServiceImpl(conn, feedback);
+
+        if (isQueued) {
+            service = new QueuedApnsService(service, queueThreadFactory);
+        }
+        
+        if (isBatched) {
+            service = new BatchApnsService(conn, feedback, batchWaitTimeInSec, batchMaxWaitTimeInSec, batchThreadPoolExecutor);
+        }
+
+        service.start();
+
+        return service;
+    }
+
+    private void checkInitialization() {
+        if (sslContext == null)
+            throw new IllegalStateException(
+                    "SSL Certificates and attribute are not initialized\n"
+                    + "Use .withCert() methods.");
+        if (gatewayHost == null || gatewayPort == -1)
+            throw new IllegalStateException(
+                    "The Destination APNS server is not stated\n"
+                    + "Use .withDestination(), withSandboxDestination(), "
+                    + "or withProductionDestination().");
+    }
+}

+ 81 - 0
src/main/java/com/notnoop/apns/DeliveryError.java

@@ -0,0 +1,81 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+/**
+ * Errors in delivery that may get reported by Apple APN servers
+ */
+public enum DeliveryError {
+    /**
+     * Connection closed without any error.
+     *
+     * This may occur if the APN service faces an invalid simple
+     * APNS notification while running in enhanced mode
+     */
+    NO_ERROR(0),
+    PROCESSING_ERROR(1),
+    MISSING_DEVICE_TOKEN(2),
+    MISSING_TOPIC(3),
+    MISSING_PAYLOAD(4),
+    INVALID_TOKEN_SIZE(5),
+    INVALID_TOPIC_SIZE(6),
+    INVALID_PAYLOAD_SIZE(7),
+    INVALID_TOKEN(8),
+
+    NONE(255),
+    UNKNOWN(254);
+
+    private final byte code;
+    DeliveryError(int code) {
+        this.code = (byte)code;
+    }
+
+    /** The status code as specified by Apple */
+    public int code() {
+        return code;
+    }
+
+    /**
+     * Returns the appropriate {@code DeliveryError} enum
+     * corresponding to the Apple provided status code
+     *
+     * @param code  status code provided by Apple
+     * @return  the appropriate DeliveryError
+     */
+    public static DeliveryError ofCode(int code) {
+        for (DeliveryError e : DeliveryError.values()) {
+            if (e.code == code)
+                return e;
+        }
+
+        return UNKNOWN;
+    }
+}

+ 191 - 0
src/main/java/com/notnoop/apns/EnhancedApnsNotification.java

@@ -0,0 +1,191 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+import java.util.Arrays;
+import java.util.concurrent.atomic.AtomicInteger;
+import com.notnoop.apns.internal.Utilities;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Represents an APNS notification to be sent to Apple service.
+ */
+public class EnhancedApnsNotification implements ApnsNotification {
+	
+    private static final Logger LOGGER = LoggerFactory.getLogger(EnhancedApnsNotification.class);
+    private final static byte COMMAND = 1;
+    private static AtomicInteger nextId = new AtomicInteger(0);
+    private final int identifier;
+    private final int expiry;
+    private final byte[] deviceToken;
+    private final byte[] payload;
+    private String deviceId;
+
+    public void setDeviceId(String deviceId) {
+        this.deviceId = deviceId;
+    }
+
+    public static int INCREMENT_ID() {
+        return nextId.incrementAndGet();
+    }
+    
+    /**
+     * The infinite future for the purposes of Apple expiry date
+     */
+    public final static int MAXIMUM_EXPIRY = Integer.MAX_VALUE;
+
+    /**
+     * Constructs an instance of {@code ApnsNotification}.
+     *
+     * The message encodes the payload with a {@code UTF-8} encoding.
+     *
+     * @param dtoken    The Hex of the device token of the destination phone
+     * @param payload   The payload message to be sent
+     */
+    public EnhancedApnsNotification(
+            int identifier, int expiryTime,
+            String dtoken, String payload) {
+        this.identifier = identifier;
+        this.expiry = expiryTime;
+        this.deviceToken = Utilities.decodeHex(dtoken);
+        this.payload = Utilities.toUTF8Bytes(payload);
+    }
+
+    /**
+     * Constructs an instance of {@code ApnsNotification}.
+     *
+     * @param dtoken    The binary representation of the destination device token
+     * @param payload   The binary representation of the payload to be sent
+     */
+    public EnhancedApnsNotification(
+            int identifier, int expiryTime,
+            byte[] dtoken, byte[] payload) {
+        this.identifier = identifier;
+        this.expiry = expiryTime;
+        this.deviceToken = Utilities.copyOf(dtoken);
+        this.payload = Utilities.copyOf(payload);
+    }
+
+    /**
+     * Returns the binary representation of the device token.
+     *
+     */
+    public byte[] getDeviceToken() {
+        return Utilities.copyOf(deviceToken);
+    }
+
+    /**
+     * Returns the binary representation of the payload.
+     *
+     */
+    public byte[] getPayload() {
+        return Utilities.copyOf(payload);
+    }
+
+    public int getIdentifier() {
+        return identifier;
+    }
+
+    public int getExpiry() {
+        return expiry;
+    }
+
+    private byte[] marshall;
+    /**
+     * Returns the binary representation of the message as expected by the
+     * APNS server.
+     *
+     * The returned array can be used to sent directly to the APNS server
+     * (on the wire/socket) without any modification.
+     */
+    public byte[] marshall() {
+        if (marshall == null) {
+            marshall = Utilities.marshallEnhanced(COMMAND, identifier,
+                    expiry, deviceToken, payload);
+        }
+        return marshall.clone();
+    }
+
+    @Override
+    public String getDeviceId() {
+        return deviceId;
+    }
+
+    /**
+     * Returns the length of the message in bytes as it is encoded on the wire.
+     *
+     * Apple require the message to be of length 255 bytes or less.
+     *
+     * @return length of encoded message in bytes
+     */
+    public int length() {
+        int length = 1 + 4 + 4 + 2 + deviceToken.length + 2 + payload.length;
+        final int marshalledLength = marshall().length;
+        assert marshalledLength == length;
+        return length;
+    }
+
+    @Override
+    public int hashCode() {
+        return (21
+               + 31 * identifier
+               + 31 * expiry
+               + 31 * Arrays.hashCode(deviceToken)
+               + 31 * Arrays.hashCode(payload));
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof EnhancedApnsNotification))
+            return false;
+        EnhancedApnsNotification o = (EnhancedApnsNotification)obj;
+        return (identifier == o.identifier
+                && expiry == o.expiry
+                && Arrays.equals(this.deviceToken, o.deviceToken)
+                && Arrays.equals(this.payload, o.payload));
+    }
+
+    @Override
+    @SuppressFBWarnings("DE_MIGHT_IGNORE")
+    public String toString() {
+        String payloadString;
+        try {
+            payloadString = new String(payload, "UTF-8");
+        } catch (UnsupportedEncodingException ex) {
+            LOGGER.debug("UTF-8 charset not found on the JRE", ex);
+            payloadString = "???";
+        }
+        return "Message(Id="+identifier+"; Token="+Utilities.encodeHex(deviceToken)+"; Payload="+payloadString+")";
+    }
+}

+ 535 - 0
src/main/java/com/notnoop/apns/PayloadBuilder.java

@@ -0,0 +1,535 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.HashMap;
+import java.util.Map;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.notnoop.apns.internal.Utilities;
+
+/**
+ * Represents a builder for constructing Payload requests, as
+ * specified by Apple Push Notification Programming Guide.
+ */
+public final class PayloadBuilder {
+    private static final ObjectMapper mapper = new ObjectMapper();
+
+    private final Map<String, Object> root;
+    private final Map<String, Object> aps;
+    private final Map<String, Object> customAlert;
+
+    /**
+     * Constructs a new instance of {@code PayloadBuilder}
+     */
+    PayloadBuilder() {
+        root = new HashMap<String, Object>();
+        aps = new HashMap<String, Object>();
+        customAlert = new HashMap<String, Object>();
+    }
+
+    /**
+     * Sets the alert body text, the text the appears to the user,
+     * to the passed value
+     *
+     * @param alert the text to appear to the user
+     * @return  this
+     */
+    public PayloadBuilder alertBody(final String alert) {
+        customAlert.put("body", alert);
+        return this;
+    }
+
+    /**
+     * Sets the alert title text, the text the appears to the user,
+     * to the passed value.
+     *
+     * Used on iOS 8.2, iWatch and also Safari
+     *
+     * @param title the text to appear to the user
+     * @return  this
+     */
+    public PayloadBuilder alertTitle(final String title) {
+        customAlert.put("title", title);
+        return this;
+    }
+
+    /**
+     * The key to a title string in the Localizable.strings file for the current localization.
+     *
+     * @param key  the localizable message title key
+     * @return  this
+     */
+    public PayloadBuilder localizedTitleKey(final String key) {
+        customAlert.put("title-loc-key", key);
+        return this;
+    }
+
+    /**
+     * Sets the arguments for the localizable title key.
+     *
+     * @param arguments the arguments to the localized alert message
+     * @return  this
+     */
+    public PayloadBuilder localizedTitleArguments(final Collection<String> arguments) {
+        customAlert.put("title-loc-args", arguments);
+        return this;
+    }
+
+    /**
+     * Sets the arguments for the localizable title key.
+     *
+     * @param arguments the arguments to the localized alert message
+     * @return  this
+     */
+    public PayloadBuilder localizedTitleArguments(final String... arguments) {
+        return localizedTitleArguments(Arrays.asList(arguments));
+    }
+
+    /**
+     * Sets the alert action text
+     *
+     * @param action The label of the action button
+     * @return  this
+     */
+    public PayloadBuilder alertAction(final String action) {
+        customAlert.put("action", action);
+        return this;
+    }
+
+    /**
+     * Sets the "url-args" key that are paired with the placeholders
+     * inside the urlFormatString value of your website.json file.
+     * The order of the placeholders in the URL format string determines
+     * the order of the values supplied by the url-args array.
+     *
+     * @param urlArgs the values to be paired with the placeholders inside
+     *                the urlFormatString value of your website.json file.
+     * @return  this
+     */
+    public PayloadBuilder urlArgs(final String... urlArgs){
+        aps.put("url-args", urlArgs);
+        return this;
+    }
+
+    /**
+     * Sets the alert sound to be played.
+     *
+     * Passing {@code null} disables the notification sound.
+     *
+     * @param sound the file name or song name to be played
+     *              when receiving the notification
+     * @return  this
+     */
+    public PayloadBuilder sound(final String sound) {
+        if (sound != null) {
+            aps.put("sound", sound);
+        } else {
+            aps.remove("sound");
+        }
+        return this;
+    }
+
+    /**
+     * Sets the category of the notification for iOS8 notification
+     * actions.  See 13 minutes into "What's new in iOS Notifications"
+     *
+     * Passing {@code null} removes the category.
+     *
+     * @param category the name of the category supplied to the app
+     *              when receiving the notification
+     * @return  this
+     */
+    public PayloadBuilder category(final String category) {
+        if (category != null) {
+            aps.put("category", category);
+        } else {
+            aps.remove("category");
+        }
+        return this;
+    }
+
+    /**
+     * Sets the notification badge to be displayed next to the
+     * application icon.
+     *
+     * The passed value is the value that should be displayed
+     * (it will be added to the previous badge number), and
+     * a badge of 0 clears the badge indicator.
+     *
+     * @param badge the badge number to be displayed
+     * @return  this
+     */
+    public PayloadBuilder badge(final int badge) {
+        aps.put("badge", badge);
+        return this;
+    }
+
+    /**
+     * Requests clearing of the badge number next to the application
+     * icon.
+     *
+     * This is an alias to {@code badge(0)}.
+     *
+     * @return this
+     */
+    public PayloadBuilder clearBadge() {
+        return badge(0);
+    }
+
+    /**
+     * Sets the value of action button (the right button to be
+     * displayed).  The default value is "View".
+     *
+     * The value can be either the simple String to be displayed or
+     * a localizable key, and the iPhone will show the appropriate
+     * localized message.
+     *
+     * A {@code null} actionKey indicates no additional button
+     * is displayed, just the Cancel button.
+     *
+     * @param actionKey the title of the additional button
+     * @return  this
+     */
+    public PayloadBuilder actionKey(final String actionKey) {
+        customAlert.put("action-loc-key", actionKey);
+        return this;
+    }
+
+    /**
+     * Set the notification view to display an action button.
+     *
+     * This is an alias to {@code actionKey(null)}
+     *
+     * @return this
+     */
+    public PayloadBuilder noActionButton() {
+        return actionKey(null);
+    }
+
+    /**
+     * Sets the notification type to be a 'newstand' notification.
+     *
+     * A Newstand Notification targets the Newstands app so that the app
+     * updates the subscription info and content.
+     *
+     * @return this
+     */
+    public PayloadBuilder forNewsstand() {
+        aps.put("content-available", 1);
+        return this;
+    }
+
+    /**
+     * With iOS7 it is possible to have the application wake up before the user opens the app.
+     * 
+     * The same key-word can also be used to send 'silent' notifications. With these 'silent' notification 
+     * a different app delegate is being invoked, allowing the app to perform background tasks.
+     *
+     * @return this
+     */
+    public PayloadBuilder instantDeliveryOrSilentNotification() {
+        aps.put("content-available", 1);
+        return this;
+    }
+
+    /**
+     * Set the notification localized key for the alert body
+     * message.
+     *
+     * @param key   the localizable message body key
+     * @return  this
+     */
+    public PayloadBuilder localizedKey(final String key) {
+        customAlert.put("loc-key", key);
+        return this;
+    }
+
+    /**
+     * Sets the arguments for the alert message localizable message.
+     *
+     * The iPhone doesn't localize the arguments.
+     *
+     * @param arguments the arguments to the localized alert message
+     * @return  this
+     */
+    public PayloadBuilder localizedArguments(final Collection<String> arguments) {
+        customAlert.put("loc-args", arguments);
+        return this;
+    }
+
+    /**
+     * Sets the arguments for the alert message localizable message.
+     *
+     * The iPhone doesn't localize the arguments.
+     *
+     * @param arguments the arguments to the localized alert message
+     * @return  this
+     */
+    public PayloadBuilder localizedArguments(final String... arguments) {
+        return localizedArguments(Arrays.asList(arguments));
+    }
+
+    /**
+     * Sets the launch image file for the push notification
+     *
+     * @param launchImage   the filename of the image file in the
+     *      application bundle.
+     * @return  this
+     */
+    public PayloadBuilder launchImage(final String launchImage) {
+        customAlert.put("launch-image", launchImage);
+        return this;
+    }
+
+    /**
+     * Sets any application-specific custom fields.  The values
+     * are presented to the application and the iPhone doesn't
+     * display them automatically.
+     *
+     * This can be used to pass specific values (urls, ids, etc) to
+     * the application in addition to the notification message
+     * itself.
+     *
+     * @param key   the custom field name
+     * @param value the custom field value
+     * @return  this
+     */
+    public PayloadBuilder customField(final String key, final Object value) {
+        root.put(key, value);
+        return this;
+    }
+
+    public PayloadBuilder mdm(final String s) {
+        return customField("mdm", s);
+    }
+
+    /**
+     * Set any application-specific custom fields.  These values
+     * are presented to the application and the iPhone doesn't
+     * display them automatically.
+     *
+     * This method *adds* the custom fields in the map to the
+     * payload, and subsequent calls add but doesn't reset the
+     * custom fields.
+     *
+     * @param values   the custom map
+     * @return  this
+     */
+    public PayloadBuilder customFields(final Map<String, ?> values) {
+        root.putAll(values);
+        return this;
+    }
+
+    /**
+     * Returns the length of payload bytes once marshaled to bytes
+     *
+     * @return the length of the payload
+     */
+    public int length() {
+        return copy().buildBytes().length;
+    }
+
+    /**
+     * Returns true if the payload built so far is larger than
+     * the size permitted by Apple (which is 2048 bytes).
+     *
+     * @return true if the result payload is too long
+     */
+    public boolean isTooLong() {
+        return length() > Utilities.MAX_PAYLOAD_LENGTH;
+    }
+
+    /**
+     * Shrinks the alert message body so that the resulting payload
+     * message fits within the passed expected payload length.
+     *
+     * This method performs best-effort approach, and its behavior
+     * is unspecified when handling alerts where the payload
+     * without body is already longer than the permitted size, or
+     * if the break occurs within word.
+     *
+     * @param payloadLength the expected max size of the payload
+     * @return  this
+     */
+    public PayloadBuilder resizeAlertBody(final int payloadLength) {
+        return resizeAlertBody(payloadLength, "");
+    }
+
+    /**
+     * Shrinks the alert message body so that the resulting payload
+     * message fits within the passed expected payload length.
+     *
+     * This method performs best-effort approach, and its behavior
+     * is unspecified when handling alerts where the payload
+     * without body is already longer than the permitted size, or
+     * if the break occurs within word.
+     *
+     * @param payloadLength the expected max size of the payload
+     * @param postfix for the truncated body, e.g. "..."
+     * @return  this
+     */
+    public PayloadBuilder resizeAlertBody(final int payloadLength, final String postfix) {
+        int currLength = length();
+        if (currLength <= payloadLength) {
+            return this;
+        }
+
+        // now we are sure that truncation is required
+        String body = (String)customAlert.get("body");
+
+        final int acceptableSize = Utilities.toUTF8Bytes(body).length
+                - (currLength - payloadLength
+                        + Utilities.toUTF8Bytes(postfix).length);
+        body = Utilities.truncateWhenUTF8(body, acceptableSize) + postfix;
+
+        // set it back
+        customAlert.put("body", body);
+
+        // calculate the length again
+        currLength = length();
+
+        if(currLength > payloadLength) {
+            // string is still too long, just remove the body as the body is
+            // anyway not the cause OR the postfix might be too long
+            customAlert.remove("body");
+        }
+
+        return this;
+    }
+
+    /**
+     * Shrinks the alert message body so that the resulting payload
+     * message fits within require Apple specification (2048 bytes).
+     *
+     * This method performs best-effort approach, and its behavior
+     * is unspecified when handling alerts where the payload
+     * without body is already longer than the permitted size, or
+     * if the break occurs within word.
+     *
+     * @return  this
+     */
+    public PayloadBuilder shrinkBody() {
+        return shrinkBody("");
+    }
+
+    /**
+     * Shrinks the alert message body so that the resulting payload
+     * message fits within require Apple specification (2048 bytes).
+     *
+     * This method performs best-effort approach, and its behavior
+     * is unspecified when handling alerts where the payload
+     * without body is already longer than the permitted size, or
+     * if the break occurs within word.
+     *
+     * @param postfix for the truncated body, e.g. "..."
+     *
+     * @return  this
+     */
+    public PayloadBuilder shrinkBody(final String postfix) {
+        return resizeAlertBody(Utilities.MAX_PAYLOAD_LENGTH, postfix);
+    }
+
+    /**
+     * Returns the JSON String representation of the payload
+     * according to Apple APNS specification
+     *
+     * @return  the String representation as expected by Apple
+     */
+    public String build() {
+        if (!root.containsKey("mdm")) {
+            insertCustomAlert();
+            root.put("aps", aps);
+        }
+        try {
+            return mapper.writeValueAsString(root);
+        } catch (final Exception e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    private void insertCustomAlert() {
+        switch (customAlert.size()) {
+            case 0:
+                aps.remove("alert");
+                break;
+            case 1:
+                if (customAlert.containsKey("body")) {
+                    aps.put("alert", customAlert.get("body"));
+                    break;
+                }
+                // else follow through
+                //$FALL-THROUGH$
+            default:
+                aps.put("alert", customAlert);
+        }
+    }
+
+    /**
+     * Returns the bytes representation of the payload according to
+     * Apple APNS specification
+     *
+     * @return the bytes as expected by Apple
+     */
+    public byte[] buildBytes() {
+        return Utilities.toUTF8Bytes(build());
+    }
+
+    @Override
+    public String toString() {
+        return build();
+    }
+
+    private PayloadBuilder(final Map<String, Object> root,
+            final Map<String, Object> aps,
+            final Map<String, Object> customAlert) {
+        this.root = new HashMap<String, Object>(root);
+        this.aps = new HashMap<String, Object>(aps);
+        this.customAlert = new HashMap<String, Object>(customAlert);
+    }
+
+    /**
+     * Returns a copy of this builder
+     *
+     * @return a copy of this builder
+     */
+    public PayloadBuilder copy() {
+        return new PayloadBuilder(root, aps, customAlert);
+    }
+
+    /**
+     * @return a new instance of Payload Builder
+     */
+    public static PayloadBuilder newPayload() {
+        return new PayloadBuilder();
+    }
+}

+ 118 - 0
src/main/java/com/notnoop/apns/ReconnectPolicy.java

@@ -0,0 +1,118 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+import com.notnoop.apns.internal.ReconnectPolicies;
+
+/**
+ * Represents the reconnection policy for the library.
+ *
+ * Each object should be used exclusively for one
+ * {@code ApnsService} only.
+ */
+public interface ReconnectPolicy {
+    /**
+     * Returns {@code true} if the library should initiate a new
+     * connection for sending the message.
+     *
+     * The library calls this method at every message push.
+     *
+     * @return true if the library should be reconnected
+     */
+    public boolean shouldReconnect();
+
+    /**
+     * Callback method to be called whenever the library
+     * makes a new connection
+     */
+    public void reconnected();
+
+    /**
+     * Returns a deep copy of this reconnection policy, if needed.
+     *
+     * Subclasses may return this instance if the object is immutable.
+     */
+    public ReconnectPolicy copy();
+
+    /**
+     * Types of the library provided reconnection policies.
+     *
+     * This should capture most of the commonly used cases.
+     */
+    public enum Provided {
+        /**
+         * Only reconnect if absolutely needed, e.g. when the connection is dropped.
+         * <p>
+         * Apple recommends using a persistent connection.  This improves the latency of sending push notification messages.
+         * <p>
+         * The down-side is that once the connection is closed ungracefully (e.g. because Apple server drops it), the library wouldn't
+         * detect such failure and not warn against the messages sent after the drop before the detection.
+         */
+        NEVER {
+            @Override
+            public ReconnectPolicy newObject() {
+                return new ReconnectPolicies.Never();
+            }
+        },
+
+        /**
+         * Makes a new connection if the current connection has lasted for more than half an hour.
+         * <p>
+         * This is the recommended mode.
+         * <p>
+         * This is the sweat-spot in my experiments between dropped connections while minimizing latency.
+         */
+        EVERY_HALF_HOUR {
+            @Override
+            public ReconnectPolicy newObject() {
+                return new ReconnectPolicies.EveryHalfHour();
+            }
+        },
+
+        /**
+         * Makes a new connection for every message being sent.
+         *
+         * This option ensures that each message is actually
+         * delivered to Apple.
+         *
+         * If you send <strong>a lot</strong> of messages though,
+         * Apple may consider your requests to be a DoS attack.
+         */
+        EVERY_NOTIFICATION {
+            @Override
+            public ReconnectPolicy newObject() {
+                return new ReconnectPolicies.Always();
+            }
+        };
+
+        abstract ReconnectPolicy newObject();
+    }
+}

+ 172 - 0
src/main/java/com/notnoop/apns/SimpleApnsNotification.java

@@ -0,0 +1,172 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+import java.util.Arrays;
+
+import com.notnoop.apns.internal.Utilities;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Represents an APNS notification to be sent to Apple service. This is for legacy use only
+ * and should not be used in new development.
+ * https://developer.apple.com/library/ios/documentation/NetworkingInternet/Conceptual/RemoteNotificationsPG/Chapters/LegacyFormat.html
+ *
+ * This SimpleApnsNotification also only has limited error handling (by the APNS closing the connection
+ * when a bad message was received) This prevents us from location the malformed notification.
+ *
+ * As push messages sent after a malformed notification are discarded by APNS messages will get lost
+ * and not be delivered with the SimpleApnsNotification.
+ *
+ * @deprecated use EnhancedApnsNotification instead.
+ */
+@SuppressWarnings("deprecation")
+@Deprecated
+public class SimpleApnsNotification implements ApnsNotification {
+	
+    private static final Logger LOGGER = LoggerFactory.getLogger(SimpleApnsNotification.class);
+    private final static byte COMMAND = 0;
+    private final byte[] deviceToken;
+    private final byte[] payload;
+
+    /**
+     * Constructs an instance of {@code ApnsNotification}.
+     *
+     * The message encodes the payload with a {@code UTF-8} encoding.
+     *
+     * @param dtoken    The Hex of the device token of the destination phone
+     * @param payload   The payload message to be sent
+     */
+    public SimpleApnsNotification(String dtoken, String payload) {
+        this.deviceToken = Utilities.decodeHex(dtoken);
+        this.payload = Utilities.toUTF8Bytes(payload);
+    }
+
+    /**
+     * Constructs an instance of {@code ApnsNotification}.
+     *
+     * @param dtoken    The binary representation of the destination device token
+     * @param payload   The binary representation of the payload to be sent
+     */
+    public SimpleApnsNotification(byte[] dtoken, byte[] payload) {
+        this.deviceToken = Utilities.copyOf(dtoken);
+        this.payload = Utilities.copyOf(payload);
+    }
+
+    /**
+     * Returns the binary representation of the device token.
+     *
+     */
+    public byte[] getDeviceToken() {
+        return Utilities.copyOf(deviceToken);
+    }
+
+    /**
+     * Returns the binary representation of the payload.
+     *
+     */
+    public byte[] getPayload() {
+        return Utilities.copyOf(payload);
+    }
+
+    private byte[] marshall;
+    /**
+     * Returns the binary representation of the message as expected by the
+     * APNS server.
+     *
+     * The returned array can be used to sent directly to the APNS server
+     * (on the wire/socket) without any modification.
+     */
+    public byte[] marshall() {
+        if (marshall == null)
+            marshall = Utilities.marshall(COMMAND, deviceToken, payload);
+        return marshall.clone();
+    }
+
+    /**
+     * Returns the length of the message in bytes as it is encoded on the wire.
+     *
+     * Apple require the message to be of length 255 bytes or less.
+     *
+     * @return length of encoded message in bytes
+     */
+    public int length() {
+        int length = 1 + 2 + deviceToken.length + 2 + payload.length;
+        final int marshalledLength = marshall().length;
+        assert marshalledLength == length;
+        return length;
+    }
+
+    @Override
+    public int hashCode() {
+        return 21
+               + 31 * Arrays.hashCode(deviceToken)
+               + 31 * Arrays.hashCode(payload);
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+        if (!(obj instanceof SimpleApnsNotification))
+            return false;
+        SimpleApnsNotification o = (SimpleApnsNotification)obj;
+        return Arrays.equals(this.deviceToken, o.deviceToken)
+                && Arrays.equals(this.payload, o.payload);
+    }
+
+    public int getIdentifier() {
+        return -1;
+    }
+
+    public int getExpiry() {
+        return -1;
+    }
+    
+    @Override
+    @SuppressFBWarnings("DE_MIGHT_IGNORE")
+    public String toString() {
+        String payloadString;
+        try {
+            payloadString = new String(payload, "UTF-8");
+        } catch (UnsupportedEncodingException ex) {
+            LOGGER.debug("UTF-8 charset not found on the JRE", ex);
+            payloadString = "???";
+        }
+        return "Message(Token="+Utilities.encodeHex(deviceToken)+"; Payload="+payloadString+")";
+    }
+
+    @Override
+    public String getDeviceId() {
+        return null;
+    }
+}

+ 47 - 0
src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java

@@ -0,0 +1,47 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns;
+
+/**
+ * A delegate that also gets notified just before a notification is being delivered to the
+ * Apple Server.
+ */
+public interface StartSendingApnsDelegate extends ApnsDelegate {
+
+    /**
+     * Called when message is about to be sent to the Apple servers.
+     *
+     * @param message the notification that is about to be sent
+     * @param resent whether the notification is being resent after an error
+     */
+    public void startSending(ApnsNotification message, boolean resent);
+
+}

+ 90 - 0
src/main/java/com/notnoop/apns/internal/AbstractApnsService.java

@@ -0,0 +1,90 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import com.notnoop.apns.ApnsNotification;
+import com.notnoop.apns.ApnsService;
+import com.notnoop.apns.EnhancedApnsNotification;
+import com.notnoop.exceptions.NetworkIOException;
+
+abstract class AbstractApnsService implements ApnsService {
+    private ApnsFeedbackConnection feedback;
+    private AtomicInteger c = new AtomicInteger();
+
+    public AbstractApnsService(ApnsFeedbackConnection feedback) {
+        this.feedback = feedback;
+    }
+
+    public EnhancedApnsNotification push(String deviceToken, String payload, String deviceId) throws NetworkIOException {
+        EnhancedApnsNotification notification =
+            new EnhancedApnsNotification(c.incrementAndGet(), EnhancedApnsNotification.MAXIMUM_EXPIRY, deviceToken, payload);
+        notification.setDeviceId(deviceId);
+        push(notification);
+        return notification;
+    }
+
+    public EnhancedApnsNotification push(String deviceToken, String payload, Date expiry, String deviceId) throws NetworkIOException {
+        EnhancedApnsNotification notification =
+            new EnhancedApnsNotification(c.incrementAndGet(), (int)(expiry.getTime() / 1000), deviceToken, payload);
+        notification.setDeviceId(deviceId);
+        push(notification);
+        return notification;
+    }
+
+    public EnhancedApnsNotification push(byte[] deviceToken, byte[] payload, String deviceId) throws NetworkIOException {
+        EnhancedApnsNotification notification =
+            new EnhancedApnsNotification(c.incrementAndGet(), EnhancedApnsNotification.MAXIMUM_EXPIRY, deviceToken, payload);
+        notification.setDeviceId(deviceId);
+        push(notification);
+        return notification;
+    }
+
+    public EnhancedApnsNotification push(byte[] deviceToken, byte[] payload, int expiry, String deviceId) throws NetworkIOException {
+        EnhancedApnsNotification notification =
+            new EnhancedApnsNotification(c.incrementAndGet(), expiry, deviceToken, payload);
+        notification.setDeviceId(deviceId);
+        push(notification);
+        return notification;
+    }
+
+    public abstract void push(ApnsNotification message) throws NetworkIOException;
+
+    public Map<String, Date> getInactiveDevices() throws NetworkIOException {
+        return feedback.getInactiveDevices();
+    }
+}

+ 52 - 0
src/main/java/com/notnoop/apns/internal/ApnsConnection.java

@@ -0,0 +1,52 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import java.io.Closeable;
+
+import com.notnoop.apns.ApnsNotification;
+import com.notnoop.exceptions.NetworkIOException;
+
+public interface ApnsConnection extends Closeable {
+
+    //Default number of notifications to keep for error purposes
+    public static final int DEFAULT_CACHE_LENGTH = 100;
+
+    void sendMessage(ApnsNotification m) throws NetworkIOException;
+
+    void testConnection() throws NetworkIOException;
+
+    ApnsConnection copy();
+    
+    void setCacheLength(int cacheLength);
+    
+    int getCacheLength();
+}

+ 412 - 0
src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java

@@ -0,0 +1,412 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.Socket;
+import java.util.LinkedList;
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.Executors;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.atomic.AtomicInteger;
+
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLHandshakeException;
+import javax.net.ssl.SSLSocketFactory;
+import com.notnoop.apns.ApnsDelegate;
+import com.notnoop.apns.StartSendingApnsDelegate;
+import com.notnoop.apns.ApnsNotification;
+import com.notnoop.apns.DeliveryError;
+import com.notnoop.apns.EnhancedApnsNotification;
+import com.notnoop.apns.ReconnectPolicy;
+import com.notnoop.exceptions.ApnsDeliveryErrorException;
+import com.notnoop.exceptions.NetworkIOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ApnsConnectionImpl implements ApnsConnection {
+
+    private static final Logger logger = LoggerFactory.getLogger(ApnsConnectionImpl.class);
+
+    private final SocketFactory factory;
+    private final String host;
+    private final int port;
+    private final int readTimeout;
+    private final int connectTimeout;
+    private final Proxy proxy;
+    private final String proxyUsername;
+    private final String proxyPassword;
+    private final ReconnectPolicy reconnectPolicy;
+    private final ApnsDelegate delegate;
+    private int cacheLength;
+    private final boolean errorDetection;
+    private final ThreadFactory threadFactory;
+    private final boolean autoAdjustCacheLength;
+    private final ConcurrentLinkedQueue<ApnsNotification> cachedNotifications, notificationsBuffer;
+    private Socket socket;
+    private final AtomicInteger threadId = new AtomicInteger(0);
+
+    public ApnsConnectionImpl(SocketFactory factory, String host, int port) {
+        this(factory, host, port, new ReconnectPolicies.Never(), ApnsDelegate.EMPTY);
+    }
+
+    private ApnsConnectionImpl(SocketFactory factory, String host, int port, ReconnectPolicy reconnectPolicy, ApnsDelegate delegate) {
+        this(factory, host, port, null, null, null, reconnectPolicy, delegate);
+    }
+
+    private ApnsConnectionImpl(SocketFactory factory, String host, int port, Proxy proxy, String proxyUsername, String proxyPassword,
+                               ReconnectPolicy reconnectPolicy, ApnsDelegate delegate) {
+        this(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy, delegate, false, null,
+                ApnsConnection.DEFAULT_CACHE_LENGTH, true, 0, 0);
+    }
+
+    public ApnsConnectionImpl(SocketFactory factory, String host, int port, Proxy proxy, String proxyUsername, String proxyPassword,
+                              ReconnectPolicy reconnectPolicy, ApnsDelegate delegate, boolean errorDetection, ThreadFactory tf, int cacheLength,
+                              boolean autoAdjustCacheLength, int readTimeout, int connectTimeout) {
+        this.factory = factory;
+        this.host = host;
+        this.port = port;
+        this.reconnectPolicy = reconnectPolicy;
+        this.delegate = delegate == null ? ApnsDelegate.EMPTY : delegate;
+        this.proxy = proxy;
+        this.errorDetection = errorDetection;
+        this.threadFactory = tf == null ? defaultThreadFactory() : tf;
+        this.cacheLength = cacheLength;
+        this.autoAdjustCacheLength = autoAdjustCacheLength;
+        this.readTimeout = readTimeout;
+        this.connectTimeout = connectTimeout;
+        this.proxyUsername = proxyUsername;
+        this.proxyPassword = proxyPassword;
+        cachedNotifications = new ConcurrentLinkedQueue<ApnsNotification>();
+        notificationsBuffer = new ConcurrentLinkedQueue<ApnsNotification>();
+    }
+
+    private ThreadFactory defaultThreadFactory() {
+        return new ThreadFactory() {
+            ThreadFactory wrapped = Executors.defaultThreadFactory();
+            @Override
+            public Thread newThread( Runnable r )
+            {
+                Thread result = wrapped.newThread(r);
+                result.setName("MonitoringThread-"+threadId.incrementAndGet());
+                result.setDaemon(true);
+                return result;
+            }
+        };
+    }
+
+    public synchronized void close() {
+        Utilities.close(socket);
+    }
+
+    private void monitorSocket(final Socket socketToMonitor) {
+        logger.debug("Launching Monitoring Thread for socket {}", socketToMonitor);
+
+        Thread t = threadFactory.newThread(new Runnable() {
+            final static int EXPECTED_SIZE = 6;
+
+            @SuppressWarnings("InfiniteLoopStatement")
+            @Override
+            public void run() {
+                logger.debug("Started monitoring thread");
+                try {
+                    InputStream in;
+                    try {
+                        in = socketToMonitor.getInputStream();
+                    } catch (IOException ioe) {
+                        logger.warn("The value of socket is null", ioe);
+                        in = null;
+                    }
+
+                    byte[] bytes = new byte[EXPECTED_SIZE];
+                    while (in != null && readPacket(in, bytes)) {
+                        logger.debug("Error-response packet {}", Utilities.encodeHex(bytes));
+                        // Quickly close socket, so we won't ever try to send push notifications
+                        // using the defective socket.
+                        Utilities.close(socketToMonitor);
+
+                        int command = bytes[0] & 0xFF;
+                        if (command != 8) {
+                            throw new IOException("Unexpected command byte " + command);
+                        }
+                        int statusCode = bytes[1] & 0xFF;
+                        DeliveryError e = DeliveryError.ofCode(statusCode);
+
+                        int id = Utilities.parseBytes(bytes[2], bytes[3], bytes[4], bytes[5]);
+
+                        logger.debug("Closed connection cause={}; id={}", e, id);
+                        delegate.connectionClosed(e, id);
+
+                        Queue<ApnsNotification> tempCache = new LinkedList<ApnsNotification>();
+                        ApnsNotification notification = null;
+                        boolean foundNotification = false;
+
+                        while (!cachedNotifications.isEmpty()) {
+                            notification = cachedNotifications.poll();
+                            logger.debug("Candidate for removal, message id {}", notification.getIdentifier());
+
+                            if (notification.getIdentifier() == id) {
+                                logger.debug("Bad message found {}", notification.getIdentifier());
+                                foundNotification = true;
+                                break;
+                            }
+                            tempCache.add(notification);
+                        }
+
+                        if (foundNotification) {
+                            logger.debug("delegate.messageSendFailed, message id {}", notification.getIdentifier());
+                            delegate.messageSendFailed(notification, new ApnsDeliveryErrorException(e));
+                        } else {
+                            cachedNotifications.addAll(tempCache);
+                            int resendSize = tempCache.size();
+                            logger.warn("Received error for message that wasn't in the cache...");
+                            if (autoAdjustCacheLength) {
+                                cacheLength = cacheLength + (resendSize / 2);
+                                delegate.cacheLengthExceeded(cacheLength);
+                            }
+                            logger.debug("delegate.messageSendFailed, unknown id");
+                            delegate.messageSendFailed(null, new ApnsDeliveryErrorException(e));
+                        }
+
+                        int resendSize = 0;
+
+                        while (!cachedNotifications.isEmpty()) {
+
+                            resendSize++;
+                            final ApnsNotification resendNotification = cachedNotifications.poll();
+                            logger.debug("Queuing for resend {}", resendNotification.getIdentifier());
+                            notificationsBuffer.add(resendNotification);
+                        }
+                        logger.debug("resending {} notifications", resendSize);
+                        delegate.notificationsResent(resendSize);
+                    }
+                    logger.debug("Monitoring input stream closed by EOF");
+
+                } catch (IOException e) {
+                    // An exception when reading the error code is non-critical, it will cause another retry
+                    // sending the message. Other than providing a more stable network connection to the APNS
+                    // server we can't do much about it - so let's not spam the application's error log.
+                    logger.info("Exception while waiting for error code", e);
+                    delegate.connectionClosed(DeliveryError.UNKNOWN, -1);
+                } finally {
+                    Utilities.close(socketToMonitor);
+                    drainBuffer();
+                }
+            }
+
+            /**
+             * Read a packet like in.readFully(bytes) does - but do not throw an exception and return false if nothing
+             * could be read at all.
+             * @param in the input stream
+             * @param bytes the array to be filled with data
+             * @return true if a packet as been read, false if the stream was at EOF right at the beginning.
+             * @throws IOException When a problem occurs, especially EOFException when there's an EOF in the middle of the packet.
+             */
+            private boolean readPacket(final InputStream in, final byte[] bytes) throws IOException {
+                final int len = bytes.length;
+                int n = 0;
+                while (n < len) {
+                    try {
+                        int count = in.read(bytes, n, len - n);
+                        if (count < 0) {
+                            throw new EOFException("EOF after reading "+n+" bytes of new packet.");
+                        }
+                        n += count;
+                    } catch (IOException ioe) {
+                        if (n == 0)
+                            return false;
+                        throw new IOException("Error after reading "+n+" bytes of packet", ioe);
+                    }
+                }
+                return true;
+            }
+        });
+        t.start();
+    }
+
+    private synchronized Socket getOrCreateSocket(boolean resend) throws NetworkIOException {
+        if (reconnectPolicy.shouldReconnect()) {
+            logger.debug("Reconnecting due to reconnectPolicy dictating it");
+            Utilities.close(socket);
+            socket = null;
+        }
+
+        if (socket == null || socket.isClosed()) {
+            try {
+                if (proxy == null) {
+                    socket = factory.createSocket(host, port);
+                    logger.debug("Connected new socket {}", socket);
+                } else if (proxy.type() == Proxy.Type.HTTP) {
+                    TlsTunnelBuilder tunnelBuilder = new TlsTunnelBuilder();
+                    socket = tunnelBuilder.build((SSLSocketFactory) factory, proxy, proxyUsername, proxyPassword, host, port);
+                    logger.debug("Connected new socket through http tunnel {}", socket);
+                } else {
+                    boolean success = false;
+                    Socket proxySocket = null;
+                    try {
+                        proxySocket = new Socket(proxy);
+                        proxySocket.connect(new InetSocketAddress(host, port), connectTimeout);
+                        socket = ((SSLSocketFactory) factory).createSocket(proxySocket, host, port, false);
+                        success = true;
+                    } finally {
+                        if (!success) {
+                            Utilities.close(proxySocket);
+                        }
+                    }
+                    logger.debug("Connected new socket through socks tunnel {}", socket);
+                }
+
+                socket.setSoTimeout(readTimeout);
+                socket.setKeepAlive(true);
+
+                if (errorDetection) {
+                    monitorSocket(socket);
+                }
+
+                reconnectPolicy.reconnected();
+                logger.debug("Made a new connection to APNS");
+            } catch (IOException e) {
+                logger.error("Couldn't connect to APNS server", e);
+                // indicate to clients whether this is a resend or initial send
+                throw new NetworkIOException(e, resend);
+            }
+        }
+        return socket;
+    }
+
+    int DELAY_IN_MS = 1000;
+    private static final int RETRIES = 3;
+
+    public synchronized void sendMessage(ApnsNotification m) throws NetworkIOException {
+        sendMessage(m, false);
+        drainBuffer();
+    }
+
+    private synchronized void sendMessage(ApnsNotification m, boolean fromBuffer) throws NetworkIOException {
+        logger.debug("sendMessage {} fromBuffer: {}", m, fromBuffer);
+
+        if (delegate instanceof StartSendingApnsDelegate) {
+            ((StartSendingApnsDelegate) delegate).startSending(m, fromBuffer);
+        }
+
+        int attempts = 0;
+        while (true) {
+            try {
+                attempts++;
+                Socket socket = getOrCreateSocket(fromBuffer);
+                socket.getOutputStream().write(m.marshall());
+                socket.getOutputStream().flush();
+                cacheNotification(m);
+
+                delegate.messageSent(m, fromBuffer);
+
+                //logger.debug("Message \"{}\" sent", m);
+                attempts = 0;
+                break;
+            } catch (SSLHandshakeException e) {
+                // No use retrying this, it's dead Jim
+                throw new NetworkIOException(e);
+            } catch (IOException e) {
+                Utilities.close(socket);
+                if (attempts >= RETRIES) {
+                    logger.error("Couldn't send message after " + RETRIES + " retries." + m, e);
+                    delegate.messageSendFailed(m, e);
+                    Utilities.wrapAndThrowAsRuntimeException(e);
+                }
+                // The first failure might be due to closed connection (which in turn might be caused by
+                // a message containing a bad token), so don't delay for the first retry.
+                //
+                // Additionally we don't want to spam the log file in this case, only after the second retry
+                // which uses the delay.
+
+                if (attempts != 1) {
+                    logger.info("Failed to send message " + m + "... trying again after delay", e);
+                    Utilities.sleep(DELAY_IN_MS);
+                }
+            }
+        }
+    }
+
+    private synchronized void drainBuffer() {
+        logger.debug("draining buffer");
+        while (!notificationsBuffer.isEmpty()) {
+            final ApnsNotification notification = notificationsBuffer.poll();
+            try {
+                sendMessage(notification, true);
+            }
+            catch (NetworkIOException ex) {
+                // at this point we are retrying the submission of messages but failing to connect to APNS, therefore
+                // notify the client of this
+                delegate.messageSendFailed(notification, ex);
+            }
+        }
+    }
+
+    private void cacheNotification(ApnsNotification notification) {
+        cachedNotifications.add(notification);
+        while (cachedNotifications.size() > cacheLength) {
+            cachedNotifications.poll();
+            logger.debug("Removing notification from cache " + notification);
+        }
+    }
+
+    public ApnsConnectionImpl copy() {
+        return new ApnsConnectionImpl(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy.copy(), delegate,
+                errorDetection, threadFactory, cacheLength, autoAdjustCacheLength, readTimeout, connectTimeout);
+    }
+
+    public void testConnection() throws NetworkIOException {
+        ApnsConnectionImpl testConnection = null;
+        try {
+            testConnection =
+                    new ApnsConnectionImpl(factory, host, port, proxy, proxyUsername, proxyPassword, reconnectPolicy.copy(), delegate);
+            final ApnsNotification notification = new EnhancedApnsNotification(0, 0, new byte[]{0}, new byte[]{0});
+            testConnection.sendMessage(notification);
+        } finally {
+            if (testConnection != null) {
+                testConnection.close();
+            }
+        }
+    }
+
+    public void setCacheLength(int cacheLength) {
+        this.cacheLength = cacheLength;
+    }
+
+    public int getCacheLength() {
+        return cacheLength;
+    }
+}

+ 121 - 0
src/main/java/com/notnoop/apns/internal/ApnsFeedbackConnection.java

@@ -0,0 +1,121 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.InetSocketAddress;
+import java.net.Proxy;
+import java.net.Socket;
+import java.util.Date;
+import java.util.Map;
+import javax.net.SocketFactory;
+import javax.net.ssl.SSLSocketFactory;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import com.notnoop.exceptions.NetworkIOException;
+
+public class ApnsFeedbackConnection {
+    private static final Logger logger = LoggerFactory.getLogger(ApnsFeedbackConnection.class);
+
+    private final SocketFactory factory;
+    private final String host;
+    private final int port;
+    private final Proxy proxy;
+    private final int readTimeout;
+    private final int connectTimeout;
+    private final String proxyUsername;
+    private final String proxyPassword;
+
+    public ApnsFeedbackConnection(final SocketFactory factory, final String host, final int port) {
+        this(factory, host, port, null, 0, 0, null, null);
+    }
+
+    public ApnsFeedbackConnection(final SocketFactory factory, final String host, final int port,
+            final Proxy proxy, int readTimeout, int connectTimeout, final String proxyUsername, final String proxyPassword) {
+        this.factory = factory;
+        this.host = host;
+        this.port = port;
+        this.proxy = proxy;
+        this.readTimeout = readTimeout;
+        this.connectTimeout = connectTimeout;
+        this.proxyUsername = proxyUsername;
+        this.proxyPassword = proxyPassword;
+    }
+
+    int DELAY_IN_MS = 1000;
+    private static final int RETRIES = 3;
+
+    public Map<String, Date> getInactiveDevices() throws NetworkIOException {
+        int attempts = 0;
+        while (true) {
+            try {
+                attempts++;
+                final Map<String, Date> result = getInactiveDevicesImpl();
+
+                attempts = 0;
+                return result;
+            } catch (final Exception e) {
+                logger.warn("Failed to retrieve invalid devices", e);
+                if (attempts >= RETRIES) {
+                    logger.error("Couldn't get feedback connection", e);
+                    Utilities.wrapAndThrowAsRuntimeException(e);
+                }
+                Utilities.sleep(DELAY_IN_MS);
+            }
+        }
+    }
+
+    public Map<String, Date> getInactiveDevicesImpl() throws IOException {
+        Socket proxySocket = null;
+        Socket socket = null;
+        try {
+            if (proxy == null) {
+                socket = factory.createSocket(host, port);
+            } else if (proxy.type() == Proxy.Type.HTTP) {
+                TlsTunnelBuilder tunnelBuilder = new TlsTunnelBuilder();
+                socket = tunnelBuilder.build((SSLSocketFactory) factory, proxy, proxyUsername, proxyPassword, host, port);
+            } else {
+                proxySocket = new Socket(proxy);
+                proxySocket.connect(new InetSocketAddress(host, port), connectTimeout);
+                socket = ((SSLSocketFactory) factory).createSocket(proxySocket, host, port, false);
+            }
+            socket.setSoTimeout(readTimeout);
+            socket.setKeepAlive(true);
+            final InputStream stream = socket.getInputStream();
+            return Utilities.parseFeedbackStream(stream);
+        } finally {
+            Utilities.close(socket);
+            Utilities.close(proxySocket);
+        }
+    }
+
+}

+ 121 - 0
src/main/java/com/notnoop/apns/internal/ApnsPooledConnection.java

@@ -0,0 +1,121 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import java.util.concurrent.*;
+import com.notnoop.apns.ApnsNotification;
+import com.notnoop.exceptions.NetworkIOException;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class ApnsPooledConnection implements ApnsConnection {
+    private static final Logger logger = LoggerFactory.getLogger(ApnsPooledConnection.class);
+
+    private final ApnsConnection prototype;
+    private final int max;
+
+    private final ExecutorService executors;
+    private final ConcurrentLinkedQueue<ApnsConnection> prototypes;
+
+    public ApnsPooledConnection(ApnsConnection prototype, int max) {
+        this(prototype, max, Executors.newFixedThreadPool(max));
+    }
+
+    public ApnsPooledConnection(ApnsConnection prototype, int max, ExecutorService executors) {
+        this.prototype = prototype;
+        this.max = max;
+
+        this.executors = executors;
+        this.prototypes = new ConcurrentLinkedQueue<ApnsConnection>();
+    }
+
+    private final ThreadLocal<ApnsConnection> uniquePrototype =
+        new ThreadLocal<ApnsConnection>() {
+        protected ApnsConnection initialValue() {
+            ApnsConnection newCopy = prototype.copy();
+            prototypes.add(newCopy);
+            return newCopy;
+        }
+    };
+
+    public void sendMessage(final ApnsNotification m) throws NetworkIOException {
+        Future<Void> future = executors.submit(new Callable<Void>() {
+            public Void call() throws Exception {
+                uniquePrototype.get().sendMessage(m);
+                return null;
+            }
+        });
+        try {
+            future.get();
+        } catch (InterruptedException ie) {
+            Thread.currentThread().interrupt();
+        } catch (ExecutionException ee) {
+            if (ee.getCause() instanceof NetworkIOException) {
+                throw (NetworkIOException) ee.getCause();
+            }
+        }
+    }
+
+    public ApnsConnection copy() {
+        // TODO: Should copy executor properly.... What should copy do
+        // really?!
+        return new ApnsPooledConnection(prototype, max);
+    }
+
+    public void close() {
+        executors.shutdown();
+        try {
+            executors.awaitTermination(10, TimeUnit.SECONDS);
+        } catch (InterruptedException e) {
+            logger.warn("pool termination interrupted", e);
+        }
+        for (ApnsConnection conn : prototypes) {
+            Utilities.close(conn);
+        }
+        Utilities.close(prototype);
+    }
+
+    public void testConnection() {
+        prototype.testConnection();
+    }
+
+    public synchronized void setCacheLength(int cacheLength) {  
+        for (ApnsConnection conn : prototypes) {
+            conn.setCacheLength(cacheLength);
+        }
+    }
+
+    @SuppressFBWarnings(value = "UG_SYNC_SET_UNSYNC_GET", justification = "prototypes is a MT-safe container")
+    public int getCacheLength() {
+        return prototypes.peek().getCacheLength();
+    }
+}

+ 59 - 0
src/main/java/com/notnoop/apns/internal/ApnsServiceImpl.java

@@ -0,0 +1,59 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import com.notnoop.apns.ApnsNotification;
+import com.notnoop.exceptions.NetworkIOException;
+
+public class ApnsServiceImpl extends AbstractApnsService {
+    private ApnsConnection connection;
+
+    public ApnsServiceImpl(ApnsConnection connection, ApnsFeedbackConnection feedback) {
+        super(feedback);
+        this.connection = connection;
+    }
+
+    @Override
+    public void push(ApnsNotification msg) throws NetworkIOException {
+        connection.sendMessage(msg);
+    }
+
+    public void start() {
+    }
+
+    public void stop() {
+        Utilities.close(connection);
+    }
+
+    public void testConnection() {
+        connection.testConnection();
+    }
+}

+ 143 - 0
src/main/java/com/notnoop/apns/internal/BatchApnsService.java

@@ -0,0 +1,143 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import static java.util.concurrent.Executors.defaultThreadFactory;
+
+import java.util.Queue;
+import java.util.concurrent.ConcurrentLinkedQueue;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ScheduledFuture;
+import java.util.concurrent.ScheduledThreadPoolExecutor;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+
+import com.notnoop.apns.ApnsNotification;
+import com.notnoop.exceptions.NetworkIOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class BatchApnsService extends AbstractApnsService {
+
+    private static final Logger logger = LoggerFactory.getLogger(BatchApnsService.class);
+
+	/**
+	 * How many seconds to wait for more messages before batch is send.
+	 * Each message reset the wait time
+	 * 
+	 * @see #maxBatchWaitTimeInSec
+	 */
+	private int batchWaitTimeInSec = 5;
+	
+	/**
+	 * How many seconds can be batch delayed before execution.
+	 * This time is not exact amount after which the batch will run its roughly the time
+	 */
+	private int maxBatchWaitTimeInSec = 10;
+	
+	private long firstMessageArrivedTime; 
+	
+	private ApnsConnection prototype;
+
+	private Queue<ApnsNotification> batch = new ConcurrentLinkedQueue<ApnsNotification>();
+
+	private ScheduledExecutorService scheduleService;
+	private ScheduledFuture<?> taskFuture;
+
+	private Runnable batchRunner = new SendMessagesBatch();
+
+    public BatchApnsService(ApnsConnection prototype, ApnsFeedbackConnection feedback, int batchWaitTimeInSec, int maxBachWaitTimeInSec, ThreadFactory tf) {
+        this(prototype, feedback, batchWaitTimeInSec, maxBachWaitTimeInSec,
+                new ScheduledThreadPoolExecutor(1,
+                        tf != null ? tf : defaultThreadFactory()));
+    }
+
+    public BatchApnsService(ApnsConnection prototype, ApnsFeedbackConnection feedback, int batchWaitTimeInSec, int maxBachWaitTimeInSec, ScheduledExecutorService executor) {
+		super(feedback);
+		this.prototype = prototype;
+		this.batchWaitTimeInSec = batchWaitTimeInSec;
+		this.maxBatchWaitTimeInSec = maxBachWaitTimeInSec;
+		this.scheduleService = executor != null ? executor : new ScheduledThreadPoolExecutor(1, defaultThreadFactory());
+	}
+
+	public void start() {
+		// no code
+	}
+
+	public void stop() {
+		Utilities.close(prototype);
+		if (taskFuture != null) {
+			taskFuture.cancel(true);
+		}
+		scheduleService.shutdownNow();
+	}
+
+	public void testConnection() throws NetworkIOException {
+		prototype.testConnection();
+	}
+
+	@Override
+	public void push(ApnsNotification message) throws NetworkIOException {
+		if (batch.isEmpty()) {
+			firstMessageArrivedTime = System.nanoTime();
+		}
+		
+		long sinceFirstMessageSec = (System.nanoTime() - firstMessageArrivedTime) / 1000 / 1000 / 1000;
+		
+		if (taskFuture != null && sinceFirstMessageSec < maxBatchWaitTimeInSec) {
+			taskFuture.cancel(false);
+		}
+		
+		batch.add(message);
+		
+		if (taskFuture == null || taskFuture.isDone()) {
+			taskFuture = scheduleService.schedule(batchRunner, batchWaitTimeInSec, TimeUnit.SECONDS);
+		}
+	}
+
+	class SendMessagesBatch implements Runnable {
+		public void run() {
+			ApnsConnection newConnection = prototype.copy();
+			try {
+				ApnsNotification msg;
+				while ((msg = batch.poll()) != null) {
+					try {
+						newConnection.sendMessage(msg);
+					} catch (NetworkIOException e) {
+                        logger.warn("Network exception sending message msg "+ msg.getIdentifier(), e);
+                    }
+				}
+			} finally {
+				Utilities.close(newConnection);
+			}
+		}
+	}
+}

+ 126 - 0
src/main/java/com/notnoop/apns/internal/QueuedApnsService.java

@@ -0,0 +1,126 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import java.util.Date;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.notnoop.apns.ApnsNotification;
+import com.notnoop.apns.ApnsService;
+import com.notnoop.exceptions.NetworkIOException;
+
+public class QueuedApnsService extends AbstractApnsService {
+
+	private static final Logger logger = LoggerFactory.getLogger(QueuedApnsService.class);
+	
+    private ApnsService service;
+    private BlockingQueue<ApnsNotification> queue;
+    private AtomicBoolean started = new AtomicBoolean(false);
+
+    public QueuedApnsService(ApnsService service) {
+        this(service, null);
+    }
+
+    public QueuedApnsService(ApnsService service, final ThreadFactory tf) {
+        super(null);
+        this.service = service;
+        this.queue = new LinkedBlockingQueue<ApnsNotification>();
+        this.threadFactory = tf == null ? Executors.defaultThreadFactory() : tf;
+        this.thread = null;
+    }
+
+    @Override
+    public void push(ApnsNotification msg) {
+        if (!started.get()) {
+            throw new IllegalStateException("service hasn't be started or was closed");
+        }
+        queue.add(msg);
+    }
+
+    private final ThreadFactory threadFactory;
+    private Thread thread;
+    private volatile boolean shouldContinue;
+
+    public void start() {
+        if (started.getAndSet(true)) {
+            // I prefer if we throw a runtime IllegalStateException here,
+            // but I want to maintain semantic backward compatibility.
+            // So it is returning immediately here
+            return;
+        }
+
+        service.start();
+        shouldContinue = true;
+        thread = threadFactory.newThread(new Runnable() {
+            public void run() {
+                while (shouldContinue) {
+                    try {
+                        ApnsNotification msg = queue.take();
+                        service.push(msg);
+                    } catch (InterruptedException e) {
+                    	// ignore
+                    } catch (NetworkIOException e) {
+                    	// ignore: failed connect...
+                    } catch (Exception e) {
+                    	// weird if we reached here - something wrong is happening, but we shouldn't stop the service anyway!
+                    	logger.warn("Unexpected message caught... Shouldn't be here", e);
+                    }
+                }
+            }
+        });
+        thread.start();
+    }
+
+    public void stop() {
+        started.set(false);
+        shouldContinue = false;
+        thread.interrupt();
+        service.stop();
+    }
+
+    @Override
+    public Map<String, Date> getInactiveDevices() throws NetworkIOException {
+        return service.getInactiveDevices();
+    }
+
+    public void testConnection() throws NetworkIOException {
+        service.testConnection();
+    }
+
+}

+ 67 - 0
src/main/java/com/notnoop/apns/internal/ReconnectPolicies.java

@@ -0,0 +1,67 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import com.notnoop.apns.ReconnectPolicy;
+
+public final class ReconnectPolicies {
+
+    public static class Never implements ReconnectPolicy {
+
+        public boolean shouldReconnect() { return false; }
+        public void reconnected() { }
+        public Never copy() { return this; }
+    }
+
+    public static class Always implements ReconnectPolicy {
+        public boolean shouldReconnect() { return true; }
+        public void reconnected() { }
+        public Always copy() { return this; }
+    }
+
+    public static class EveryHalfHour implements ReconnectPolicy {
+        private static final long PERIOD = 30 * 60 * 1000;
+
+        private long lastRunning = System.currentTimeMillis();
+
+        public boolean shouldReconnect() {
+            return System.currentTimeMillis() - lastRunning > PERIOD;
+        }
+
+        public void reconnected() {
+            lastRunning = System.currentTimeMillis();
+        }
+
+        public EveryHalfHour copy() {
+            return new EveryHalfHour();
+        }
+    }
+}

+ 179 - 0
src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java

@@ -0,0 +1,179 @@
+/*
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import com.notnoop.exceptions.InvalidSSLConfig;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManager;
+import javax.net.ssl.TrustManagerFactory;
+import java.io.IOException;
+import java.io.InputStream;
+import java.security.GeneralSecurityException;
+import java.security.Key;
+import java.security.KeyStore;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.Certificate;
+import java.security.cert.CertificateException;
+
+public class SSLContextBuilder {
+    private String algorithm = "sunx509";
+    private KeyManagerFactory keyManagerFactory;
+    private TrustManager[] trustManagers;
+
+    public SSLContextBuilder withAlgorithm(String algorithm) {
+        this.algorithm = algorithm;
+        return this;
+    }
+
+    public SSLContextBuilder withDefaultTrustKeyStore() throws InvalidSSLConfig {
+        try {
+            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
+            trustManagerFactory.init((KeyStore)null);
+            trustManagers = trustManagerFactory.getTrustManagers();
+            return this;
+        } catch (GeneralSecurityException e) {
+            throw new InvalidSSLConfig(e);
+        }
+    }
+
+    public SSLContextBuilder withTrustKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType) throws InvalidSSLConfig {
+        try {
+            final KeyStore ks = KeyStore.getInstance(keyStoreType);
+            ks.load(keyStoreStream, keyStorePassword.toCharArray());
+            return withTrustKeyStore(ks, keyStorePassword);
+        } catch (GeneralSecurityException e) {
+            throw new InvalidSSLConfig(e);
+        } catch (IOException e) {
+            throw new InvalidSSLConfig(e);
+        }
+
+    }
+    public SSLContextBuilder withTrustKeyStore(KeyStore keyStore, String keyStorePassword) throws InvalidSSLConfig {
+        try {
+            TrustManagerFactory trustManagerFactory = TrustManagerFactory.getInstance(algorithm);
+            trustManagerFactory.init(keyStore);
+            trustManagers = trustManagerFactory.getTrustManagers();
+            return this;
+        } catch (GeneralSecurityException e) {
+            throw new InvalidSSLConfig(e);
+        }
+    }
+
+    public SSLContextBuilder withTrustManager(TrustManager trustManager) {
+        trustManagers = new TrustManager[] { trustManager };
+        return this;
+    }
+
+    public SSLContextBuilder withCertificateKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType) throws InvalidSSLConfig {
+        try {
+            final KeyStore ks = KeyStore.getInstance(keyStoreType);
+            ks.load(keyStoreStream, keyStorePassword.toCharArray());
+            return withCertificateKeyStore(ks, keyStorePassword);
+        } catch (GeneralSecurityException e) {
+            throw new InvalidSSLConfig(e);
+        } catch (IOException e) {
+            throw new InvalidSSLConfig(e);
+        }
+    }
+
+    public SSLContextBuilder withCertificateKeyStore(InputStream keyStoreStream, String keyStorePassword, String keyStoreType, String keyAlias) throws InvalidSSLConfig {
+        try {
+            final KeyStore ks = KeyStore.getInstance(keyStoreType);
+            ks.load(keyStoreStream, keyStorePassword.toCharArray());
+            return withCertificateKeyStore(ks, keyStorePassword, keyAlias);
+        } catch (GeneralSecurityException e) {
+            throw new InvalidSSLConfig(e);
+        } catch (IOException e) {
+            throw new InvalidSSLConfig(e);
+        }
+    }
+
+    public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keyStorePassword) throws InvalidSSLConfig {
+        try {
+            keyManagerFactory = KeyManagerFactory.getInstance(algorithm);
+            keyManagerFactory.init(keyStore, keyStorePassword.toCharArray());
+            return this;
+        } catch (GeneralSecurityException e) {
+            throw new InvalidSSLConfig(e);
+        }
+    }
+
+    public SSLContextBuilder withCertificateKeyStore(KeyStore keyStore, String keyStorePassword, String keyAlias) throws InvalidSSLConfig {
+        try {
+            if (!keyStore.containsAlias(keyAlias)) {
+                throw new InvalidSSLConfig("No key with alias " + keyAlias);
+            }
+            KeyStore singleKeyKeyStore = getKeyStoreWithSingleKey(keyStore, keyStorePassword, keyAlias);
+            return withCertificateKeyStore(singleKeyKeyStore, keyStorePassword);
+        } catch (GeneralSecurityException e) {
+            throw new InvalidSSLConfig(e);
+        } catch (IOException e) {
+            throw new InvalidSSLConfig(e);
+        }
+    }
+
+    /*
+     * Workaround for keystores containing multiple keys. Java will take the first key that matches
+     * and this way we can still offer configuration for a keystore with multiple keys and a selection
+     * based on alias. Also much easier than making a subclass of a KeyManagerFactory
+     */
+    private KeyStore getKeyStoreWithSingleKey(KeyStore keyStore, String keyStorePassword, String keyAlias)
+            throws KeyStoreException, IOException, NoSuchAlgorithmException, CertificateException, UnrecoverableKeyException {
+        KeyStore singleKeyKeyStore = KeyStore.getInstance(keyStore.getType(), keyStore.getProvider());
+        final char[] password = keyStorePassword.toCharArray();
+        singleKeyKeyStore.load(null, password);
+        Key key = keyStore.getKey(keyAlias, password);
+        Certificate[] chain = keyStore.getCertificateChain(keyAlias);
+        singleKeyKeyStore.setKeyEntry(keyAlias, key, password, chain);
+        return singleKeyKeyStore;
+    }
+
+    public SSLContext build() throws InvalidSSLConfig {
+        if (keyManagerFactory == null) {
+            throw new InvalidSSLConfig("Missing KeyManagerFactory");
+        }
+
+        if (trustManagers == null) {
+            throw new InvalidSSLConfig("Missing TrustManagers");
+        }
+
+        try {
+            final SSLContext sslContext = SSLContext.getInstance("TLS");
+            sslContext.init(keyManagerFactory.getKeyManagers(), trustManagers, null);
+            return sslContext;
+        } catch (GeneralSecurityException e) {
+            throw new InvalidSSLConfig(e);
+        }
+    }
+}

+ 147 - 0
src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java

@@ -0,0 +1,147 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.net.ProtocolException;
+import java.net.Proxy;
+import java.net.Socket;
+import javax.net.ssl.SSLSocketFactory;
+import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
+import org.apache.commons.httpclient.ConnectMethod;
+import org.apache.commons.httpclient.NTCredentials;
+import org.apache.commons.httpclient.ProxyClient;
+import org.apache.commons.httpclient.UsernamePasswordCredentials;
+import org.apache.commons.httpclient.auth.AuthScope;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Establishes a TLS connection using an HTTP proxy. See <a
+ * href="http://www.ietf.org/rfc/rfc2817.txt">RFC 2817 5.2</a>. This class does
+ * not support proxies requiring a "Proxy-Authorization" header.
+ */
+public final class TlsTunnelBuilder {
+    
+    private static final Logger logger = LoggerFactory.getLogger(TlsTunnelBuilder.class);
+    
+    public Socket build(SSLSocketFactory factory, Proxy proxy, String proxyUsername, String proxyPassword, String host, int port)
+            throws IOException {
+        boolean success = false;
+        Socket proxySocket = null;
+        try {
+            logger.debug("Attempting to use proxy : " + proxy.toString());
+            InetSocketAddress proxyAddress = (InetSocketAddress) proxy.address();
+            proxySocket = makeTunnel(host, port, proxyUsername, proxyPassword, proxyAddress);
+
+            // Handshake with the origin server.
+            if(proxySocket ==  null) {
+                throw new ProtocolException("Unable to create tunnel through proxy server.");
+            }
+            Socket socket = factory.createSocket(proxySocket, host, port, true /* auto close */);
+            success = true;
+            return socket;
+        } finally {
+            if (!success) {
+                Utilities.close(proxySocket);
+            }
+        }
+    }
+
+    @SuppressFBWarnings(value = "VA_FORMAT_STRING_USES_NEWLINE",
+            justification = "use <CR><LF> as according to RFC, not platform-linefeed")
+    Socket makeTunnel(String host, int port, String proxyUsername, 
+            String proxyPassword, InetSocketAddress proxyAddress) throws IOException {
+        if(host == null || port < 0 || host.isEmpty() || proxyAddress == null){
+            throw new ProtocolException("Incorrect parameters to build tunnel.");   
+        }
+        logger.debug("Creating socket for Proxy : " + proxyAddress.getAddress() + ":" + proxyAddress.getPort());
+        Socket socket;
+        try {
+            ProxyClient client = new ProxyClient();
+            client.getParams().setParameter("http.useragent", "java-apns");
+            client.getHostConfiguration().setHost(host, port);
+            String proxyHost = proxyAddress.getAddress().toString().substring(0, proxyAddress.getAddress().toString().indexOf("/"));
+            client.getHostConfiguration().setProxy(proxyHost, proxyAddress.getPort());
+            
+        
+            ProxyClient.ConnectResponse response = client.connect();
+            socket = response.getSocket();
+            if (socket == null) {
+                ConnectMethod method = response.getConnectMethod();
+                // Read the proxy's HTTP response.
+                if(method.getStatusLine().getStatusCode() == 407) {
+                    // Proxy server returned 407. We will now try to connect with auth Header
+                    if(proxyUsername != null && proxyPassword != null) {
+                        socket = AuthenticateProxy(method, client,proxyHost, proxyAddress.getPort(),
+                                proxyUsername, proxyPassword);
+                    } else {
+                        throw new ProtocolException("Socket not created: " + method.getStatusLine()); 
+                    }
+                }             
+            }
+            
+        } catch (Exception e) {
+            throw new ProtocolException("Error occurred while creating proxy socket : " + e.toString());
+        }
+        if (socket != null) {
+            logger.debug("Socket for proxy created successfully : " + socket.getRemoteSocketAddress().toString());
+        }
+        return socket;
+    }
+    
+    private Socket AuthenticateProxy(ConnectMethod method, ProxyClient client, 
+            String proxyHost, int proxyPort, 
+            String proxyUsername, String proxyPassword) throws IOException {   
+        if("ntlm".equalsIgnoreCase(method.getProxyAuthState().getAuthScheme().getSchemeName())) {
+            // If Auth scheme is NTLM, set NT credentials with blank host and domain name
+            client.getState().setProxyCredentials(new AuthScope(proxyHost, proxyPort), 
+                            new NTCredentials(proxyUsername, proxyPassword,"",""));
+        } else {
+            // If Auth scheme is Basic/Digest, set regular Credentials
+            client.getState().setProxyCredentials(new AuthScope(proxyHost, proxyPort), 
+                    new UsernamePasswordCredentials(proxyUsername, proxyPassword));
+        }
+        
+        ProxyClient.ConnectResponse response = client.connect();
+        Socket socket = response.getSocket();
+        
+        if (socket == null) {
+            method = response.getConnectMethod();
+            throw new ProtocolException("Proxy Authentication failed. Socket not created: " 
+                    + method.getStatusLine());
+        }
+        return socket;
+    }
+    
+}
+

+ 296 - 0
src/main/java/com/notnoop/apns/internal/Utilities.java

@@ -0,0 +1,296 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.apns.internal;
+
+import java.io.ByteArrayOutputStream;
+import java.io.Closeable;
+import java.io.DataInputStream;
+import java.io.DataOutputStream;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.Socket;
+import java.security.GeneralSecurityException;
+import java.security.KeyStore;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.regex.Pattern;
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import javax.net.ssl.TrustManagerFactory;
+import com.notnoop.exceptions.InvalidSSLConfig;
+import com.notnoop.exceptions.NetworkIOException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public final class Utilities {
+    private static Logger logger = LoggerFactory.getLogger(Utilities.class);
+
+    public static final String SANDBOX_GATEWAY_HOST = "gateway.sandbox.push.apple.com";
+    public static final int SANDBOX_GATEWAY_PORT = 2195;
+
+    public static final String SANDBOX_FEEDBACK_HOST = "feedback.sandbox.push.apple.com";
+    public static final int SANDBOX_FEEDBACK_PORT = 2196;
+
+    public static final String PRODUCTION_GATEWAY_HOST = "gateway.push.apple.com";
+    public static final int PRODUCTION_GATEWAY_PORT = 2195;
+
+    public static final String PRODUCTION_FEEDBACK_HOST = "feedback.push.apple.com";
+    public static final int PRODUCTION_FEEDBACK_PORT = 2196;
+
+    public static final int MAX_PAYLOAD_LENGTH = 2048;
+
+    private Utilities() { throw new AssertionError("Uninstantiable class"); }
+
+    private static final Pattern pattern = Pattern.compile("[ -]");
+    public static byte[] decodeHex(final String deviceToken) {
+        final String hex = pattern.matcher(deviceToken).replaceAll("");
+
+        final byte[] bts = new byte[hex.length() / 2];
+        for (int i = 0; i < bts.length; i++) {
+            bts[i] = (byte) (charVal(hex.charAt(2 * i)) * 16 + charVal(hex.charAt(2 * i + 1)));
+        }
+        return bts;
+    }
+
+    private static int charVal(final char a) {
+        if ('0' <= a && a <= '9') {
+            return (a - '0');
+        } else if ('a' <= a && a <= 'f') {
+            return (a - 'a') + 10;
+        } else if ('A' <= a && a <= 'F') {
+            return (a - 'A') + 10;
+        } else {
+            throw new RuntimeException("Invalid hex character: " + a);
+        }
+    }
+
+    private static final char base[] = {'0','1','2','3','4','5','6','7','8','9','A','B','C','D','E','F'};
+
+    public static String encodeHex(final byte[] bytes) {
+        final char[] chars = new char[bytes.length * 2];
+
+        for (int i = 0; i < bytes.length; ++i) {
+            final int b = (bytes[i]) & 0xFF;
+            chars[2 * i] = base[b >>> 4];
+            chars[2 * i + 1] = base[b & 0xF];
+        }
+
+        return new String(chars);
+    }
+
+    public static byte[] toUTF8Bytes(final String s) {
+        try {
+            return s.getBytes("UTF-8");
+        } catch (final UnsupportedEncodingException e) {
+            throw new RuntimeException(e);
+        }
+    }
+
+    public static byte[] marshall(final byte command, final byte[] deviceToken, final byte[] payload) {
+        final ByteArrayOutputStream boas = new ByteArrayOutputStream();
+        final DataOutputStream dos = new DataOutputStream(boas);
+
+        try {
+            dos.writeByte(command);
+            dos.writeShort(deviceToken.length);
+            dos.write(deviceToken);
+            dos.writeShort(payload.length);
+            dos.write(payload);
+            return boas.toByteArray();
+        } catch (final IOException e) {
+            throw new AssertionError();
+        }
+    }
+
+    public static byte[] marshallEnhanced(final byte command, final int identifier,
+            final int expiryTime, final byte[] deviceToken, final byte[] payload) {
+        final ByteArrayOutputStream boas = new ByteArrayOutputStream();
+        final DataOutputStream dos = new DataOutputStream(boas);
+
+        try {
+            dos.writeByte(command);
+            dos.writeInt(identifier);
+            dos.writeInt(expiryTime);
+            dos.writeShort(deviceToken.length);
+            dos.write(deviceToken);
+            dos.writeShort(payload.length);
+            dos.write(payload);
+            return boas.toByteArray();
+        } catch (final IOException e) {
+            throw new AssertionError();
+        }
+    }
+
+    public static Map<byte[], Integer> parseFeedbackStreamRaw(final InputStream in) {
+        final Map<byte[], Integer> result = new HashMap<byte[], Integer>();
+
+        final DataInputStream data = new DataInputStream(in);
+
+        while (true) {
+            try {
+                final int time = data.readInt();
+                final int dtLength = data.readUnsignedShort();
+                final byte[] deviceToken = new byte[dtLength];
+                data.readFully(deviceToken);
+
+                result.put(deviceToken, time);
+            } catch (final EOFException e) {
+                break;
+            } catch (final IOException e) {
+                throw new RuntimeException(e);
+            }
+        }
+
+        return result;
+    }
+
+    public static Map<String, Date> parseFeedbackStream(final InputStream in) {
+        final Map<String, Date> result = new HashMap<String, Date>();
+
+        final Map<byte[], Integer> raw = parseFeedbackStreamRaw(in);
+        for (final Map.Entry<byte[], Integer> entry : raw.entrySet()) {
+            final byte[] dtArray = entry.getKey();
+            final int time = entry.getValue(); // in seconds
+
+            final Date date = new Date(time * 1000L);    // in ms
+            final String dtString = encodeHex(dtArray);
+            result.put(dtString, date);
+        }
+
+        return result;
+    }
+
+    public static void close(final Closeable closeable) {
+        logger.debug("close {}", closeable);
+
+        try {
+            if (closeable != null) {
+                closeable.close();
+            }
+        } catch (final IOException e) {
+            logger.debug("error while closing resource", e);
+        }
+    }
+
+    public static void close(final Socket closeable) {
+        logger.debug("close {}", closeable);
+
+        try {
+            if (closeable != null) {
+                closeable.close();
+            }
+        } catch (final IOException e) {
+            logger.debug("error while closing socket", e);
+        }
+    }
+
+    public static void sleep(final int delay) {
+        try {
+            Thread.sleep(delay);
+        } catch (final InterruptedException e1) {
+            Thread.currentThread().interrupt();
+        }
+    }
+
+    public static byte[] copyOf(final byte[] bytes) {
+        final byte[] copy = new byte[bytes.length];
+        System.arraycopy(bytes, 0, copy, 0, bytes.length);
+        return copy;
+    }
+
+    public static byte[] copyOfRange(final byte[] original, final int from, final int to) {
+        final int newLength = to - from;
+        if (newLength < 0) {
+            throw new IllegalArgumentException(from + " > " + to);
+        }
+        final byte[] copy = new byte[newLength];
+        System.arraycopy(original, from, copy, 0,
+                Math.min(original.length - from, newLength));
+        return copy;
+    }
+
+    public static void wrapAndThrowAsRuntimeException(final Exception e) throws NetworkIOException {
+        if (e instanceof IOException) {
+            throw new NetworkIOException((IOException)e);
+        } else if (e instanceof NetworkIOException) {
+            throw (NetworkIOException)e;
+        } else if (e instanceof RuntimeException) {
+            throw (RuntimeException)e;
+        } else {
+            throw new RuntimeException(e);
+        }
+    }
+
+    @SuppressWarnings({"PointlessArithmeticExpression", "PointlessBitwiseExpression"})
+    public static int parseBytes(final int b1, final int b2, final int b3, final int b4) {
+        return  ((b1 << 3 * 8) & 0xFF000000)
+              | ((b2 << 2 * 8) & 0x00FF0000)
+              | ((b3 << 1 * 8) & 0x0000FF00)
+              | ((b4 << 0 * 8) & 0x000000FF);
+    }
+
+    // @see http://stackoverflow.com/questions/119328/how-do-i-truncate-a-java-string-to-fit-in-a-given-number-of-bytes-once-utf-8-enc
+    public static String truncateWhenUTF8(final String s, final int maxBytes) {
+        int b = 0;
+        for (int i = 0; i < s.length(); i++) {
+            final char c = s.charAt(i);
+
+            // ranges from http://en.wikipedia.org/wiki/UTF-8
+            int skip = 0;
+            int more;
+            if (c <= 0x007f) {
+                more = 1;
+            }
+            else if (c <= 0x07FF) {
+                more = 2;
+            } else if (c <= 0xd7ff) {
+                more = 3;
+            } else if (c <= 0xDFFF) {
+                // surrogate area, consume next char as well
+                more = 4;
+                skip = 1;
+            } else {
+                more = 3;
+            }
+
+            if (b + more > maxBytes) {
+                return s.substring(0, i);
+            }
+            b += more;
+            i += skip;
+        }
+        return s;
+    }
+
+}

+ 61 - 0
src/main/java/com/notnoop/exceptions/ApnsDeliveryErrorException.java

@@ -0,0 +1,61 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+/*
+ * To change this template, choose Tools | Templates
+ * and open the template in the editor.
+ */
+package com.notnoop.exceptions;
+
+import com.notnoop.apns.DeliveryError;
+
+/**
+ *
+ * @author kkirch
+ */
+public class ApnsDeliveryErrorException extends ApnsException {
+
+    private final DeliveryError deliveryError;
+
+    public ApnsDeliveryErrorException(DeliveryError error) {
+        this.deliveryError = error;
+    }
+
+    @Override
+    public String getMessage() {
+        return "Failed to deliver notification with error code " + deliveryError.code();
+    }
+
+    public DeliveryError getDeliveryError() {
+        return deliveryError;
+    }
+    
+    
+}

+ 44 - 0
src/main/java/com/notnoop/exceptions/ApnsException.java

@@ -0,0 +1,44 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.exceptions;
+
+/**
+ * Base class for all the exceptions thrown in Apns Library
+ */
+public abstract class ApnsException extends RuntimeException {
+    private static final long serialVersionUID = -4756693306121825229L;
+
+    public ApnsException()                      { super(); }
+    public ApnsException(String message)        { super(message); }
+    public ApnsException(Throwable cause)       { super(cause); }
+    public ApnsException(String m, Throwable c) { super(m, c); }
+
+}

+ 64 - 0
src/main/java/com/notnoop/exceptions/InvalidSSLConfig.java

@@ -0,0 +1,64 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.exceptions;
+
+import java.io.IOException;
+import java.security.KeyManagementException;
+import java.security.KeyStoreException;
+import java.security.NoSuchAlgorithmException;
+import java.security.UnrecoverableKeyException;
+import java.security.cert.CertificateException;
+
+/**
+ * Signals that the the provided SSL context settings (e.g.
+ * keystore path, password, encryption type, etc) are invalid
+ *
+ * This Exception can be caused by any of the following:
+ *
+ * <ol>
+ * <li>{@link KeyStoreException}</li>
+ * <li>{@link NoSuchAlgorithmException}</li>
+ * <li>{@link CertificateException}</li>
+ * <li>{@link IOException}</li>
+ * <li>{@link UnrecoverableKeyException}</li>
+ * <li>{@link KeyManagementException}</li>
+ * </ol>
+ *
+ */
+public class InvalidSSLConfig extends ApnsException {
+    private static final long serialVersionUID = -7283168775864517167L;
+
+    public InvalidSSLConfig()                      { super(); }
+    public InvalidSSLConfig(String message)        { super(message); }
+    public InvalidSSLConfig(Throwable cause)       { super(cause); }
+    public InvalidSSLConfig(String m, Throwable c) { super(m, c); }
+
+}

+ 69 - 0
src/main/java/com/notnoop/exceptions/NetworkIOException.java

@@ -0,0 +1,69 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.exceptions;
+
+import java.io.IOException;
+
+/**
+ * Thrown to indicate that that a network operation has failed:
+ * (e.g. connectivity problems, domain cannot be found, network
+ * dropped).
+ */
+public class NetworkIOException extends ApnsException {
+    private static final long serialVersionUID = 3353516625486306533L;
+
+    private boolean resend;
+
+    public NetworkIOException()                      { super(); }
+    public NetworkIOException(String message)        { super(message); }
+    public NetworkIOException(IOException cause)       { super(cause); }
+    public NetworkIOException(String m, IOException c) { super(m, c); }
+    public NetworkIOException(IOException cause, boolean resend) {
+        super(cause);
+        this.resend = resend;
+    }
+
+    /**
+     * Identifies whether an exception was thrown during a resend of a
+     * message or not.  In this case a resend refers to whether the
+     * message is being resent from the buffer of messages internal.
+     * This would occur if we sent 5 messages quickly to APNS:
+     * 1,2,3,4,5 and the 3 message was rejected.  We would
+     * then need to resend 4 and 5.  If a network exception was
+     * triggered when doing this, then the resend flag will be
+     * {@code true}.
+     * @return {@code true} for an exception trigger during a resend, otherwise {@code false}.
+     */
+    public boolean isResend() {
+        return resend;
+    }
+
+}

+ 50 - 0
src/main/java/com/notnoop/exceptions/RuntimeIOException.java

@@ -0,0 +1,50 @@
+/*
+ *  Copyright 2009, Mahmood Ali.
+ *  All rights reserved.
+ *
+ *  Redistribution and use in source and binary forms, with or without
+ *  modification, are permitted provided that the following conditions are
+ *  met:
+ *
+ *    * Redistributions of source code must retain the above copyright
+ *      notice, this list of conditions and the following disclaimer.
+ *    * Redistributions in binary form must reproduce the above
+ *      copyright notice, this list of conditions and the following disclaimer
+ *      in the documentation and/or other materials provided with the
+ *      distribution.
+ *    * Neither the name of Mahmood Ali. nor the names of its
+ *      contributors may be used to endorse or promote products derived from
+ *      this software without specific prior written permission.
+ *
+ *  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ *  "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ *  LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ *  A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ *  OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ *  SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ *  LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ *  DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ *  THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ *  (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ *  OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+package com.notnoop.exceptions;
+
+import java.io.IOException;
+
+/**
+ * Signals that an I/O exception of some sort has occurred. This
+ * class is the general class of exceptions produced by failed or
+ * interrupted I/O operations.
+ *
+ * This is a RuntimeException, unlike the java.io.IOException
+ */
+public class RuntimeIOException extends ApnsException {
+    private static final long serialVersionUID = 8665285084049041306L;
+
+    public RuntimeIOException()                      { super(); }
+    public RuntimeIOException(String message)        { super(message); }
+    public RuntimeIOException(IOException cause)       { super(cause); }
+    public RuntimeIOException(String m, IOException c) { super(m, c); }
+
+}

BIN
src/main/libs/MiPush_SDK_Server_2_2_19.jar


+ 9 - 0
src/main/resources/apns.properties

@@ -0,0 +1,9 @@
+apns.product_cer_path=apns/product.p12
+apns.product_cer_pwd=123456
+apns.develop_cer_path=apns/develop.p12
+apns.develop_cer_pwd=123456
+apns.voip_cer_path=apns/voip.p12
+apns.voip_cer_pwd=123456
+
+apns.alert=default
+apns.voip_alert=ring.caf

+ 1 - 0
src/main/resources/application.properties

@@ -0,0 +1 @@
+server.port=8085

+ 2 - 0
src/main/resources/hms.properties

@@ -0,0 +1,2 @@
+hms.appSecret=a4e5e6a0c8a5d8424aba5a8f0aae3d0c
+hms.appId=100221325

+ 2 - 0
src/main/resources/meizu.properties

@@ -0,0 +1,2 @@
+meizu.appSecret=098f6939499a44328fe201eb82d01fb2
+meizu.appId=113616

+ 1 - 0
src/main/resources/xiaomi.properties

@@ -0,0 +1 @@
+xiaomi.appSecret=66nAHUMwmGz042clVI5bVg==

+ 16 - 0
src/test/java/cn/wildfirechat/push/PushApplicationTests.java

@@ -0,0 +1,16 @@
+package cn.wildfirechat.push;
+
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.junit4.SpringRunner;
+
+@RunWith(SpringRunner.class)
+@SpringBootTest
+public class PushApplicationTests {
+
+	@Test
+	public void contextLoads() {
+	}
+
+}