/*
* 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.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.ThreadFactory;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import com.notnoop.apns.internal.*;
import com.notnoop.exceptions.InvalidSSLConfig;
import com.notnoop.exceptions.RuntimeIOException;
import static com.notnoop.apns.internal.Utilities.*;
/**
* The class is used to create instances of {@link ApnsService}.
*
* Note that this class is not synchronized. If multiple threads access a
* {@code ApnsServiceBuilder} instance concurrently, and at least on of the
* threads modifies one of the attributes structurally, it must be
* synchronized externally.
*
* Starting a new {@code ApnsService} is easy:
*
* <pre>
* ApnsService = APNS.newService()
* .withCert("/path/to/certificate.p12", "MyCertPassword")
* .withSandboxDestination()
* .build()
* </pre>
*/
public class ApnsServiceBuilder {
private static final String KEYSTORE_TYPE = "PKCS12";
private static final String KEY_ALGORITHM = ((java.security.Security.getProperty("ssl.KeyManagerFactory.algorithm") == null)? "sunx509" : java.security.Security.getProperty("ssl.KeyManagerFactory.algorithm"));
private SSLContext sslContext;
private int readTimeout = 0;
private int connectTimeout = 0;
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 = null;
private ReconnectPolicy reconnectPolicy = ReconnectPolicy.Provided.EVERY_HALF_HOUR.newObject();
private boolean isQueued = false;
private ThreadFactory queueThreadFactory = null;
private boolean isBatched = false;
private int batchWaitTimeInSec;
private int batchMaxWaitTimeInSec;
private ThreadFactory batchThreadFactory = null;
private ApnsDelegate delegate = ApnsDelegate.EMPTY;
private Proxy proxy = null;
private String proxyUsername = null;
private String proxyPassword = null;
private boolean errorDetection = true;
private ThreadFactory errorDetectionThreadFactory = null;
/**
* Constructs a new instance of {@code ApnsServiceBuilder}
*/
public ApnsServiceBuilder() { sslContext = null; }
/**
* Specify the certificate used to connect to Apple APNS
* servers. This relies on the path (absolute or relative to
* working path) to the keystore (*.p12) containing the
* certificate, along with the given password.
*
* The keystore needs to be of PKCS12 and the keystore
* needs to be encrypted using the SunX509 algorithm. Both
* of these settings are the default.
*
* This library does not support password-less p12 certificates, due to a
* Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
* Bug 6415637</a>. There are three workarounds: use a password-protected
* certificate, use a different boot Java SDK implementation, or construct
* the `SSLContext` yourself! Needless to say, the password-protected
* certificate is most recommended option.
*
* @param fileName the path to the certificate
* @param password the password of the keystore
* @return this
* @throws RuntimeIOException if it {@code fileName} cannot be
* found or read
* @throws InvalidSSLConfig if fileName is invalid Keystore
* or the password is invalid
*/
public ApnsServiceBuilder withCert(String fileName, String password)
throws RuntimeIOException, InvalidSSLConfig {
FileInputStream stream = null;
try {
stream = new FileInputStream(fileName);
return withCert(stream, password);
} catch (FileNotFoundException e) {
throw new RuntimeIOException(e);
} finally {
Utilities.close(stream);
}
}
/**
* Specify the certificate used to connect to Apple APNS
* servers. This relies on the stream of keystore (*.p12)
* containing the certificate, along with the given password.
*
* The keystore needs to be of PKCS12 and the keystore
* needs to be encrypted using the SunX509 algorithm. Both
* of these settings are the default.
*
* This library does not support password-less p12 certificates, due to a
* Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
* Bug 6415637</a>. There are three workarounds: use a password-protected
* certificate, use a different boot Java SDK implementation, or constract
* the `SSLContext` yourself! Needless to say, the password-protected
* certificate is most recommended option.
*
* @param stream the keystore represented as input stream
* @param password the password of the keystore
* @return this
* @throws InvalidSSLConfig if stream is invalid Keystore
* or the password is invalid
*/
public ApnsServiceBuilder withCert(InputStream stream, String password)
throws InvalidSSLConfig {
assertPasswordNotEmpty(password);
return withSSLContext(
newSSLContext(stream, password,
KEYSTORE_TYPE, KEY_ALGORITHM));
}
/**
* Specify the certificate used to connect to Apple APNS
* servers. This relies on a keystore (*.p12)
* containing the certificate, along with the given password.
*
* This library does not support password-less p12 certificates, due to a
* Oracle Java library <a href="http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6415637">
* Bug 6415637</a>. There are three workarounds: use a password-protected
* certificate, use a different boot Java SDK implementation, or construct
* the `SSLContext` yourself! Needless to say, the password-protected
* certificate is most recommended option.
*
* @param keyStore the keystore
* @param password the password of the keystore
* @return this
* @throws InvalidSSLConfig if stream is invalid Keystore
* or the password is invalid
*/
public ApnsServiceBuilder withCert(KeyStore keyStore, String password)
throws InvalidSSLConfig {
assertPasswordNotEmpty(password);
return withSSLContext(
newSSLContext(keyStore, password, KEY_ALGORITHM));
}
private void assertPasswordNotEmpty(String password) {
if (password == null || password.length() == 0) {
throw new IllegalArgumentException("Passwords must be specified." +
"Oracle Java SDK does not support passwordless p12 certificates");
}
}
/**
* Specify the SSLContext that should be used to initiate the
* connection to Apple Server.
*
* Most clients would use {@link #withCert(InputStream, String)}
* or {@link #withCert(String, String)} instead. But some
* clients may need to represent the Keystore in a different
* format than supported.
*
* @param sslContext Context to be used to create secure connections
* @return this
*/
public ApnsServiceBuilder withSSLContext(SSLContext sslContext) {
this.sslContext = sslContext;
return this;
}
/**
* Specify the timeout value to be set in new setSoTimeout in created
* sockets, for both feedback and push connections, in milliseconds.
* @param readTimeout timeout value to be set in new setSoTimeout
* @return this
*/
public ApnsServiceBuilder withReadTimeout(int readTimeout) {
this.readTimeout = readTimeout;
return this;
}
/**
* Specify the timeout value to use for connectionTimeout in created
* sockets, for both feedback and push connections, in milliseconds.
* @param connectTimeout timeout value to use for connectionTimeout
* @return this
*/
public ApnsServiceBuilder withConnectTimeout(int connectTimeout) {
this.connectTimeout = connectTimeout;
return this;
}
/**
* Specify the gateway server for sending Apple iPhone
* notifications.
*
* Most clients should use {@link #withSandboxDestination()}
* or {@link #withProductionDestination()}. Clients may use
* this method to connect to mocking tests and such.
*
* @param host hostname the notification gateway of Apple
* @param port port of the notification gateway of Apple
* @return this
*/
public ApnsServiceBuilder withGatewayDestination(String host, int port) {
this.gatewayHost = host;
this.gatewayPort = port;
return this;
}
/**
* Specify the Feedback for getting failed devices from
* Apple iPhone Push servers.
*
* Most clients should use {@link #withSandboxDestination()}
* or {@link #withProductionDestination()}. Clients may use
* this method to connect to mocking tests and such.
*
* @param host hostname of the feedback server of Apple
* @param port port of the feedback server of Apple
* @return this
*/
public ApnsServiceBuilder withFeedbackDestination(String host, int port) {
this.feedbackHost = host;
this.feedbackPort = port;
return this;
}
/**
* Specify to use Apple servers as iPhone gateway and feedback servers.
*
* If the passed {@code isProduction} is true, then it connects to the
* production servers, otherwise, it connects to the sandbox servers
*
* @param isProduction determines which Apple servers should be used:
* production or sandbox
* @return this
*/
public ApnsServiceBuilder withAppleDestination(boolean isProduction) {
if (isProduction) {
return withProductionDestination();
} else {
return withSandboxDestination();
}
}
/**
* Specify to use the Apple sandbox servers as iPhone gateway
* and feedback servers.
*
* This is desired when in testing and pushing notifications
* with a development provision.
*
* @return this
*/
public ApnsServiceBuilder withSandboxDestination() {
return withGatewayDestination(SANDBOX_GATEWAY_HOST, SANDBOX_GATEWAY_PORT)
.withFeedbackDestination(SANDBOX_FEEDBACK_HOST, SANDBOX_FEEDBACK_PORT);
}
/**
* Specify to use the Apple Production servers as iPhone gateway
* and feedback servers.
*
* This is desired when sending notifications to devices with
* a production provision (whether through App Store or Ad hoc
* distribution).
*
* @return this
*/
public ApnsServiceBuilder withProductionDestination() {
return withGatewayDestination(PRODUCTION_GATEWAY_HOST, PRODUCTION_GATEWAY_PORT)
.withFeedbackDestination(PRODUCTION_FEEDBACK_HOST, PRODUCTION_FEEDBACK_PORT);
}
/**
* Specify the reconnection policy for the socket connection.
*
* Note: This option has no effect when using non-blocking
* connections.
*/
public ApnsServiceBuilder withReconnectPolicy(ReconnectPolicy rp) {
this.reconnectPolicy = rp;
return this;
}
/**
* Specify if the notification cache should auto adjust.
* Default is true
*
* @param autoAdjustCacheLength the notification cache should auto adjust.
* @return this
*/
public ApnsServiceBuilder withAutoAdjustCacheLength(boolean autoAdjustCacheLength) {
this.autoAdjustCacheLength = autoAdjustCacheLength;
return this;
}
/**
* Specify the reconnection policy for the socket connection.
*
* Note: This option has no effect when using non-blocking
* connections.
*/
public ApnsServiceBuilder withReconnectPolicy(ReconnectPolicy.Provided rp) {
this.reconnectPolicy = rp.newObject();
return this;
}
/**
* Specify the address of the SOCKS proxy the connection should
* use.
*
* <p>Read the <a href="http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html">
* Java Networking and Proxies</a> guide to understand the
* proxies complexity.
*
* <p>Be aware that this method only handles SOCKS proxies, not
* HTTPS proxies. Use {@link #withProxy(Proxy)} instead.
*
* @param host the hostname of the SOCKS proxy
* @param port the port of the SOCKS proxy server
* @return this
*/
public ApnsServiceBuilder withSocksProxy(String host, int port) {
Proxy proxy = new Proxy(Proxy.Type.SOCKS,
new InetSocketAddress(host, port));
return withProxy(proxy);
}
/**
* Specify the proxy and the authentication parameters to be used
* to establish the connections to Apple Servers.
*
* <p>Read the <a href="http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html">
* Java Networking and Proxies</a> guide to understand the
* proxies complexity.
*
* @param proxy the proxy object to be used to create connections
* @param proxyUsername a String object representing the username of the proxy server
* @param proxyPassword a String object representing the password of the proxy server
* @return this
*/
public ApnsServiceBuilder withAuthProxy(Proxy proxy, String proxyUsername, String proxyPassword) {
this.proxy = proxy;
this.proxyUsername = proxyUsername;
this.proxyPassword = proxyPassword;
return this;
}
/**
* Specify the proxy to be used to establish the connections
* to Apple Servers
*
* <p>Read the <a href="http://java.sun.com/javase/6/docs/technotes/guides/net/proxies.html">
* Java Networking and Proxies</a> guide to understand the
* proxies complexity.
*
* @param proxy the proxy object to be used to create connections
* @return this
*/
public ApnsServiceBuilder withProxy(Proxy proxy) {
this.proxy = proxy;
return this;
}
/**
* Specify the number of notifications to cache for error purposes.
* Default is 100
*
* @param cacheLength Number of notifications to cache for error purposes
* @return this
*/
public ApnsServiceBuilder withCacheLength(int cacheLength) {
this.cacheLength = cacheLength;
return this;
}
/**
* Specify the socket to be used as underlying socket to connect
* to the APN service.
*
* This assumes that the socket connects to a SOCKS proxy.
*
* @deprecated use {@link ApnsServiceBuilder#withProxy(Proxy)} instead
* @param proxySocket the underlying socket for connections
* @return this
*/
@Deprecated
public ApnsServiceBuilder withProxySocket(Socket proxySocket) {
return this.withProxy(new Proxy(Proxy.Type.SOCKS,
proxySocket.getRemoteSocketAddress()));
}
/**
* Constructs a pool of connections to the notification servers.
*
* Apple servers recommend using a pooled connection up to
* 15 concurrent persistent connections to the gateways.
*
* Note: This option has no effect when using non-blocking
* connections.
*/
public ApnsServiceBuilder asPool(int maxConnections) {
return asPool(Executors.newFixedThreadPool(maxConnections), maxConnections);
}
/**
* Constructs a pool of connections to the notification servers.
*
* Apple servers recommend using a pooled connection up to
* 15 concurrent persistent connections to the gateways.
*
* Note: This option has no effect when using non-blocking
* connections.
*
* Note: The maxConnections here is used as a hint to how many connections
* get created.
*/
public ApnsServiceBuilder asPool(ExecutorService executor, int maxConnections) {
this.pooledMax = maxConnections;
this.executor = executor;
return this;
}
/**
* Constructs a new thread with a processing queue to process
* notification requests.
*
* @return this
*/
public ApnsServiceBuilder asQueued() {
return asQueued(Executors.defaultThreadFactory());
}
/**
* Constructs a new thread with a processing queue to process
* notification requests.
*
* @param threadFactory
* thread factory to use for queue processing
* @return this
*/
public ApnsServiceBuilder asQueued(ThreadFactory threadFactory) {
this.isQueued = true;
this.queueThreadFactory = threadFactory;
return this;
}
/**
* Construct service which will process notification requests in batch.
* After each request batch will wait <code>waitTimeInSec (set as 5sec)</code> for more request to come
* before executing but not more than <code>maxWaitTimeInSec (set as 10sec)</code>
*
* Note: It is not recommended to use pooled connection
*/
public ApnsServiceBuilder asBatched() {
return asBatched(5, 10);
}
/**
* Construct service which will process notification requests in batch.
* After each request batch will wait <code>waitTimeInSec</code> for more request to come
* before executing but not more than <code>maxWaitTimeInSec</code>
*
* Note: It is not recommended to use pooled connection
*
* @param waitTimeInSec
* time to wait for more notification request before executing
* batch
* @param maxWaitTimeInSec
* maximum wait time for batch before executing
*/
public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec) {
return asBatched(waitTimeInSec, maxWaitTimeInSec, null);
}
/**
* Construct service which will process notification requests in batch.
* After each request batch will wait <code>waitTimeInSec</code> for more request to come
* before executing but not more than <code>maxWaitTimeInSec</code>
*
* Each batch creates new connection and close it after finished.
* In case reconnect policy is specified it will be applied by batch processing.
* E.g.: {@link ReconnectPolicy.Provided#EVERY_HALF_HOUR} will reconnect the connection in case batch is running for more than half an hour
*
* Note: It is not recommended to use pooled connection
*
* @param waitTimeInSec
* time to wait for more notification request before executing
* batch
* @param maxWaitTimeInSec
* maximum wait time for batch before executing
* @param threadFactory
* thread factory to use for batch processing
*/
public ApnsServiceBuilder asBatched(int waitTimeInSec, int maxWaitTimeInSec, ThreadFactory threadFactory) {
this.isBatched = true;
this.batchWaitTimeInSec = waitTimeInSec;
this.batchMaxWaitTimeInSec = maxWaitTimeInSec;
this.batchThreadFactory = threadFactory;
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, batchThreadFactory);
}
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().");
}
}