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;
+
+}
diff --git a/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java b/src/main/java/com/notnoop/apns/ApnsServiceBuilder.java
new file mode 100755
index 0000000..abc01f3
--- /dev/null
+++ b/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:
+ *
+ *
+ * ApnsService = APNS.newService()
+ * .withCert("/path/to/certificate.p12", "MyCertPassword")
+ * .withSandboxDestination()
+ * .build()
+ *
+ */
+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
+ * Bug 6415637. 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
+ * Bug 6415637. 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
+ * Bug 6415637. 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
+ * Bug 6415637. 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
+ * Bug 6415637. 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.
+ *
+ * Read the
+ * Java Networking and Proxies guide to understand the
+ * proxies complexity.
+ *
+ *
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.
+ *
+ *
Read the
+ * Java Networking and Proxies 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
+ *
+ *
Read the
+ * Java Networking and Proxies 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 waitTimeInSec (set as 5sec) for more request to come
+ * before executing but not more than maxWaitTimeInSec (set as 10sec)
+ *
+ * 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 waitTimeInSec for more request to come
+ * before executing but not more than maxWaitTimeInSec
+ *
+ * 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 waitTimeInSec for more request to come
+ * before executing but not more than maxWaitTimeInSec
+ *
+ * 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 waitTimeInSec for more request to come
+ * before executing but not more than maxWaitTimeInSec
+ *
+ * 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().");
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/DeliveryError.java b/src/main/java/com/notnoop/apns/DeliveryError.java
new file mode 100755
index 0000000..4ba33f6
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/EnhancedApnsNotification.java b/src/main/java/com/notnoop/apns/EnhancedApnsNotification.java
new file mode 100755
index 0000000..b5b2661
--- /dev/null
+++ b/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+")";
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/PayloadBuilder.java b/src/main/java/com/notnoop/apns/PayloadBuilder.java
new file mode 100755
index 0000000..798c22a
--- /dev/null
+++ b/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 root;
+ private final Map aps;
+ private final Map customAlert;
+
+ /**
+ * Constructs a new instance of {@code PayloadBuilder}
+ */
+ PayloadBuilder() {
+ root = new HashMap();
+ aps = new HashMap();
+ customAlert = new HashMap();
+ }
+
+ /**
+ * 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 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 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 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 root,
+ final Map aps,
+ final Map customAlert) {
+ this.root = new HashMap(root);
+ this.aps = new HashMap(aps);
+ this.customAlert = new HashMap(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();
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/ReconnectPolicy.java b/src/main/java/com/notnoop/apns/ReconnectPolicy.java
new file mode 100755
index 0000000..bff7dd9
--- /dev/null
+++ b/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.
+ *
+ * Apple recommends using a persistent connection. This improves the latency of sending push notification messages.
+ *
+ * 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.
+ *
+ * This is the recommended mode.
+ *
+ * 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 a lot 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();
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/SimpleApnsNotification.java b/src/main/java/com/notnoop/apns/SimpleApnsNotification.java
new file mode 100755
index 0000000..6c2dad7
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java b/src/main/java/com/notnoop/apns/StartSendingApnsDelegate.java
new file mode 100755
index 0000000..cf7d957
--- /dev/null
+++ b/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);
+
+}
diff --git a/src/main/java/com/notnoop/apns/internal/AbstractApnsService.java b/src/main/java/com/notnoop/apns/internal/AbstractApnsService.java
new file mode 100755
index 0000000..5a721e9
--- /dev/null
+++ b/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 getInactiveDevices() throws NetworkIOException {
+ return feedback.getInactiveDevices();
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/internal/ApnsConnection.java b/src/main/java/com/notnoop/apns/internal/ApnsConnection.java
new file mode 100755
index 0000000..757a81b
--- /dev/null
+++ b/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();
+}
diff --git a/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java b/src/main/java/com/notnoop/apns/internal/ApnsConnectionImpl.java
new file mode 100755
index 0000000..825aea3
--- /dev/null
+++ b/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 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();
+ notificationsBuffer = new ConcurrentLinkedQueue();
+ }
+
+ 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 tempCache = new LinkedList();
+ 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;
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/internal/ApnsFeedbackConnection.java b/src/main/java/com/notnoop/apns/internal/ApnsFeedbackConnection.java
new file mode 100755
index 0000000..26db208
--- /dev/null
+++ b/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 getInactiveDevices() throws NetworkIOException {
+ int attempts = 0;
+ while (true) {
+ try {
+ attempts++;
+ final Map 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 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);
+ }
+ }
+
+}
diff --git a/src/main/java/com/notnoop/apns/internal/ApnsPooledConnection.java b/src/main/java/com/notnoop/apns/internal/ApnsPooledConnection.java
new file mode 100755
index 0000000..e27d170
--- /dev/null
+++ b/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 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();
+ }
+
+ private final ThreadLocal uniquePrototype =
+ new ThreadLocal() {
+ protected ApnsConnection initialValue() {
+ ApnsConnection newCopy = prototype.copy();
+ prototypes.add(newCopy);
+ return newCopy;
+ }
+ };
+
+ public void sendMessage(final ApnsNotification m) throws NetworkIOException {
+ Future future = executors.submit(new Callable() {
+ 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();
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/internal/ApnsServiceImpl.java b/src/main/java/com/notnoop/apns/internal/ApnsServiceImpl.java
new file mode 100755
index 0000000..387f5a8
--- /dev/null
+++ b/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();
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/internal/BatchApnsService.java b/src/main/java/com/notnoop/apns/internal/BatchApnsService.java
new file mode 100755
index 0000000..d7dcbef
--- /dev/null
+++ b/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 batch = new ConcurrentLinkedQueue();
+
+ 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);
+ }
+ }
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/internal/QueuedApnsService.java b/src/main/java/com/notnoop/apns/internal/QueuedApnsService.java
new file mode 100755
index 0000000..21cc820
--- /dev/null
+++ b/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 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();
+ 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 getInactiveDevices() throws NetworkIOException {
+ return service.getInactiveDevices();
+ }
+
+ public void testConnection() throws NetworkIOException {
+ service.testConnection();
+ }
+
+}
diff --git a/src/main/java/com/notnoop/apns/internal/ReconnectPolicies.java b/src/main/java/com/notnoop/apns/internal/ReconnectPolicies.java
new file mode 100755
index 0000000..b6a70eb
--- /dev/null
+++ b/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();
+ }
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java b/src/main/java/com/notnoop/apns/internal/SSLContextBuilder.java
new file mode 100755
index 0000000..5175bfd
--- /dev/null
+++ b/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);
+ }
+ }
+}
diff --git a/src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java b/src/main/java/com/notnoop/apns/internal/TlsTunnelBuilder.java
new file mode 100755
index 0000000..5e0d68c
--- /dev/null
+++ b/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 RFC 2817 5.2. 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 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;
+ }
+
+}
+
diff --git a/src/main/java/com/notnoop/apns/internal/Utilities.java b/src/main/java/com/notnoop/apns/internal/Utilities.java
new file mode 100755
index 0000000..1849098
--- /dev/null
+++ b/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 parseFeedbackStreamRaw(final InputStream in) {
+ final Map result = new HashMap();
+
+ 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 parseFeedbackStream(final InputStream in) {
+ final Map result = new HashMap();
+
+ final Map raw = parseFeedbackStreamRaw(in);
+ for (final Map.Entry 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;
+ }
+
+}
diff --git a/src/main/java/com/notnoop/exceptions/ApnsDeliveryErrorException.java b/src/main/java/com/notnoop/exceptions/ApnsDeliveryErrorException.java
new file mode 100755
index 0000000..c9b3bea
--- /dev/null
+++ b/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;
+ }
+
+
+}
diff --git a/src/main/java/com/notnoop/exceptions/ApnsException.java b/src/main/java/com/notnoop/exceptions/ApnsException.java
new file mode 100755
index 0000000..f1725e5
--- /dev/null
+++ b/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); }
+
+}
diff --git a/src/main/java/com/notnoop/exceptions/InvalidSSLConfig.java b/src/main/java/com/notnoop/exceptions/InvalidSSLConfig.java
new file mode 100755
index 0000000..be97578
--- /dev/null
+++ b/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:
+ *
+ *
+ * - {@link KeyStoreException}
+ * - {@link NoSuchAlgorithmException}
+ * - {@link CertificateException}
+ * - {@link IOException}
+ * - {@link UnrecoverableKeyException}
+ * - {@link KeyManagementException}
+ *
+ *
+ */
+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); }
+
+}
diff --git a/src/main/java/com/notnoop/exceptions/NetworkIOException.java b/src/main/java/com/notnoop/exceptions/NetworkIOException.java
new file mode 100755
index 0000000..df84eb5
--- /dev/null
+++ b/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;
+ }
+
+}
diff --git a/src/main/java/com/notnoop/exceptions/RuntimeIOException.java b/src/main/java/com/notnoop/exceptions/RuntimeIOException.java
new file mode 100755
index 0000000..1a447bd
--- /dev/null
+++ b/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); }
+
+}
diff --git a/src/main/libs/MiPush_SDK_Server_2_2_19.jar b/src/main/libs/MiPush_SDK_Server_2_2_19.jar
new file mode 100644
index 0000000..2aea5d1
Binary files /dev/null and b/src/main/libs/MiPush_SDK_Server_2_2_19.jar differ
diff --git a/src/main/resources/apns.properties b/src/main/resources/apns.properties
new file mode 100644
index 0000000..85c9a0d
--- /dev/null
+++ b/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
\ No newline at end of file
diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties
new file mode 100644
index 0000000..a30a91f
--- /dev/null
+++ b/src/main/resources/application.properties
@@ -0,0 +1 @@
+server.port=8085
\ No newline at end of file
diff --git a/src/main/resources/hms.properties b/src/main/resources/hms.properties
new file mode 100644
index 0000000..d5900b4
--- /dev/null
+++ b/src/main/resources/hms.properties
@@ -0,0 +1,2 @@
+hms.appSecret=a4e5e6a0c8a5d8424aba5a8f0aae3d0c
+hms.appId=100221325
\ No newline at end of file
diff --git a/src/main/resources/meizu.properties b/src/main/resources/meizu.properties
new file mode 100644
index 0000000..46eadd1
--- /dev/null
+++ b/src/main/resources/meizu.properties
@@ -0,0 +1,2 @@
+meizu.appSecret=098f6939499a44328fe201eb82d01fb2
+meizu.appId=113616
\ No newline at end of file
diff --git a/src/main/resources/xiaomi.properties b/src/main/resources/xiaomi.properties
new file mode 100644
index 0000000..3072ae4
--- /dev/null
+++ b/src/main/resources/xiaomi.properties
@@ -0,0 +1 @@
+xiaomi.appSecret=66nAHUMwmGz042clVI5bVg==
\ No newline at end of file
diff --git a/src/test/java/cn/wildfirechat/push/PushApplicationTests.java b/src/test/java/cn/wildfirechat/push/PushApplicationTests.java
new file mode 100644
index 0000000..dc48b44
--- /dev/null
+++ b/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() {
+ }
+
+}