├── .codeclimate.yml ├── .gitignore ├── .editorconfig ├── src ├── main │ └── java │ │ └── javapns │ │ ├── devices │ │ ├── package.html │ │ ├── exceptions │ │ │ ├── package.html │ │ │ ├── InvalidDeviceTokenFormatException.java │ │ │ ├── NullIdException.java │ │ │ ├── NullDeviceTokenException.java │ │ │ ├── UnknownDeviceException.java │ │ │ └── DuplicateDeviceException.java │ │ ├── implementations │ │ │ ├── basic │ │ │ │ ├── package.html │ │ │ │ ├── BasicDeviceFactory.java │ │ │ │ └── BasicDevice.java │ │ │ ├── package.html │ │ │ └── jpa │ │ │ │ ├── package.html │ │ │ │ ├── README.md │ │ │ │ └── injection.xml │ │ ├── Device.java │ │ ├── DeviceFactory.java │ │ └── Devices.java │ │ ├── communication │ │ ├── package.html │ │ ├── exceptions │ │ │ ├── package.html │ │ │ ├── CommunicationException.java │ │ │ ├── InvalidKeystoreFormatException.java │ │ │ ├── KeystoreException.java │ │ │ ├── InvalidKeystorePasswordException.java │ │ │ ├── InvalidCertificateChainException.java │ │ │ └── InvalidKeystoreReferenceException.java │ │ ├── WrappedKeystore.java │ │ ├── ServerTrustingTrustManager.java │ │ ├── AppleServer.java │ │ ├── AppleServerBasicImpl.java │ │ ├── ProxyManager.java │ │ ├── ConnectionToAppleServer.java │ │ └── KeystoreManager.java │ │ ├── feedback │ │ ├── package.html │ │ ├── AppleFeedbackServer.java │ │ ├── ConnectionToFeedbackServer.java │ │ ├── AppleFeedbackServerBasicImpl.java │ │ └── FeedbackServiceManager.java │ │ ├── notification │ │ ├── package.html │ │ ├── exceptions │ │ │ ├── package.html │ │ │ ├── PayloadIsEmptyException.java │ │ │ ├── PayloadAlertAlreadyExistsException.java │ │ │ ├── ErrorResponsePacketReceivedException.java │ │ │ ├── PayloadMaxSizeExceededException.java │ │ │ └── PayloadMaxSizeProbablyExceededException.java │ │ ├── transmission │ │ │ ├── package.html │ │ │ ├── NotificationProgressListener.java │ │ │ └── PushQueue.java │ │ ├── management │ │ │ ├── package.html │ │ │ ├── RemovalPasswordPayload.java │ │ │ ├── WebClipPayload.java │ │ │ ├── WiFiPayload.java │ │ │ ├── VPNPayload.java │ │ │ ├── CalDAVPayload.java │ │ │ ├── APNPayload.java │ │ │ ├── CalendarSubscriptionPayload.java │ │ │ ├── RestrictionsPayload.java │ │ │ ├── MobileConfigPayload.java │ │ │ ├── PasswordPolicyPayload.java │ │ │ ├── SCEPPayload.java │ │ │ ├── LDAPPayload.java │ │ │ └── EmailPayload.java │ │ ├── AppleNotificationServer.java │ │ ├── ConnectionToNotificationServer.java │ │ ├── PayloadPerDevice.java │ │ ├── PushNotificationBigPayload.java │ │ ├── NewsstandNotificationPayload.java │ │ ├── AppleNotificationServerBasicImpl.java │ │ ├── ResponsePacketReader.java │ │ ├── ResponsePacket.java │ │ ├── PushedNotifications.java │ │ ├── PushedNotification.java │ │ ├── PushNotificationPayload.java │ │ └── Payload.java │ │ ├── package.html │ │ └── overview.html └── test │ └── java │ └── javapns │ └── test │ ├── package.html │ ├── FeedbackTest.java │ ├── TestFoundation.java │ ├── NotificationTest.java │ └── SpecificNotificationTests.java ├── .travis.yml ├── README.md ├── LICENSE └── pom.xml /.codeclimate.yml: -------------------------------------------------------------------------------- 1 | engines: 2 | fixme: 3 | enabled: true 4 | ratings: 5 | paths: [] 6 | exclude_paths: [] 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | 3 | .project 4 | .classpath 5 | .settings 6 | target 7 | 8 | # Package Files # 9 | *.jar 10 | *.war 11 | *.ear 12 | 13 | .idea 14 | *.iml 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | 6 | indent_style = space 7 | indent_size = 2 8 | 9 | end_of_line = lf 10 | 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true -------------------------------------------------------------------------------- /src/main/java/javapns/devices/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Classes representing mobile devices. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/test/java/javapns/test/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Testing tools for the javapns library. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Classes for communicating with Apple servers. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/feedback/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Classes for interacting with the Apple Feedback Service. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Classes for pushing notifications through Apple servers. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/exceptions/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Device-related exceptions thrown by the javapns library. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/basic/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | A basic non-persistent implementation for devices. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Concrete implementations of devices and device factories. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false 2 | 3 | language: java 4 | 5 | jdk: 6 | - oraclejdk8 7 | 8 | cache: 9 | apt: true 10 | directories: 11 | - $HOME/.m2 12 | 13 | script: 14 | - mvn clean compile -Dmaven.test.skip=true 15 | 16 | after_success: 17 | - mvn clean test jacoco:report coveralls:report 18 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Communication-related exceptions thrown by the javapns library. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/exceptions/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Notification-related exceptions thrown by the javapns library. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/jpa/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Details on implementing devices and device factories using JPA. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/transmission/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 | Specialized classes for transmitting notifications to large number of devices. 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/main/java/javapns/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 |

The JavaPNS library

10 |

See the javapns.Push class for easy push notifications.

11 | 12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/CommunicationException.java: -------------------------------------------------------------------------------- 1 | package javapns.communication.exceptions; 2 | 3 | public class CommunicationException extends Exception { 4 | private static final long serialVersionUID = 1286560293829685555L; 5 | 6 | public CommunicationException(final String message, final Exception cause) { 7 | super(message, cause); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/package.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 |

Specific payloads for Apple's MDM technology

10 | 11 |

MDM is not involved in Apple Push Notification, but uses the 12 | same communication technologies as APN.

13 |

Nevertheless, this package is provided in an effort to help 14 | javapns become more widely used.

15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/javapns/feedback/AppleFeedbackServer.java: -------------------------------------------------------------------------------- 1 | package javapns.feedback; 2 | 3 | import javapns.communication.AppleServer; 4 | 5 | /** 6 | * Interface representing a connection to an Apple Feedback Server 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | public interface AppleFeedbackServer extends AppleServer { 11 | String PRODUCTION_HOST = "feedback.push.apple.com"; 12 | int PRODUCTION_PORT = 2196; 13 | 14 | String DEVELOPMENT_HOST = "feedback.sandbox.push.apple.com"; 15 | int DEVELOPMENT_PORT = 2196; 16 | 17 | String getFeedbackServerHost(); 18 | 19 | int getFeedbackServerPort(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/jpa/README.md: -------------------------------------------------------------------------------- 1 | ### To support JPA 2 | 3 | 1. Implement javapns.devices.Device as a POJO 4 | 1. Implement javapns.devices.DeviceFactory to hook up to your own JPA-backed DAO 5 | 1. See "injection.xml" file for wiring JavaPNS using Spring 6 | 1. Define a getter/setter pair for pushNotificationManager and feedbackServiceManager on any Spring-managed bean to get access to fully-functional PushNotificationManager and FeedbackServiceManager 7 | 8 | Notes: 9 | 10 | - AppleNotificationServer and AppleFeedbackServer objects can also be injected instead of using *BasicImpl implementations 11 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/AppleNotificationServer.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import javapns.communication.AppleServer; 4 | 5 | /** 6 | * Interface representing a connection to an Apple Notification Server 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | public interface AppleNotificationServer extends AppleServer { 11 | String PRODUCTION_HOST = "gateway.push.apple.com"; 12 | int PRODUCTION_PORT = 2195; 13 | 14 | String DEVELOPMENT_HOST = "gateway.sandbox.push.apple.com"; 15 | int DEVELOPMENT_PORT = 2195; 16 | 17 | String getNotificationServerHost(); 18 | 19 | int getNotificationServerPort(); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/WrappedKeystore.java: -------------------------------------------------------------------------------- 1 | package javapns.communication; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.security.KeyStore; 6 | 7 | /** 8 | * Special wrapper for a KeyStore. 9 | * 10 | * @author Sylvain Pedneault 11 | */ 12 | class WrappedKeystore extends InputStream { 13 | private final KeyStore keystore; 14 | 15 | WrappedKeystore(final KeyStore keystore) { 16 | this.keystore = keystore; 17 | } 18 | 19 | public KeyStore getKeystore() { 20 | return keystore; 21 | } 22 | 23 | @Override 24 | public int read() throws IOException { 25 | return 0; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/exceptions/PayloadIsEmptyException.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.exceptions; 2 | 3 | /** 4 | * Thrown when a payload is empty. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | 9 | public class PayloadIsEmptyException extends Exception { 10 | 11 | private static final long serialVersionUID = 8142083854784121700L; 12 | 13 | public PayloadIsEmptyException() { 14 | super("Payload is empty"); 15 | } 16 | 17 | /** 18 | * Constructor with custom message 19 | * 20 | * @param message 21 | */ 22 | public PayloadIsEmptyException(final String message) { 23 | super(message); 24 | } 25 | 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/exceptions/InvalidDeviceTokenFormatException.java: -------------------------------------------------------------------------------- 1 | package javapns.devices.exceptions; 2 | 3 | /** 4 | * Thrown when a device token cannot be parsed (invalid format). 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | 9 | public class InvalidDeviceTokenFormatException extends Exception { 10 | 11 | private static final long serialVersionUID = -8571997399252125457L; 12 | 13 | public InvalidDeviceTokenFormatException(final String message) { 14 | super(message); 15 | } 16 | 17 | public InvalidDeviceTokenFormatException(final String token, final String problem) { 18 | super(String.format("Device token cannot be parsed, most likely because it contains invalid hexadecimal characters: %s in %s", problem, token)); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/RemovalPasswordPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | 5 | /** 6 | * An MDM payload for RemovalPassword. 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | class RemovalPasswordPayload extends MobileConfigPayload { 11 | public RemovalPasswordPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName) throws JSONException { 12 | super(payloadVersion, "com.apple.profileRemovalPassword", payloadOrganization, payloadIdentifier, payloadDisplayName); 13 | } 14 | 15 | public void setRemovalPasword(final String value) throws JSONException { 16 | getPayload().put("RemovalPassword", value); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/InvalidKeystoreFormatException.java: -------------------------------------------------------------------------------- 1 | package javapns.communication.exceptions; 2 | 3 | /** 4 | * Thrown when we try to contact Apple with an invalid keystore format. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | public class InvalidKeystoreFormatException extends KeystoreException { 9 | 10 | private static final long serialVersionUID = 8822634206752412121L; 11 | 12 | /** 13 | * Constructor 14 | */ 15 | public InvalidKeystoreFormatException() { 16 | super("Invalid keystore format! Make sure it is PKCS12..."); 17 | } 18 | 19 | /** 20 | * Constructor with custom message 21 | * 22 | * @param message 23 | */ 24 | public InvalidKeystoreFormatException(final String message) { 25 | super(message); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/KeystoreException.java: -------------------------------------------------------------------------------- 1 | package javapns.communication.exceptions; 2 | 3 | /** 4 | * Thrown when we try to contact Apple with an invalid keystore format. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | 9 | public class KeystoreException extends Exception { 10 | 11 | private static final long serialVersionUID = 2549063865160633139L; 12 | 13 | /** 14 | * Constructor with custom message 15 | * 16 | * @param message 17 | */ 18 | public KeystoreException(final String message) { 19 | super(message); 20 | } 21 | 22 | /** 23 | * Constructor with custom message 24 | * 25 | * @param message 26 | */ 27 | public KeystoreException(final String message, final Exception cause) { 28 | super(message, cause); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/transmission/NotificationProgressListener.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.transmission; 2 | 3 | /** 4 | *

An event listener for monitoring progress of NotificationThreads

5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | public interface NotificationProgressListener { 9 | void eventAllThreadsStarted(NotificationThreads notificationThreads); 10 | 11 | void eventThreadStarted(NotificationThread notificationThread); 12 | 13 | void eventThreadFinished(NotificationThread notificationThread); 14 | 15 | void eventConnectionRestarted(NotificationThread notificationThread); 16 | 17 | void eventAllThreadsFinished(NotificationThreads notificationThreads); 18 | 19 | void eventCriticalException(NotificationThread notificationThread, Exception exception); 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/exceptions/PayloadAlertAlreadyExistsException.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.exceptions; 2 | 3 | import org.json.JSONException; 4 | 5 | /** 6 | * Thrown when a payload exceeds the maximum size allowed. 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | 11 | public class PayloadAlertAlreadyExistsException extends JSONException { 12 | 13 | private static final long serialVersionUID = -4514511954076864373L; 14 | 15 | /** 16 | * Default constructor 17 | */ 18 | public PayloadAlertAlreadyExistsException() { 19 | super("Payload alert already exists"); 20 | } 21 | 22 | /** 23 | * Constructor with custom message 24 | * 25 | * @param message 26 | */ 27 | public PayloadAlertAlreadyExistsException(final String message) { 28 | super(message); 29 | } 30 | 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/InvalidKeystorePasswordException.java: -------------------------------------------------------------------------------- 1 | package javapns.communication.exceptions; 2 | 3 | /** 4 | * Thrown when we try to contact Apple with an invalid password for the keystore. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | 9 | public class InvalidKeystorePasswordException extends KeystoreException { 10 | 11 | private static final long serialVersionUID = 5973743951334025887L; 12 | 13 | /** 14 | * Constructor 15 | */ 16 | public InvalidKeystorePasswordException() { 17 | super("Invalid keystore password! Verify settings for connecting to Apple..."); 18 | } 19 | 20 | /** 21 | * Constructor with custom message 22 | * 23 | * @param message 24 | */ 25 | public InvalidKeystorePasswordException(final String message) { 26 | super(message); 27 | } 28 | 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/exceptions/ErrorResponsePacketReceivedException.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.exceptions; 2 | 3 | import javapns.notification.ResponsePacket; 4 | 5 | /** 6 | * Thrown when an error response packet was received from an APNS server. 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | 11 | public class ErrorResponsePacketReceivedException extends Exception { 12 | 13 | private static final long serialVersionUID = 5798868422603574079L; 14 | private final ResponsePacket packet; 15 | 16 | public ErrorResponsePacketReceivedException(final ResponsePacket packet) { 17 | super(String.format("An error response packet was received from the APNS server: %s", packet.getMessage())); 18 | this.packet = packet; 19 | } 20 | 21 | public ResponsePacket getPacket() { 22 | return packet; 23 | } 24 | 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/exceptions/NullIdException.java: -------------------------------------------------------------------------------- 1 | package javapns.devices.exceptions; 2 | 3 | /** 4 | * Thrown when the given id is null 5 | * 6 | * @author Maxime Peron 7 | */ 8 | 9 | public class NullIdException extends Exception { 10 | 11 | private static final long serialVersionUID = -2842793759970312540L; 12 | /* Custom message for this exception */ 13 | private final String message; 14 | 15 | /** 16 | * Constructor 17 | */ 18 | public NullIdException() { 19 | this.message = "Client already exists"; 20 | } 21 | 22 | /** 23 | * Constructor with custom message 24 | * 25 | * @param message 26 | */ 27 | public NullIdException(final String message) { 28 | this.message = message; 29 | } 30 | 31 | /** 32 | * String representation 33 | */ 34 | public String toString() { 35 | return this.message; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/jpa/injection.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/exceptions/NullDeviceTokenException.java: -------------------------------------------------------------------------------- 1 | package javapns.devices.exceptions; 2 | 3 | /** 4 | * Thrown when the given token is null 5 | * 6 | * @author Maxime Peron 7 | */ 8 | 9 | public class NullDeviceTokenException extends Exception { 10 | 11 | private static final long serialVersionUID = 208339461070934305L; 12 | /* Custom message for this exception */ 13 | private final String message; 14 | 15 | /** 16 | * Constructor 17 | */ 18 | public NullDeviceTokenException() { 19 | this.message = "Client already exists"; 20 | } 21 | 22 | /** 23 | * Constructor with custom message 24 | * 25 | * @param message 26 | */ 27 | public NullDeviceTokenException(final String message) { 28 | this.message = message; 29 | } 30 | 31 | /** 32 | * String representation 33 | */ 34 | public String toString() { 35 | return this.message; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/javapns/overview.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 6 | 7 | 8 | 9 |

JavaPNS 2.1

10 |

A Java library for interacting with Apple's Push Notification Service.

11 | 12 |

Usage

13 |

The simplest way of pushing notifications with JavaPNS is to use the javapns.Push class:

14 |

15 |   import javapns.Push;
16 | 
17 |   public class PushTest {
18 | 
19 |   public static void main(String[] args) {
20 | 
21 |   Push.alert("Hello World!", "keystore.p12", "keystore_password", false, "Your token");
22 |   }
23 |   }
24 | 
25 | 26 | For more details about using the library, see the on-line wiki at: 27 | 28 | http://code.google.com/p/javapns/w/list 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/exceptions/UnknownDeviceException.java: -------------------------------------------------------------------------------- 1 | package javapns.devices.exceptions; 2 | 3 | /** 4 | * Thrown when we try to retrieve a device that doesn't exist 5 | * 6 | * @author Maxime Peron 7 | */ 8 | 9 | public class UnknownDeviceException extends Exception { 10 | 11 | private static final long serialVersionUID = -322193098126184434L; 12 | /* Custom message for this exception */ 13 | private final String message; 14 | 15 | /** 16 | * Constructor 17 | */ 18 | public UnknownDeviceException() { 19 | this.message = "Unknown client"; 20 | } 21 | 22 | /** 23 | * Constructor with custom message 24 | * 25 | * @param message 26 | */ 27 | public UnknownDeviceException(final String message) { 28 | this.message = message; 29 | } 30 | 31 | /** 32 | * String representation 33 | */ 34 | public String toString() { 35 | return this.message; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/ServerTrustingTrustManager.java: -------------------------------------------------------------------------------- 1 | package javapns.communication; 2 | 3 | import javax.net.ssl.X509TrustManager; 4 | import java.security.cert.CertificateException; 5 | import java.security.cert.X509Certificate; 6 | 7 | /** 8 | * A trust manager that automatically trusts all servers. 9 | * Used to avoid having handshake errors with Apple's sandbox servers. 10 | * 11 | * @author Sylvain Pedneault 12 | */ 13 | class ServerTrustingTrustManager implements X509TrustManager { 14 | public void checkClientTrusted(final X509Certificate[] chain, final String authType) throws CertificateException { 15 | throw new CertificateException("Client is not trusted."); 16 | } 17 | 18 | public void checkServerTrusted(final X509Certificate[] chain, final String authType) { 19 | // trust all servers 20 | } 21 | 22 | public X509Certificate[] getAcceptedIssuers() { 23 | return null; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/exceptions/DuplicateDeviceException.java: -------------------------------------------------------------------------------- 1 | package javapns.devices.exceptions; 2 | 3 | /** 4 | * Thrown when a Device already exist and we try to add it a second time 5 | * 6 | * @author Maxime Peron 7 | */ 8 | 9 | public class DuplicateDeviceException extends Exception { 10 | 11 | private static final long serialVersionUID = -7116507420722667479L; 12 | /* Custom message for this exception */ 13 | private final String message; 14 | 15 | /** 16 | * Constructor 17 | */ 18 | public DuplicateDeviceException() { 19 | this.message = "Client already exists"; 20 | } 21 | 22 | /** 23 | * Constructor with custom message 24 | * 25 | * @param message 26 | */ 27 | public DuplicateDeviceException(final String message) { 28 | this.message = message; 29 | } 30 | 31 | /** 32 | * String representation 33 | */ 34 | public String toString() { 35 | return this.message; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/InvalidCertificateChainException.java: -------------------------------------------------------------------------------- 1 | package javapns.communication.exceptions; 2 | 3 | /** 4 | * Thrown when we try to contact Apple with an invalid keystore or certificate chain. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | public class InvalidCertificateChainException extends KeystoreException { 9 | 10 | private static final long serialVersionUID = -1978821654637371922L; 11 | 12 | /** 13 | * Constructor 14 | */ 15 | public InvalidCertificateChainException() { 16 | super("Invalid certificate chain! Verify that the keystore you provided was produced according to specs..."); 17 | } 18 | 19 | /** 20 | * Constructor with custom message 21 | * 22 | * @param message 23 | */ 24 | public InvalidCertificateChainException(final String message) { 25 | super("Invalid certificate chain (" + message + ")! Verify that the keystore you provided was produced according to specs..."); 26 | } 27 | 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/WebClipPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | /** 7 | * An MDM payload for WebClip. 8 | * 9 | * @author Sylvain Pedneault 10 | */ 11 | class WebClipPayload extends MobileConfigPayload { 12 | public WebClipPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String url, final String label) throws JSONException { 13 | super(payloadVersion, "com.apple.webClip.managed", payloadOrganization, payloadIdentifier, payloadDisplayName); 14 | final JSONObject payload = getPayload(); 15 | payload.put("URL", url); 16 | payload.put("Label", label); 17 | } 18 | 19 | public void setIcon(final Object data) throws JSONException { 20 | getPayload().put("Icon", data); 21 | } 22 | 23 | public void setIsRemovable(final boolean value) throws JSONException { 24 | getPayload().put("IsRemovable", value); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/ConnectionToNotificationServer.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import javapns.communication.ConnectionToAppleServer; 4 | import javapns.communication.exceptions.KeystoreException; 5 | 6 | import java.security.KeyStore; 7 | 8 | /** 9 | * Connection details specific to the Notification Service. 10 | * 11 | * @author Sylvain Pedneault 12 | */ 13 | public class ConnectionToNotificationServer extends ConnectionToAppleServer { 14 | public ConnectionToNotificationServer(final AppleNotificationServer server) throws KeystoreException { 15 | super(server); 16 | } 17 | 18 | public ConnectionToNotificationServer(final AppleNotificationServer server, final KeyStore keystore) throws KeystoreException { 19 | super(server, keystore); 20 | } 21 | 22 | @Override 23 | public String getServerHost() { 24 | return ((AppleNotificationServer) getServer()).getNotificationServerHost(); 25 | } 26 | 27 | @Override 28 | public int getServerPort() { 29 | return ((AppleNotificationServer) getServer()).getNotificationServerPort(); 30 | } 31 | 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/PayloadPerDevice.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import javapns.devices.Device; 4 | import javapns.devices.exceptions.InvalidDeviceTokenFormatException; 5 | import javapns.devices.implementations.basic.BasicDevice; 6 | 7 | /** 8 | * A one-to-one link between a payload and device. 9 | * Provides support for a typical payload-per-device scenario. 10 | * 11 | * @author Sylvain Pedneault 12 | */ 13 | public class PayloadPerDevice { 14 | private final Payload payload; 15 | private final Device device; 16 | 17 | public PayloadPerDevice(final Payload payload, final String token) throws InvalidDeviceTokenFormatException { 18 | super(); 19 | this.payload = payload; 20 | this.device = new BasicDevice(token); 21 | } 22 | 23 | public PayloadPerDevice(final Payload payload, final Device device) { 24 | super(); 25 | this.payload = payload; 26 | this.device = device; 27 | } 28 | 29 | public Payload getPayload() { 30 | return payload; 31 | } 32 | 33 | public Device getDevice() { 34 | return device; 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/javapns/feedback/ConnectionToFeedbackServer.java: -------------------------------------------------------------------------------- 1 | package javapns.feedback; 2 | 3 | import javapns.communication.ConnectionToAppleServer; 4 | import javapns.communication.exceptions.KeystoreException; 5 | import javapns.notification.AppleNotificationServer; 6 | 7 | import java.security.KeyStore; 8 | 9 | /** 10 | * Class representing a connection to a specific Feedback Server. 11 | * 12 | * @author Sylvain Pedneault 13 | */ 14 | public class ConnectionToFeedbackServer extends ConnectionToAppleServer { 15 | public ConnectionToFeedbackServer(final AppleFeedbackServer feedbackServer) throws KeystoreException { 16 | super(feedbackServer); 17 | } 18 | 19 | public ConnectionToFeedbackServer(final AppleNotificationServer server, final KeyStore keystore) throws KeystoreException { 20 | super(server, keystore); 21 | } 22 | 23 | @Override 24 | public String getServerHost() { 25 | return ((AppleFeedbackServer) getServer()).getFeedbackServerHost(); 26 | } 27 | 28 | @Override 29 | public int getServerPort() { 30 | return ((AppleFeedbackServer) getServer()).getFeedbackServerPort(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/PushNotificationBigPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import org.json.JSONException; 4 | 5 | public class PushNotificationBigPayload extends PushNotificationPayload { 6 | /* Maximum total length (serialized) of a payload */ 7 | private static final int MAXIMUM_PAYLOAD_LENGTH = 2048; 8 | 9 | private PushNotificationBigPayload() { 10 | super(); 11 | } 12 | 13 | private PushNotificationBigPayload(final String rawJSON) throws JSONException { 14 | super(rawJSON); 15 | } 16 | 17 | public static PushNotificationBigPayload complex() { 18 | return new PushNotificationBigPayload(); 19 | } 20 | 21 | public static PushNotificationBigPayload fromJSON(final String rawJSON) throws JSONException { 22 | return new PushNotificationBigPayload(rawJSON); 23 | } 24 | 25 | /** 26 | * Return the maximum payload size in bytes. 27 | * For APNS payloads, since iOS8, this method returns 2048. 28 | * 29 | * @return the maximum payload size in bytes (2048) 30 | */ 31 | @Override 32 | public int getMaximumPayloadSize() { 33 | return MAXIMUM_PAYLOAD_LENGTH; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/exceptions/InvalidKeystoreReferenceException.java: -------------------------------------------------------------------------------- 1 | package javapns.communication.exceptions; 2 | 3 | /** 4 | * Thrown when we try to contact Apple with an invalid keystore format. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | 9 | public class InvalidKeystoreReferenceException extends KeystoreException { 10 | 11 | private static final long serialVersionUID = 3144387163593035745L; 12 | 13 | /** 14 | * Constructor 15 | */ 16 | public InvalidKeystoreReferenceException() { 17 | super("Invalid keystore parameter. Must be InputStream, File, String (as a file path), or byte[]."); 18 | } 19 | 20 | /** 21 | * Constructor with custom message 22 | * 23 | * @param keystore 24 | */ 25 | public InvalidKeystoreReferenceException(final Object keystore) { 26 | super("Invalid keystore parameter (" + keystore + "). Must be InputStream, File, String (as a file path), or byte[]."); 27 | } 28 | 29 | /** 30 | * Constructor with custom message 31 | * 32 | * @param message 33 | */ 34 | public InvalidKeystoreReferenceException(final String message) { 35 | super(message); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/exceptions/PayloadMaxSizeExceededException.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.exceptions; 2 | 3 | /** 4 | * Thrown when a payload exceeds the maximum size allowed. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | 9 | public class PayloadMaxSizeExceededException extends Exception { 10 | 11 | private static final long serialVersionUID = 2896151447959250527L; 12 | 13 | /** 14 | * Default constructor 15 | */ 16 | public PayloadMaxSizeExceededException() { 17 | super("Total payload size exceeds allowed limit"); 18 | } 19 | 20 | public PayloadMaxSizeExceededException(final int maxSize) { 21 | super(String.format("Total payload size exceeds allowed limit (%s bytes max)", maxSize)); 22 | } 23 | 24 | public PayloadMaxSizeExceededException(final int maxSize, final int currentSize) { 25 | super(String.format("Total payload size exceeds allowed limit (payload is %s bytes, limit is %s)", currentSize, maxSize)); 26 | } 27 | 28 | /** 29 | * Constructor with custom message 30 | * 31 | * @param message 32 | */ 33 | public PayloadMaxSizeExceededException(final String message) { 34 | super(message); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/WiFiPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | /** 7 | * An MDM payload for Wi-Fi. 8 | * 9 | * @author Sylvain Pedneault 10 | */ 11 | class WiFiPayload extends MobileConfigPayload { 12 | public WiFiPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String ssidStr, final boolean hiddenNetwork, final String encryptionType) throws JSONException { 13 | super(payloadVersion, "com.apple.wifi.managed", payloadOrganization, payloadIdentifier, payloadDisplayName); 14 | final JSONObject payload = getPayload(); 15 | payload.put("SSID_STR", ssidStr); 16 | payload.put("HIDDEN_NETWORK", hiddenNetwork); 17 | payload.put("EncryptionType", encryptionType); 18 | } 19 | 20 | public void setPassword(final String value) throws JSONException { 21 | getPayload().put("Password", value); 22 | } 23 | 24 | public JSONObject addEAPClientConfiguration() throws JSONException { 25 | final JSONObject object = new JSONObject(); 26 | getPayload().put("EAPClientConfiguration", object); 27 | return object; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/VPNPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | /** 7 | * An MDM payload for VPN. 8 | * 9 | * @author Sylvain Pedneault 10 | */ 11 | class VPNPayload extends MobileConfigPayload { 12 | public static final String VPNTYPE_L2TP = "L2TP"; 13 | public static final String VPNTYPE_PPTP = "PPTP"; 14 | public static final String VPNTYPE_IP_SEC = "IPSec"; 15 | 16 | public VPNPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String userDefinedName, final boolean overridePrimary, final String vpnType) throws JSONException { 17 | super(payloadVersion, "com.apple.vpn.managed", payloadOrganization, payloadIdentifier, payloadDisplayName); 18 | final JSONObject payload = getPayload(); 19 | payload.put("UserDefinedName", userDefinedName); 20 | payload.put("OverridePrimary", overridePrimary); 21 | payload.put("VPNType", vpnType); 22 | } 23 | 24 | public JSONObject addPPP() throws JSONException { 25 | final JSONObject object = new JSONObject(); 26 | getPayload().put("PPP", object); 27 | return object; 28 | } 29 | 30 | public JSONObject addIPSec() throws JSONException { 31 | final JSONObject object = new JSONObject(); 32 | getPayload().put("IPSec", object); 33 | return object; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/exceptions/PayloadMaxSizeProbablyExceededException.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.exceptions; 2 | 3 | import org.json.JSONException; 4 | 5 | /** 6 | * Thrown when a payload is expected to exceed the maximum size allowed after adding a given property. 7 | * Invoke payload.setPayloadSizeEstimatedWhenAdding(false) to disable this automatic checking. 8 | * 9 | * @author Sylvain Pedneault 10 | */ 11 | 12 | public class PayloadMaxSizeProbablyExceededException extends JSONException { 13 | 14 | private static final long serialVersionUID = 580227446786047134L; 15 | 16 | /** 17 | * Default constructor 18 | */ 19 | public PayloadMaxSizeProbablyExceededException() { 20 | super("Total payload size will most likely exceed allowed limit"); 21 | } 22 | 23 | public PayloadMaxSizeProbablyExceededException(final int maxSize) { 24 | super(String.format("Total payload size will most likely exceed allowed limit (%s bytes max)", maxSize)); 25 | } 26 | 27 | public PayloadMaxSizeProbablyExceededException(final int maxSize, final int estimatedSize) { 28 | super(String.format("Total payload size will most likely exceed allowed limit (estimated to become %s bytes, limit is %s)", estimatedSize, maxSize)); 29 | } 30 | 31 | /** 32 | * Constructor with custom message 33 | * 34 | * @param message 35 | */ 36 | public PayloadMaxSizeProbablyExceededException(final String message) { 37 | super(message); 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## JavaPNS 2 | 3 | [![Build Status](https://travis-ci.org/mlaccetti/JavaPNS.svg?branch=develop)](https://travis-ci.org/mlaccetti/JavaPNS) 4 | [![Coverage Status](https://coveralls.io/repos/github/mlaccetti/JavaPNS/badge.svg?branch=master)](https://coveralls.io/github/mlaccetti/JavaPNS?branch=develop) 5 | [![Code Climate](https://codeclimate.com/github/mlaccetti/JavaPNS/badges/gpa.svg)](https://codeclimate.com/github/mlaccetti/JavaPNS) 6 | 7 | Apple Push Notification Service Provider for Java 8 | 9 | Fork of JavaPNS to include Maven support - http://code.google.com/p/javapns/ 10 | 11 | Java 1.8+ compatible 12 | 13 | ### Updates 14 | 15 | Version 2.3.2 released! 16 | 17 | #### 2.3.2 Changes 18 | * 1.8 tweaks 19 | * General cleanup and overhaul 20 | 21 | #### 2.3.1 Changes 22 | * PushNotificationBigPayload ```complex``` and ```fromJson``` methods fixed 23 | * Fix to make trust store work on IBM JVM 24 | 25 | #### 2.3 Changes 26 | * iOS>=8 bigger notification payload support (2KB) 27 | * iOS>=7 Silent push notifications support ("content-available":1) 28 | 29 | ### Installation through Central Maven Repository 30 | javapns is available on the Central Maven Repository. 31 | To use javapns-jdk16 in your project, please add the following dependency to your pom.xml file: 32 | ``` 33 | 34 | com.github.mlaccetti 35 | javapns 36 | 2.3.2 37 | 38 | ``` 39 | 40 | ### Cutting a Release 41 | 42 | `mvn -Drelease=true ...` 43 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/CalDAVPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | /** 7 | * An MDM payload for CalDAV. 8 | * 9 | * @author Sylvain Pedneault 10 | */ 11 | class CalDAVPayload extends MobileConfigPayload { 12 | 13 | public CalDAVPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String calDAVHostName, final String calDAVUsername, final boolean calDAVUseSSL) throws JSONException { 14 | super(payloadVersion, "com.apple.caldav.account", payloadOrganization, payloadIdentifier, payloadDisplayName); 15 | final JSONObject payload = getPayload(); 16 | payload.put("CalDAVHostName", calDAVHostName); 17 | payload.put("CalDAVUsername", calDAVUsername); 18 | payload.put("CalDAVUseSSL", calDAVUseSSL); 19 | } 20 | 21 | public void setCalDAVAccountDescription(final String value) throws JSONException { 22 | getPayload().put("CalDAVAccountDescription", value); 23 | } 24 | 25 | public void setCalDAVPassword(final String value) throws JSONException { 26 | getPayload().put("CalDAVPassword", value); 27 | } 28 | 29 | public void setCalDAVPort(final int value) throws JSONException { 30 | getPayload().put("CalDAVPort", value); 31 | } 32 | 33 | public void setCalDAVPrincipalURL(final String value) throws JSONException { 34 | getPayload().put("CalDAVPrincipalURL", value); 35 | } 36 | 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/APNPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | import java.util.Map; 7 | 8 | /** 9 | * An MDM payload for APN (Access Point Name). 10 | * 11 | * @author Sylvain Pedneault 12 | */ 13 | class APNPayload extends MobileConfigPayload { 14 | public APNPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final Map defaultsData, final String defaultsDomainName, final Map[] apns, final String apn, final String username) throws JSONException { 15 | super(payloadVersion, "com.apple.apn.managed", payloadOrganization, payloadIdentifier, payloadDisplayName); 16 | final JSONObject payload = getPayload(); 17 | payload.put("DefaultsData", defaultsData); 18 | payload.put("defaultsDomainName", defaultsDomainName); 19 | for (final Map apnsEntry : apns) { 20 | payload.put("apns", apnsEntry); 21 | } 22 | payload.put("apn", apn); 23 | payload.put("username", username); 24 | } 25 | 26 | public void setPassword(final APNPayload value) throws JSONException { 27 | getPayload().put("password", value); 28 | } 29 | 30 | public void setProxy(final String value) throws JSONException { 31 | getPayload().put("proxy", value); 32 | } 33 | 34 | public void setProxyPort(final int value) throws JSONException { 35 | getPayload().put("proxyPort", value); 36 | } 37 | 38 | } 39 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/CalendarSubscriptionPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | /** 7 | * An MDM payload for CalendarSubscription. 8 | * 9 | * @author Sylvain Pedneault 10 | */ 11 | class CalendarSubscriptionPayload extends MobileConfigPayload { 12 | 13 | public CalendarSubscriptionPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String subCalAccountHostName, final boolean subCalAccountUseSSL) throws JSONException { 14 | super(payloadVersion, "com.apple.caldav.account", payloadOrganization, payloadIdentifier, payloadDisplayName); 15 | final JSONObject payload = getPayload(); 16 | payload.put("SubCalAccountHostName", subCalAccountHostName); 17 | payload.put("SubCalAccountUseSSL", subCalAccountUseSSL); 18 | } 19 | 20 | public void setSubCalAccountDescription(final String value) throws JSONException { 21 | getPayload().put("SubCalAccountDescription", value); 22 | } 23 | 24 | public void setSubCalAccountUsername(final String value) throws JSONException { 25 | getPayload().put("SubCalAccountUsername", value); 26 | } 27 | 28 | public void setSubCalAccountPassword(final String value) throws JSONException { 29 | getPayload().put("SubCalAccountPassword", value); 30 | } 31 | 32 | public void setSubCalAccountUseSSL(final boolean value) throws JSONException { 33 | getPayload().put("SubCalAccountUseSSL", value); 34 | } 35 | 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/Device.java: -------------------------------------------------------------------------------- 1 | package javapns.devices; 2 | 3 | import java.sql.Timestamp; 4 | 5 | /** 6 | * This is the common interface for all Devices. 7 | * It allows the DeviceFactory to support multiple 8 | * implementations of Device (in-memory, JPA-backed, etc.) 9 | * 10 | * @author Sylvain Pedneault 11 | */ 12 | public interface Device { 13 | /** 14 | * An id representing a particular device. 15 | *

16 | * Note that this is a local reference to the device, 17 | * which is not related to the actual device UUID or 18 | * other device-specific identification. Most of the 19 | * time, this deviceId should be the same as the token. 20 | * 21 | * @return the device id 22 | */ 23 | String getDeviceId(); 24 | 25 | /** 26 | * An id representing a particular device. 27 | *

28 | * Note that this is a local reference to the device, 29 | * which is not related to the actual device UUID or 30 | * other device-specific identification. Most of the 31 | * time, this deviceId should be the same as the token. 32 | * 33 | * @param id the device id 34 | */ 35 | void setDeviceId(String id); 36 | 37 | /** 38 | * A device token. 39 | * 40 | * @return the device token 41 | */ 42 | String getToken(); 43 | 44 | /** 45 | * Set the device token 46 | * 47 | * @param token 48 | */ 49 | void setToken(String token); 50 | 51 | /** 52 | * @return the last register 53 | */ 54 | Timestamp getLastRegister(); 55 | 56 | /** 57 | * @param lastRegister the last register 58 | */ 59 | void setLastRegister(Timestamp lastRegister); 60 | } 61 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/RestrictionsPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | 5 | /** 6 | * An MDM payload for Restrictions. 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | class RestrictionsPayload extends MobileConfigPayload { 11 | public RestrictionsPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName) throws JSONException { 12 | super(payloadVersion, "com.apple.applicationaccess", payloadOrganization, payloadIdentifier, payloadDisplayName); 13 | } 14 | 15 | public void setAllowAppInstallation(final boolean value) throws JSONException { 16 | getPayload().put("allowAppInstallation", value); 17 | } 18 | 19 | public void setAllowCamera(final boolean value) throws JSONException { 20 | getPayload().put("allowCamera", value); 21 | } 22 | 23 | public void setAllowExplicitContent(final boolean value) throws JSONException { 24 | getPayload().put("allowExplicitContent", value); 25 | } 26 | 27 | public void setAllowScreenShot(final boolean value) throws JSONException { 28 | getPayload().put("allowScreenShot", value); 29 | } 30 | 31 | public void setAllowYouTube(final boolean value) throws JSONException { 32 | getPayload().put("allowYouTube", value); 33 | } 34 | 35 | public void setAllowiTunes(final boolean value) throws JSONException { 36 | getPayload().put("allowAppInstallation", value); 37 | } 38 | 39 | public void setAllowSafari(final boolean value) throws JSONException { 40 | getPayload().put("allowSafari", value); 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/DeviceFactory.java: -------------------------------------------------------------------------------- 1 | package javapns.devices; 2 | 3 | import javapns.devices.exceptions.DuplicateDeviceException; 4 | import javapns.devices.exceptions.NullDeviceTokenException; 5 | import javapns.devices.exceptions.NullIdException; 6 | import javapns.devices.exceptions.UnknownDeviceException; 7 | 8 | /** 9 | * This is the common interface for all DeviceFactories. 10 | * It allows the PushNotificationManager to support multiple 11 | * implementations of DeviceFactory (in-memory, JPA-backed, etc.) 12 | * 13 | * @author Sylvain Pedneault 14 | * @deprecated Phasing out DeviceFactory because it has become irrelevant in the new library architecture 15 | */ 16 | @Deprecated 17 | public interface DeviceFactory { 18 | /** 19 | * Add a device to the map 20 | * 21 | * @param id The local device id 22 | * @param token The device token 23 | * @return The device created 24 | * @throws DuplicateDeviceException 25 | * @throws NullIdException 26 | * @throws NullDeviceTokenException 27 | */ 28 | Device addDevice(String id, String token) throws Exception; 29 | 30 | /** 31 | * Get a device according to his id 32 | * 33 | * @param id The local device id 34 | * @return The device 35 | * @throws UnknownDeviceException 36 | * @throws NullIdException 37 | */ 38 | Device getDevice(String id) throws UnknownDeviceException, NullIdException; 39 | 40 | /** 41 | * Remove a device 42 | * 43 | * @param id The local device id 44 | * @throws UnknownDeviceException 45 | * @throws NullIdException 46 | */ 47 | void removeDevice(String id) throws UnknownDeviceException, NullIdException; 48 | 49 | } 50 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/MobileConfigPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import javapns.notification.Payload; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | 7 | /** 8 | * A payload template compatible with Apple Mobile Device Management's Config Payload specification (beta version). 9 | * 10 | * @author Sylvain Pedneault 11 | */ 12 | abstract class MobileConfigPayload extends Payload { 13 | private static long serialuuid = 10000000; 14 | 15 | MobileConfigPayload(final int payloadVersion, final String payloadType, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName) throws JSONException { 16 | this(payloadVersion, generateUUID(), payloadType, payloadOrganization, payloadIdentifier, payloadDisplayName); 17 | } 18 | 19 | private MobileConfigPayload(final int payloadVersion, final String payloadUUID, final String payloadType, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName) throws JSONException { 20 | super(); 21 | final JSONObject payload = getPayload(); 22 | payload.put("PayloadVersion", payloadVersion); 23 | payload.put("PayloadUUID", payloadUUID); 24 | payload.put("PayloadType", payloadType); 25 | payload.put("PayloadOrganization", payloadOrganization); 26 | payload.put("PayloadIdentifier", payloadIdentifier); 27 | payload.put("PayloadDisplayName", payloadDisplayName); 28 | } 29 | 30 | private static String generateUUID() { 31 | return System.nanoTime() + "." + (++serialuuid); 32 | } 33 | 34 | public void setPayloadDescription(final String description) throws JSONException { 35 | getPayload().put("PayloadDescription", description); 36 | } 37 | 38 | public void setPayloadRemovalDisallowed(final boolean disallowed) throws JSONException { 39 | getPayload().put("PayloadRemovalDisallowed", disallowed); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/javapns/test/FeedbackTest.java: -------------------------------------------------------------------------------- 1 | package javapns.test; 2 | 3 | import javapns.Push; 4 | import javapns.communication.exceptions.CommunicationException; 5 | import javapns.communication.exceptions.KeystoreException; 6 | import javapns.devices.Device; 7 | 8 | import java.util.List; 9 | 10 | /** 11 | * A command-line test facility for the Feedback Service. 12 | *

Example: java -cp "[required libraries]" FeedbackTest keystore.p12 mypass

13 | *

14 | *

By default, this test uses the sandbox service. To switch, add "production" as a third parameter:

15 | *

Example: java -cp "[required libraries]" FeedbackTest keystore.p12 mypass production

16 | * 17 | * @author Sylvain Pedneault 18 | */ 19 | class FeedbackTest extends TestFoundation { 20 | private FeedbackTest() { 21 | // empty 22 | } 23 | 24 | /** 25 | * Execute this class from the command line to run tests. 26 | * 27 | * @param args 28 | */ 29 | public static void main(final String[] args) { 30 | /* Verify that the test is being invoked */ 31 | if (!verifyCorrectUsage(FeedbackTest.class, args, "keystore-path", "keystore-password", "[production|sandbox]")) { 32 | return; 33 | } 34 | 35 | /* Get a list of inactive devices */ 36 | feedbackTest(args); 37 | } 38 | 39 | /** 40 | * Retrieves a list of inactive devices from the Feedback service. 41 | * 42 | * @param args 43 | */ 44 | private static void feedbackTest(final String[] args) { 45 | final String keystore = args[0]; 46 | final String password = args[1]; 47 | final boolean production = args.length >= 3 && args[2].equalsIgnoreCase("production"); 48 | try { 49 | final List devices = Push.feedback(keystore, password, production); 50 | 51 | for (final Device device : devices) { 52 | System.out.println("Inactive device: " + device.getToken()); 53 | } 54 | } catch (final CommunicationException | KeystoreException e) { 55 | e.printStackTrace(); 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/AppleServer.java: -------------------------------------------------------------------------------- 1 | package javapns.communication; 2 | 3 | import javapns.communication.exceptions.InvalidKeystoreReferenceException; 4 | 5 | import java.io.InputStream; 6 | 7 | /** 8 | * Common interface of all classes representing a connection to any Apple server. 9 | * Use AppleNotificationServer and AppleFeedbackServer interfaces for specific connections. 10 | * 11 | * @author Sylvain Pedneault 12 | */ 13 | public interface AppleServer { 14 | 15 | /** 16 | * Returns a stream to a keystore. 17 | * 18 | * @return an InputStream 19 | */ 20 | InputStream getKeystoreStream() throws InvalidKeystoreReferenceException; 21 | 22 | /** 23 | * Returns the keystore's password. 24 | * 25 | * @return a password matching the keystore 26 | */ 27 | String getKeystorePassword(); 28 | 29 | /** 30 | * Returns the format used to produce the keystore (typically PKCS12). 31 | * 32 | * @return a valid keystore format identifier 33 | */ 34 | String getKeystoreType(); 35 | 36 | /** 37 | * Get the proxy host address currently configured for this specific server. 38 | * A proxy might still be configured at the library or JVM levels. 39 | * Refer to {@link javapns.communication.ProxyManager} for more information. 40 | * 41 | * @return a proxy host, or null if none is configured 42 | */ 43 | String getProxyHost(); 44 | 45 | /** 46 | * Get the proxy port currently configured for this specific server. 47 | * A proxy might still be configured at the library or JVM levels. 48 | * Refer to {@link javapns.communication.ProxyManager} for more information. 49 | * 50 | * @return a network port, or 0 if no proxy is configured 51 | */ 52 | int getProxyPort(); 53 | 54 | /** 55 | * Configure a proxy to use for this specific server. 56 | * Use {@link javapns.communication.ProxyManager} to configure a proxy for the entire library instead. 57 | * 58 | * @param proxyHost proxy host address 59 | * @param proxyPort proxy host port 60 | */ 61 | void setProxy(String proxyHost, int proxyPort); 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/AppleServerBasicImpl.java: -------------------------------------------------------------------------------- 1 | package javapns.communication; 2 | 3 | import javapns.communication.exceptions.InvalidKeystoreReferenceException; 4 | import javapns.communication.exceptions.KeystoreException; 5 | 6 | import java.io.InputStream; 7 | 8 | /** 9 | * A basic and abstract implementation of the AppleServer interface 10 | * intended to facilitate rapid deployment. 11 | * 12 | * @author Sylvain Pedneault 13 | */ 14 | public abstract class AppleServerBasicImpl implements AppleServer { 15 | private final String password; 16 | private final String type; 17 | private Object keystore; 18 | private String proxyHost; 19 | private int proxyPort; 20 | 21 | /** 22 | * Constructs a AppleServerBasicImpl object. 23 | * 24 | * @param keystore The keystore to use (can be a File, an InputStream, a String for a file path, or a byte[] array) 25 | * @param password The keystore's password 26 | * @param type The keystore type (typically PKCS12) 27 | * @throws KeystoreException thrown if an error occurs when loading the keystore 28 | */ 29 | protected AppleServerBasicImpl(final Object keystore, final String password, final String type) throws KeystoreException { 30 | KeystoreManager.validateKeystoreParameter(keystore); 31 | this.keystore = keystore; 32 | this.password = password; 33 | this.type = type; 34 | 35 | /* Make sure that the keystore reference is reusable. */ 36 | this.keystore = KeystoreManager.ensureReusableKeystore(this, this.keystore); 37 | } 38 | 39 | public InputStream getKeystoreStream() throws InvalidKeystoreReferenceException { 40 | return KeystoreManager.streamKeystore(keystore); 41 | } 42 | 43 | public String getKeystorePassword() { 44 | return password; 45 | } 46 | 47 | public String getKeystoreType() { 48 | return type; 49 | } 50 | 51 | public String getProxyHost() { 52 | return proxyHost; 53 | } 54 | 55 | public int getProxyPort() { 56 | return proxyPort; 57 | } 58 | 59 | public void setProxy(final String proxyHost, final int proxyPort) { 60 | this.proxyHost = proxyHost; 61 | this.proxyPort = proxyPort; 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/PasswordPolicyPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | 5 | /** 6 | * An MDM payload for PasswordPolicy. 7 | * 8 | * @author Sylvain Pedneault 9 | */ 10 | class PasswordPolicyPayload extends MobileConfigPayload { 11 | public PasswordPolicyPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName) throws JSONException { 12 | super(payloadVersion, "com.apple.mobiledevice.passwordpolicy", payloadOrganization, payloadIdentifier, payloadDisplayName); 13 | } 14 | 15 | public void setAllowSimple(final boolean value) throws JSONException { 16 | getPayload().put("allowSimple", value); 17 | } 18 | 19 | public void setForcePIN(final boolean value) throws JSONException { 20 | getPayload().put("forcePIN", value); 21 | } 22 | 23 | public void setMaxFailedAttempts(final int value) throws JSONException { 24 | getPayload().put("maxFailedAttempts", value); 25 | } 26 | 27 | public void setMaxInactivity(final int value) throws JSONException { 28 | getPayload().put("maxInactivity", value); 29 | } 30 | 31 | public void setMaxPINAgeInDays(final int value) throws JSONException { 32 | getPayload().put("maxPINAgeInDays", value); 33 | } 34 | 35 | public void setMinComplexChars(final int value) throws JSONException { 36 | getPayload().put("minComplexChars", value); 37 | } 38 | 39 | public void setMinLength(final int value) throws JSONException { 40 | getPayload().put("minLength", value); 41 | } 42 | 43 | public void setRequireAlphanumeric(final boolean value) throws JSONException { 44 | getPayload().put("requireAlphanumeric", value); 45 | } 46 | 47 | public void setPinHistory(final int value) throws JSONException { 48 | getPayload().put("pinHistory", value); 49 | } 50 | 51 | public void setManualFetchingWhenRoaming(final boolean value) throws JSONException { 52 | getPayload().put("manualFetchingWhenRoaming", value); 53 | } 54 | 55 | public void setMaxGracePeriod(final int value) throws JSONException { 56 | getPayload().put("maxGracePeriod", value); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/NewsstandNotificationPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | /** 9 | * A Newsstand-specific payload compatible with the Apple Push Notification Service. 10 | * 11 | * @author Sylvain Pedneault 12 | */ 13 | public class NewsstandNotificationPayload extends Payload { 14 | 15 | static final Logger logger = LoggerFactory.getLogger(NewsstandNotificationPayload.class); 16 | 17 | /* The application Dictionnary */ 18 | private final JSONObject apsDictionary; 19 | 20 | /** 21 | * Create a default payload with a blank "aps" dictionary. 22 | */ 23 | private NewsstandNotificationPayload() { 24 | super(); 25 | this.apsDictionary = new JSONObject(); 26 | try { 27 | final JSONObject payload = getPayload(); 28 | payload.put("aps", this.apsDictionary); 29 | } catch (final JSONException e) { 30 | logger.error(e.getMessage(), e); 31 | } 32 | } 33 | 34 | /** 35 | * Create a pre-defined payload with a content-available property set to 1. 36 | * 37 | * @return a ready-to-send newsstand payload 38 | */ 39 | public static NewsstandNotificationPayload contentAvailable() { 40 | final NewsstandNotificationPayload payload = complex(); 41 | try { 42 | payload.addContentAvailable(); 43 | } catch (final JSONException e) { 44 | // empty 45 | } 46 | return payload; 47 | } 48 | 49 | /** 50 | * Create an empty payload which you can configure later (most users should not use this). 51 | * This method is usually used to create complex or custom payloads. 52 | * Note: the payload actually contains the default "aps" 53 | * dictionary required by Newsstand. 54 | * 55 | * @return a blank payload that can be customized 56 | */ 57 | private static NewsstandNotificationPayload complex() { 58 | return new NewsstandNotificationPayload(); 59 | } 60 | 61 | private void addContentAvailable() throws JSONException { 62 | addContentAvailable(1); 63 | } 64 | 65 | private void addContentAvailable(final int contentAvailable) throws JSONException { 66 | logger.debug("Adding ContentAvailable [" + contentAvailable + "]"); 67 | this.apsDictionary.put("content-available", contentAvailable); 68 | } 69 | 70 | } 71 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/SCEPPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | * An MDM payload for SCEP (Simple Certificate Enrollment Protocol). 11 | * 12 | * @author Sylvain Pedneault 13 | */ 14 | class SCEPPayload extends MobileConfigPayload { 15 | public SCEPPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String url) throws JSONException { 16 | super(payloadVersion, "com.apple.encrypted-profile-service", payloadOrganization, payloadIdentifier, payloadDisplayName); 17 | final JSONObject payload = getPayload(); 18 | payload.put("URL", url); 19 | } 20 | 21 | public void setName(final String value) throws JSONException { 22 | getPayload().put("Name", value); 23 | } 24 | 25 | public void setSubject(final String value) throws JSONException { 26 | final String[] parts = value.split("/"); 27 | final List list = new ArrayList<>(); 28 | for (final String part : parts) { 29 | final String[] subparts = value.split("="); 30 | list.add(subparts); 31 | } 32 | final String[][] subject = list.toArray(new String[0][0]); 33 | setSubject(subject); 34 | } 35 | 36 | private void setSubject(final String[][] value) throws JSONException { 37 | getPayload().put("Subject", value); 38 | } 39 | 40 | public void setChallenge(final String value) throws JSONException { 41 | getPayload().put("Challenge", value); 42 | } 43 | 44 | public void setKeysize(final int value) throws JSONException { 45 | getPayload().put("Keysize", value); 46 | } 47 | 48 | public void setKeyType(final String value) throws JSONException { 49 | getPayload().put("Key Type", value); 50 | } 51 | 52 | public void setKeyUsage(final int value) throws JSONException { 53 | getPayload().put("Key Usage", value); 54 | } 55 | 56 | public JSONObject addSubjectAltName() throws JSONException { 57 | final JSONObject object = new JSONObject(); 58 | getPayload().put("SubjectAltName", object); 59 | return object; 60 | } 61 | 62 | public JSONObject addGetCACaps() throws JSONException { 63 | final JSONObject object = new JSONObject(); 64 | getPayload().put("GetCACaps", object); 65 | return object; 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/LDAPPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | /** 7 | * An MDM payload for LDAP. 8 | * 9 | * @author Sylvain Pedneault 10 | */ 11 | class LDAPPayload extends MobileConfigPayload { 12 | public LDAPPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String ldapAccountHostName, final boolean ldapAccountUseSSL) throws JSONException { 13 | super(payloadVersion, "com.apple.webClip.managed", payloadOrganization, payloadIdentifier, payloadDisplayName); 14 | final JSONObject payload = getPayload(); 15 | payload.put("LDAPAccountHostName", ldapAccountHostName); 16 | payload.put("LDAPAccountUseSSL", ldapAccountUseSSL); 17 | } 18 | 19 | public void setLDAPAccountDescription(final boolean value) throws JSONException { 20 | getPayload().put("LDAPAccountDescription", value); 21 | } 22 | 23 | public void setLDAPAccountUserName(final boolean value) throws JSONException { 24 | getPayload().put("LDAPAccountUserName", value); 25 | } 26 | 27 | public void setLDAPAccountPassword(final boolean value) throws JSONException { 28 | getPayload().put("LDAPAccountPassword", value); 29 | } 30 | 31 | public JSONObject addSearchSettings(final String ldapSearchSettingSearchBase, final String ldapSearchSettingScope) throws JSONException { 32 | return addSearchSettings(ldapSearchSettingSearchBase, ldapSearchSettingScope, null); 33 | } 34 | 35 | public JSONObject addSearchSettings(final String ldapSearchSettingSearchBase, final int ldapSearchSettingScope) throws JSONException { 36 | return addSearchSettings(ldapSearchSettingSearchBase, ldapSearchSettingScope, null); 37 | } 38 | 39 | private JSONObject addSearchSettings(final String ldapSearchSettingSearchBase, final int ldapSearchSettingScope, final String ldapSearchSettingDescription) throws JSONException { 40 | return addSearchSettings(ldapSearchSettingSearchBase, ldapSearchSettingScope == 0 ? "LDAPSearchSettingScopeBase" : ldapSearchSettingScope == 1 ? "LDAPSearchSettingScopeBase" : "LDAPSearchSettingScopeSubtree", ldapSearchSettingDescription); 41 | } 42 | 43 | private JSONObject addSearchSettings(final String ldapSearchSettingSearchBase, final String ldapSearchSettingScope, final String ldapSearchSettingDescription) throws JSONException { 44 | final JSONObject payload = getPayload(); 45 | final JSONObject searchSettings = new JSONObject(); 46 | payload.put("LDAPSearchSettings", searchSettings); 47 | searchSettings.put("LDAPSearchSettingSearchBase", ldapSearchSettingSearchBase); 48 | searchSettings.put("LDAPSearchSettingScope", ldapSearchSettingScope); 49 | if (ldapSearchSettingDescription != null) { 50 | searchSettings.put("LDAPSearchSettingDescription", ldapSearchSettingDescription); 51 | } 52 | return searchSettings; 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/Devices.java: -------------------------------------------------------------------------------- 1 | package javapns.devices; 2 | 3 | import javapns.devices.implementations.basic.BasicDevice; 4 | import javapns.notification.PayloadPerDevice; 5 | 6 | import java.util.ArrayList; 7 | import java.util.Arrays; 8 | import java.util.List; 9 | 10 | public class Devices { 11 | 12 | private Devices() {} 13 | 14 | public static List asDevices(final Object rawList) { 15 | final List list = new ArrayList<>(); 16 | if (rawList == null) { 17 | return list; 18 | } 19 | 20 | if (rawList instanceof List) { 21 | final List devices = (List) rawList; 22 | if (devices.isEmpty()) { 23 | return list; 24 | } 25 | 26 | final Object firstDevice = devices.get(0); 27 | if (firstDevice instanceof Device) { 28 | //noinspection unchecked 29 | return devices; 30 | } else if (firstDevice instanceof String) { 31 | for (final Object token : devices) { 32 | final BasicDevice device = new BasicDevice(); 33 | device.setToken((String) token); 34 | list.add(device); 35 | } 36 | } 37 | } else if (rawList instanceof String[]) { 38 | final String[] tokens = (String[]) rawList; 39 | for (final String token : tokens) { 40 | final BasicDevice device = new BasicDevice(); 41 | device.setToken(token); 42 | list.add(device); 43 | } 44 | } else if (rawList instanceof Device[]) { 45 | final Device[] dvs = (Device[]) rawList; 46 | return Arrays.asList(dvs); 47 | } else if (rawList instanceof String) { 48 | final BasicDevice device = new BasicDevice(); 49 | device.setToken((String) rawList); 50 | list.add(device); 51 | } else if (rawList instanceof Device) { 52 | list.add((Device) rawList); 53 | } else { 54 | throw new IllegalArgumentException("Device list type not supported. Supported types are: String[], List, Device[], List, String and Device"); 55 | } 56 | return list; 57 | } 58 | 59 | public static List asPayloadsPerDevices(final Object rawList) { 60 | final List list = new ArrayList<>(); 61 | if (rawList == null) { 62 | return list; 63 | } 64 | if (rawList instanceof List) { 65 | final List devices = (List) rawList; 66 | if (devices.isEmpty()) { 67 | return list; 68 | } 69 | //noinspection unchecked 70 | return devices; 71 | } else if (rawList instanceof PayloadPerDevice[]) { 72 | final PayloadPerDevice[] dvs = (PayloadPerDevice[]) rawList; 73 | return Arrays.asList(dvs); 74 | } else if (rawList instanceof PayloadPerDevice) { 75 | list.add((PayloadPerDevice) rawList); 76 | } else { 77 | throw new IllegalArgumentException("PayloadPerDevice list type not supported. Supported types are: PayloadPerDevice[], List and PayloadPerDevice"); 78 | } 79 | return list; 80 | } 81 | } 82 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/management/EmailPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.management; 2 | 3 | import org.json.JSONException; 4 | import org.json.JSONObject; 5 | 6 | /** 7 | * An MDM payload for Email. 8 | * 9 | * @author Sylvain Pedneault 10 | */ 11 | class EmailPayload extends MobileConfigPayload { 12 | public EmailPayload(final int payloadVersion, final String payloadOrganization, final String payloadIdentifier, final String payloadDisplayName, final String emailAccountType, final String emailAddress, final String incomingMailServerAuthentication, final String incomingMailServerHostName, final String incomingMailServerUsername, final String outgoingMailServerAuthentication, final String outgoingMailServerHostName, final String outgoingMailServerUsername) throws JSONException { 13 | super(payloadVersion, "com.apple.mail.managed", payloadOrganization, payloadIdentifier, payloadDisplayName); 14 | final JSONObject payload = getPayload(); 15 | payload.put("EmailAccountType", emailAccountType); 16 | payload.put("EmailAddress", emailAddress); 17 | payload.put("IncomingMailServerAuthentication", incomingMailServerAuthentication); 18 | payload.put("IncomingMailServerHostName", incomingMailServerHostName); 19 | payload.put("IncomingMailServerUsername", incomingMailServerUsername); 20 | payload.put("OutgoingMailServerAuthentication", outgoingMailServerAuthentication); 21 | payload.put("OutgoingMailServerHostName", outgoingMailServerHostName); 22 | payload.put("OutgoingMailServerUsername", outgoingMailServerUsername); 23 | } 24 | 25 | public void setEmailAccountDescription(final String value) throws JSONException { 26 | getPayload().put("EmailAccountDescription", value); 27 | } 28 | 29 | public void setEmailAccountName(final String value) throws JSONException { 30 | getPayload().put("EmailAccountName", value); 31 | } 32 | 33 | public void setIncomingMailServerPortNumber(final int value) throws JSONException { 34 | getPayload().put("IncomingMailServerPortNumber", value); 35 | } 36 | 37 | public void setIncomingMailServerUseSSL(final boolean value) throws JSONException { 38 | getPayload().put("IncomingMailServerUseSSL", value); 39 | } 40 | 41 | public void setIncomingPassword(final String value) throws JSONException { 42 | getPayload().put("IncomingPassword", value); 43 | } 44 | 45 | public void setOutgoingPassword(final String value) throws JSONException { 46 | getPayload().put("OutgoingPassword", value); 47 | } 48 | 49 | public void setOutgoingPasswwordSameAsIncomingPassword(final boolean value) throws JSONException { 50 | getPayload().put("OutgoingPasswwordSameAsIncomingPassword", value); 51 | } 52 | 53 | public void setOutgoingMailServerPortNumber(final int value) throws JSONException { 54 | getPayload().put("OutgoingMailServerPortNumber", value); 55 | } 56 | 57 | public void setOutgoingMailServerUseSSL(final boolean value) throws JSONException { 58 | getPayload().put("OutgoingMailServerUseSSL", value); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/test/java/javapns/test/TestFoundation.java: -------------------------------------------------------------------------------- 1 | package javapns.test; 2 | 3 | import javapns.communication.KeystoreManager; 4 | import javapns.notification.AppleNotificationServer; 5 | import javapns.notification.AppleNotificationServerBasicImpl; 6 | 7 | class TestFoundation { 8 | static boolean verifyCorrectUsage(final Class testClass, final String[] argsProvided, final String... argsRequired) { 9 | if (argsProvided == null) { 10 | return true; 11 | } 12 | final int numberOfArgsRequired = countArgumentsRequired(argsRequired); 13 | if (argsProvided.length < numberOfArgsRequired) { 14 | final String message = getUsageMessage(testClass, argsRequired); 15 | System.out.println(message); 16 | return false; 17 | } 18 | return true; 19 | } 20 | 21 | private static String getUsageMessage(final Class testClass, final String... argsRequired) { 22 | final StringBuilder message = new StringBuilder("Usage: "); 23 | message.append("java -cp \"\" "); 24 | message.append(testClass.getName()); 25 | for (final String argRequired : argsRequired) { 26 | final boolean optional = argRequired.startsWith("["); 27 | if (optional) { 28 | message.append(" ["); 29 | message.append(argRequired.substring(1, argRequired.length() - 1)); 30 | message.append("]"); 31 | } else { 32 | message.append(" <"); 33 | message.append(argRequired); 34 | message.append(">"); 35 | } 36 | } 37 | return message.toString(); 38 | } 39 | 40 | private static int countArgumentsRequired(final String... argsRequired) { 41 | int numberOfArgsRequired = 0; 42 | for (final String argRequired : argsRequired) { 43 | if (argRequired.startsWith("[")) { 44 | break; 45 | } 46 | numberOfArgsRequired++; 47 | } 48 | return numberOfArgsRequired; 49 | } 50 | 51 | /** 52 | * Validate a keystore reference and print the results to the console. 53 | * 54 | * @param keystoreReference a reference to or an actual keystore 55 | * @param password password for the keystore 56 | * @param production service to use 57 | */ 58 | static void verifyKeystore(final Object keystoreReference, final String password, final boolean production) { 59 | try { 60 | System.out.print("Validating keystore reference: "); 61 | KeystoreManager.validateKeystoreParameter(keystoreReference); 62 | System.out.println("VALID (keystore was found)"); 63 | } catch (final Exception e) { 64 | e.printStackTrace(); 65 | } 66 | if (password != null) { 67 | try { 68 | System.out.print("Verifying keystore content: "); 69 | final AppleNotificationServer server = new AppleNotificationServerBasicImpl(keystoreReference, password, production); 70 | KeystoreManager.verifyKeystoreContent(server, keystoreReference); 71 | System.out.println("VERIFIED (no common mistakes detected)"); 72 | } catch (final Exception e) { 73 | e.printStackTrace(); 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/java/javapns/feedback/AppleFeedbackServerBasicImpl.java: -------------------------------------------------------------------------------- 1 | package javapns.feedback; 2 | 3 | import javapns.communication.AppleServerBasicImpl; 4 | import javapns.communication.ConnectionToAppleServer; 5 | import javapns.communication.exceptions.KeystoreException; 6 | 7 | /** 8 | * Basic implementation of the AppleFeedbackServer interface, 9 | * intended to facilitate rapid deployment. 10 | * 11 | * @author Sylvain Pedneault 12 | */ 13 | public class AppleFeedbackServerBasicImpl extends AppleServerBasicImpl implements AppleFeedbackServer { 14 | private final String host; 15 | private final int port; 16 | 17 | /** 18 | * Communication settings for interacting with Apple's default production or sandbox feedback server. 19 | * This constructor uses the recommended keystore type "PCKS12". 20 | * 21 | * @param keystore The keystore to use (can be a File, an InputStream, a String for a file path, or a byte[] array) 22 | * @param password The keystore's password 23 | * @param production true to use Apple's production servers, false to use the sandbox 24 | * @throws KeystoreException thrown if an error occurs when loading the keystore 25 | */ 26 | public AppleFeedbackServerBasicImpl(final Object keystore, final String password, final boolean production) throws KeystoreException { 27 | this(keystore, password, ConnectionToAppleServer.KEYSTORE_TYPE_PKCS12, production); 28 | } 29 | 30 | /** 31 | * Communication settings for interacting with Apple's default production or sandbox feedback server. 32 | * 33 | * @param keystore The keystore to use (can be a File, an InputStream, a String for a file path, or a byte[] array) 34 | * @param password The keystore's password 35 | * @param type The keystore's type 36 | * @param production true to use Apple's production servers, false to use the sandbox 37 | * @throws KeystoreException thrown if an error occurs when loading the keystore 38 | */ 39 | private AppleFeedbackServerBasicImpl(final Object keystore, final String password, final String type, final boolean production) throws KeystoreException { 40 | this(keystore, password, type, production ? PRODUCTION_HOST : DEVELOPMENT_HOST, production ? PRODUCTION_PORT : DEVELOPMENT_PORT); 41 | } 42 | 43 | /** 44 | * Communication settings for interacting with a specific Apple Push Notification Feedback Server. 45 | * 46 | * @param keystore The keystore to use (can be a File, an InputStream, a String for a file path, or a byte[] array) 47 | * @param password The keystore's password 48 | * @param type The keystore's type 49 | * @param host A specific APNS host 50 | * @param port A specific APNS port 51 | * @throws KeystoreException thrown if an error occurs when loading the keystore 52 | */ 53 | private AppleFeedbackServerBasicImpl(final Object keystore, final String password, final String type, final String host, final int port) throws KeystoreException { 54 | super(keystore, password, type); 55 | this.host = host; 56 | this.port = port; 57 | } 58 | 59 | public String getFeedbackServerHost() { 60 | return host; 61 | } 62 | 63 | public int getFeedbackServerPort() { 64 | return port; 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/AppleNotificationServerBasicImpl.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import javapns.communication.AppleServerBasicImpl; 4 | import javapns.communication.ConnectionToAppleServer; 5 | import javapns.communication.exceptions.KeystoreException; 6 | 7 | /** 8 | * Basic implementation of the AppleNotificationServer interface, 9 | * intended to facilitate rapid deployment. 10 | * 11 | * @author Sylvain Pedneault 12 | */ 13 | public class AppleNotificationServerBasicImpl extends AppleServerBasicImpl implements AppleNotificationServer { 14 | private final String host; 15 | private final int port; 16 | 17 | /** 18 | * Communication settings for interacting with Apple's default production or sandbox notification server. 19 | * This constructor uses the recommended keystore type "PCKS12". 20 | * 21 | * @param keystore a keystore containing your private key and the certificate signed by Apple (File, InputStream, byte[], KeyStore or String for a file path) 22 | * @param password the keystore's password 23 | * @param production true to use Apple's production servers, false to use the sandbox 24 | * @throws KeystoreException thrown if an error occurs when loading the keystore 25 | */ 26 | public AppleNotificationServerBasicImpl(final Object keystore, final String password, final boolean production) throws KeystoreException { 27 | this(keystore, password, ConnectionToAppleServer.KEYSTORE_TYPE_PKCS12, production); 28 | } 29 | 30 | /** 31 | * Communication settings for interacting with Apple's default production or sandbox notification server. 32 | * 33 | * @param keystore a keystore containing your private key and the certificate signed by Apple (File, InputStream, byte[], KeyStore or String for a file path) 34 | * @param password the keystore's password 35 | * @param type the keystore's type 36 | * @param production true to use Apple's production servers, false to use the sandbox 37 | * @throws KeystoreException thrown if an error occurs when loading the keystore 38 | */ 39 | private AppleNotificationServerBasicImpl(final Object keystore, final String password, final String type, final boolean production) throws KeystoreException { 40 | this(keystore, password, type, production ? PRODUCTION_HOST : DEVELOPMENT_HOST, production ? PRODUCTION_PORT : DEVELOPMENT_PORT); 41 | } 42 | 43 | /** 44 | * Communication settings for interacting with a specific Apple Push Notification Server. 45 | * 46 | * @param keystore a keystore containing your private key and the certificate signed by Apple (File, InputStream, byte[], KeyStore or String for a file path) 47 | * @param password the keystore's password 48 | * @param type the keystore's type 49 | * @param host a specific APNS host 50 | * @param port a specific APNS port 51 | * @throws KeystoreException thrown if an error occurs when loading the keystore 52 | */ 53 | private AppleNotificationServerBasicImpl(final Object keystore, final String password, final String type, final String host, final int port) throws KeystoreException { 54 | super(keystore, password, type); 55 | this.host = host; 56 | this.port = port; 57 | } 58 | 59 | public String getNotificationServerHost() { 60 | return host; 61 | } 62 | 63 | public int getNotificationServerPort() { 64 | return port; 65 | } 66 | 67 | } 68 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/transmission/PushQueue.java: -------------------------------------------------------------------------------- 1 | package javapns.notification.transmission; 2 | 3 | import javapns.devices.Device; 4 | import javapns.devices.exceptions.InvalidDeviceTokenFormatException; 5 | import javapns.notification.Payload; 6 | import javapns.notification.PayloadPerDevice; 7 | import javapns.notification.PushedNotifications; 8 | 9 | import java.util.List; 10 | 11 | /** 12 | * A queue backed by an asynchronous notification thread or threads. 13 | * 14 | * @author Sylvain Pedneault 15 | */ 16 | public interface PushQueue { 17 | /** 18 | * Queue a message for delivery. A thread will pick it up and push it asynchroneously. 19 | * This method has no effect if the underlying notification thread is not in QUEUE mode. 20 | * 21 | * @param payload a payload 22 | * @param token a device token 23 | * @return the actual queue to which the message was added, which could be a different one if the request was delegated to a sub-queue 24 | * @throws InvalidDeviceTokenFormatException 25 | */ 26 | PushQueue add(Payload payload, String token) throws InvalidDeviceTokenFormatException; 27 | 28 | /** 29 | * Queue a message for delivery. A thread will pick it up and push it asynchroneously. 30 | * This method has no effect if the underlying notification thread is not in QUEUE mode. 31 | * 32 | * @param payload a payload 33 | * @param device a device 34 | * @return the actual queue to which the message was added, which could be a different one if the request was delegated to a sub-queue 35 | */ 36 | PushQueue add(Payload payload, Device device); 37 | 38 | /** 39 | * Queue a message for delivery. A thread will pick it up and push it asynchroneously. 40 | * This method has no effect if the underlying notification thread is not in QUEUE mode. 41 | * 42 | * @param message a payload/device pair 43 | * @return the actual queue to which the message was added, which could be a different one if the request was delegated to a sub-queue 44 | */ 45 | PushQueue add(PayloadPerDevice message); 46 | 47 | /** 48 | * Start the transmission thread(s) working for the queue. 49 | * 50 | * @return the queue itself, as a handy shortcut to create and start a queue in a single line of code 51 | */ 52 | PushQueue start(); 53 | 54 | /** 55 | * Get a list of critical exceptions that underlying threads experienced. 56 | * Critical exceptions include CommunicationException and KeystoreException. 57 | * Exceptions related to tokens, payloads and such are *not* included here, 58 | * as they are noted in individual PushedNotification objects. 59 | * If critical exceptions are present, the underlying thread(s) is most 60 | * likely not working at all and you should solve the problem before 61 | * trying to go any further. 62 | * 63 | * @return a list of critical exceptions 64 | */ 65 | 66 | List getCriticalExceptions(); 67 | 68 | /** 69 | * Get a list of all notifications pushed through this queue. 70 | * 71 | * @return a list of pushed notifications 72 | */ 73 | PushedNotifications getPushedNotifications(); 74 | 75 | /** 76 | * Clear the internal lists of PushedNotification objects maintained by this queue. 77 | * You should invoke this method once you no longer need the list of PushedNotification objects so that memory can be reclaimed. 78 | */ 79 | void clearPushedNotifications(); 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/basic/BasicDeviceFactory.java: -------------------------------------------------------------------------------- 1 | package javapns.devices.implementations.basic; 2 | 3 | import javapns.devices.Device; 4 | import javapns.devices.DeviceFactory; 5 | import javapns.devices.exceptions.DuplicateDeviceException; 6 | import javapns.devices.exceptions.NullDeviceTokenException; 7 | import javapns.devices.exceptions.NullIdException; 8 | import javapns.devices.exceptions.UnknownDeviceException; 9 | 10 | import java.sql.Timestamp; 11 | import java.util.Calendar; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | 15 | /** 16 | * This class implements an in-memory DeviceFactory (backed by a Map). 17 | * Since this class does not persist Device objects, it should not be used in a production environment. 18 | *

19 | * NB : Future Improvement : 20 | * - Add a method to find a device knowing his token 21 | * - Add a method to update a device (timestamp or token) 22 | * - method to compare two devices, and replace when the device token has changed 23 | * 24 | * @author Maxime Peron 25 | */ 26 | @Deprecated 27 | public class BasicDeviceFactory implements DeviceFactory { 28 | /* synclock */ 29 | private static final Object synclock = new Object(); 30 | /* A map containing all the devices, identified with their id */ 31 | private final Map devices; 32 | 33 | /** 34 | * Constructs a VolatileDeviceFactory 35 | */ 36 | public BasicDeviceFactory() { 37 | this.devices = new HashMap<>(); 38 | } 39 | 40 | /** 41 | * Add a device to the map 42 | * 43 | * @param id The device id 44 | * @param token The device token 45 | * @throws DuplicateDeviceException 46 | * @throws NullIdException 47 | * @throws NullDeviceTokenException 48 | */ 49 | public Device addDevice(final String id, String token) throws Exception { 50 | if ((id == null) || (id.trim().equals(""))) { 51 | throw new NullIdException(); 52 | } else if ((token == null) || (token.trim().equals(""))) { 53 | throw new NullDeviceTokenException(); 54 | } else { 55 | if (!this.devices.containsKey(id)) { 56 | final BasicDevice device = new BasicDevice(id, token.trim().replace(" ", ""), new Timestamp(Calendar.getInstance().getTime().getTime())); 57 | this.devices.put(id, device); 58 | return device; 59 | } else { 60 | throw new DuplicateDeviceException(); 61 | } 62 | } 63 | } 64 | 65 | /** 66 | * Get a device according to his id 67 | * 68 | * @param id The device id 69 | * @return The device 70 | * @throws UnknownDeviceException 71 | * @throws NullIdException 72 | */ 73 | public Device getDevice(final String id) throws UnknownDeviceException, NullIdException { 74 | if ((id == null) || (id.trim().equals(""))) { 75 | throw new NullIdException(); 76 | } else { 77 | if (this.devices.containsKey(id)) { 78 | return this.devices.get(id); 79 | } else { 80 | throw new UnknownDeviceException(); 81 | } 82 | } 83 | } 84 | 85 | /** 86 | * Remove a device 87 | * 88 | * @param id The device id 89 | * @throws UnknownDeviceException 90 | * @throws NullIdException 91 | */ 92 | public void removeDevice(final String id) throws UnknownDeviceException, NullIdException { 93 | if ((id == null) || (id.trim().equals(""))) { 94 | throw new NullIdException(); 95 | } 96 | if (this.devices.containsKey(id)) { 97 | this.devices.remove(id); 98 | } else { 99 | throw new UnknownDeviceException(); 100 | } 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/javapns/devices/implementations/basic/BasicDevice.java: -------------------------------------------------------------------------------- 1 | package javapns.devices.implementations.basic; 2 | 3 | import javapns.devices.Device; 4 | import javapns.devices.exceptions.InvalidDeviceTokenFormatException; 5 | 6 | import java.sql.Timestamp; 7 | 8 | /** 9 | * This class is used to represent a Device (iPhone) 10 | * 11 | * @author Maxime Peron 12 | */ 13 | public class BasicDevice implements Device { 14 | 15 | /* 16 | * An id representing a particular device. 17 | * 18 | * Note that this is a local reference to the device, 19 | * which is not related to the actual device UUID or 20 | * other device-specific identification. Most of the 21 | * time, this deviceId should be the same as the token. 22 | */ 23 | private String deviceId; 24 | 25 | /* The device token given by Apple Server, hexadecimal form, 64bits length */ 26 | private String token; 27 | 28 | /* The last time a device registered */ 29 | private Timestamp lastRegister; 30 | 31 | public BasicDevice() { 32 | // empty 33 | } 34 | 35 | /** 36 | * Default constructor. 37 | * 38 | * @param token The device token 39 | */ 40 | public BasicDevice(final String token) throws InvalidDeviceTokenFormatException { 41 | this(token, true); 42 | } 43 | 44 | private BasicDevice(final String token, final boolean validate) throws InvalidDeviceTokenFormatException { 45 | super(); 46 | this.deviceId = token; 47 | this.token = token; 48 | try { 49 | this.lastRegister = new Timestamp(System.currentTimeMillis()); 50 | } catch (final Exception e) { 51 | // empty 52 | } 53 | 54 | if (validate) { 55 | validateTokenFormat(token); 56 | } 57 | } 58 | 59 | /** 60 | * Constructor 61 | * 62 | * @param id The device id 63 | * @param token The device token 64 | */ 65 | BasicDevice(final String id, final String token, final Timestamp register) throws InvalidDeviceTokenFormatException { 66 | super(); 67 | this.deviceId = id; 68 | this.token = token; 69 | this.lastRegister = register; 70 | 71 | validateTokenFormat(token); 72 | 73 | } 74 | 75 | public static void validateTokenFormat(final String token) throws InvalidDeviceTokenFormatException { 76 | if (token == null) { 77 | throw new InvalidDeviceTokenFormatException("Device Token is null, and not the required 64 bytes..."); 78 | } 79 | 80 | if (token.getBytes().length != 64) { 81 | throw new InvalidDeviceTokenFormatException("Device Token has a length of [" + token.getBytes().length + "] and not the required 64 bytes!"); 82 | } 83 | } 84 | 85 | public void validateTokenFormat() throws InvalidDeviceTokenFormatException { 86 | validateTokenFormat(token); 87 | } 88 | 89 | /** 90 | * Getter 91 | * 92 | * @return the device id 93 | */ 94 | public String getDeviceId() { 95 | return deviceId; 96 | } 97 | 98 | /** 99 | * Setter 100 | * 101 | * @param id the device id 102 | */ 103 | public void setDeviceId(final String id) { 104 | this.deviceId = id; 105 | } 106 | 107 | /** 108 | * Getter 109 | * 110 | * @return the device token 111 | */ 112 | public String getToken() { 113 | return token; 114 | } 115 | 116 | /** 117 | * Setter the device token 118 | * 119 | * @param token 120 | */ 121 | public void setToken(final String token) { 122 | this.token = token; 123 | } 124 | 125 | /** 126 | * Getter 127 | * 128 | * @return the last register 129 | */ 130 | public Timestamp getLastRegister() { 131 | return lastRegister; 132 | } 133 | 134 | public void setLastRegister(final Timestamp lastRegister) { 135 | this.lastRegister = lastRegister; 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/ResponsePacketReader.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.net.Socket; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Vector; 10 | 11 | /** 12 | * Class for reading response packets from an APNS connection. 13 | * See Apple's documentation on enhanced notification format. 14 | * 15 | * @author Sylvain Pedneault 16 | */ 17 | class ResponsePacketReader { 18 | /* The number of seconds to wait for a response */ 19 | private static final int TIMEOUT = 5 * 1000; 20 | 21 | private ResponsePacketReader() {} 22 | 23 | /** 24 | * Read response packets from the current APNS connection and process them. 25 | * 26 | * @param notificationManager 27 | * @return the number of response packets received and processed 28 | */ 29 | public static int processResponses(final PushNotificationManager notificationManager) { 30 | final List responses = readResponses(notificationManager.getActiveSocket()); 31 | handleResponses(responses, notificationManager); 32 | return responses.size(); 33 | } 34 | 35 | /** 36 | * Read raw response packets from the provided socket. 37 | *

38 | * Note: this method automatically sets the socket's timeout 39 | * to TIMEOUT, so not to block the socket's input stream. 40 | * 41 | * @param socket 42 | * @return 43 | */ 44 | private static List readResponses(final Socket socket) { 45 | final List responses = new ArrayList<>(); 46 | int previousTimeout = 0; 47 | try { 48 | /* Set socket timeout to avoid getting stuck on read() */ 49 | try { 50 | previousTimeout = socket.getSoTimeout(); 51 | socket.setSoTimeout(TIMEOUT); 52 | } catch (final Exception e) { 53 | // empty 54 | } 55 | final InputStream input = socket.getInputStream(); 56 | while (true) { 57 | final ResponsePacket packet = readResponsePacketData(input); 58 | if (packet != null) { 59 | responses.add(packet); 60 | } else { 61 | break; 62 | } 63 | } 64 | 65 | } catch (final Exception e) { 66 | /* Ignore exception, as we are expecting timeout exceptions because Apple might not reply anything */ 67 | } 68 | /* Reset socket timeout, just in case */ 69 | try { 70 | socket.setSoTimeout(previousTimeout); 71 | } catch (final Exception e) { 72 | // empty 73 | } 74 | return responses; 75 | } 76 | 77 | private static void handleResponses(final List responses, final PushNotificationManager notificationManager) { 78 | for (final ResponsePacket response : responses) { 79 | response.linkToPushedNotification(notificationManager); 80 | } 81 | } 82 | 83 | private static ResponsePacket readResponsePacketData(final InputStream input) throws IOException { 84 | final int command = input.read(); 85 | if (command < 0) { 86 | return null; 87 | } 88 | final int status = input.read(); 89 | if (status < 0) { 90 | return null; 91 | } 92 | 93 | final int identifierByte1 = input.read(); 94 | if (identifierByte1 < 0) { 95 | return null; 96 | } 97 | final int identifierByte2 = input.read(); 98 | if (identifierByte2 < 0) { 99 | return null; 100 | } 101 | final int identifierByte3 = input.read(); 102 | if (identifierByte3 < 0) { 103 | return null; 104 | } 105 | final int identifierByte4 = input.read(); 106 | if (identifierByte4 < 0) { 107 | return null; 108 | } 109 | final int identifier = (identifierByte1 << 24) + (identifierByte2 << 16) + (identifierByte3 << 8) + (identifierByte4); 110 | return new ResponsePacket(command, status, identifier); 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/ProxyManager.java: -------------------------------------------------------------------------------- 1 | package javapns.communication; 2 | 3 | /** 4 | * Main class for dealing with proxies. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | class ProxyManager { 9 | private static final String LOCAL_PROXY_HOST_PROPERTY = "javapns.communication.proxyHost"; 10 | private static final String LOCAL_PROXY_PORT_PROPERTY = "javapns.communication.proxyPort"; 11 | 12 | private static final String JVM_PROXY_HOST_PROPERTY = "https.proxyHost"; 13 | private static final String JVM_PROXY_PORT_PROPERTY = "https.proxyPort"; 14 | 15 | private ProxyManager() { 16 | // empty 17 | } 18 | 19 | /** 20 | * Configure a proxy to use for HTTPS connections created by JavaPNS. 21 | * 22 | * @param host the proxyHost 23 | * @param port the proxyPort 24 | */ 25 | public static void setProxy(final String host, final String port) { 26 | System.setProperty(LOCAL_PROXY_HOST_PROPERTY, host); 27 | System.setProperty(LOCAL_PROXY_PORT_PROPERTY, port); 28 | } 29 | 30 | /** 31 | * Configure a proxy to use for HTTPS connections created anywhere in the JVM (not recommended). 32 | * 33 | * @param host the proxyHost 34 | * @param port the proxyPort 35 | */ 36 | public static void setJVMProxy(final String host, final String port) { 37 | System.setProperty(JVM_PROXY_HOST_PROPERTY, host); 38 | System.setProperty(JVM_PROXY_PORT_PROPERTY, port); 39 | } 40 | 41 | /** 42 | * Get the proxy host address currently configured. 43 | * This method checks if a server-specific proxy has been configured, 44 | * then checks if a proxy has been configured for the entire library, 45 | * and finally checks if a JVM-wide proxy setting is available for HTTPS. 46 | * 47 | * @param server a specific server to check for proxy settings (may be null) 48 | * @return a proxy host, or null if none is configured 49 | */ 50 | static String getProxyHost(final AppleServer server) { 51 | String host = server != null ? server.getProxyHost() : null; 52 | if (host != null && host.length() > 0) { 53 | return host; 54 | } else { 55 | host = System.getProperty(LOCAL_PROXY_HOST_PROPERTY); 56 | if (host != null && host.length() > 0) { 57 | return host; 58 | } else { 59 | host = System.getProperty(JVM_PROXY_HOST_PROPERTY); 60 | if (host != null && host.length() > 0) { 61 | return host; 62 | } else { 63 | return null; 64 | } 65 | } 66 | } 67 | } 68 | 69 | /** 70 | * Get the proxy port currently configured. 71 | * This method first locates a proxy host setting, then returns the proxy port from the same location. 72 | * 73 | * @param server a specific server to check for proxy settings (may be null) 74 | * @return a network port, or 0 if no proxy is configured 75 | */ 76 | static int getProxyPort(final AppleServer server) { 77 | String host = server != null ? server.getProxyHost() : null; 78 | if (host != null && host.length() > 0) { 79 | return server.getProxyPort(); 80 | } else { 81 | host = System.getProperty(LOCAL_PROXY_HOST_PROPERTY); 82 | if (host != null && host.length() > 0) { 83 | return Integer.parseInt(System.getProperty(LOCAL_PROXY_PORT_PROPERTY)); 84 | } else { 85 | host = System.getProperty(JVM_PROXY_HOST_PROPERTY); 86 | if (host != null && host.length() > 0) { 87 | return Integer.parseInt(System.getProperty(JVM_PROXY_PORT_PROPERTY)); 88 | } else { 89 | return 0; 90 | } 91 | } 92 | } 93 | } 94 | 95 | /** 96 | * Determine if a proxy is currently configured. 97 | * 98 | * @param server a specific server to check for proxy settings (may be null) 99 | * @return true if a proxy is set, false otherwise 100 | */ 101 | static boolean isUsingProxy(final AppleServer server) { 102 | final String proxyHost = getProxyHost(server); 103 | return proxyHost != null && proxyHost.length() > 0; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/ResponsePacket.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | /** 4 | * A response packet, as described in Apple's enhanced notification format. 5 | * 6 | * @author Sylvain Pedneault 7 | */ 8 | public class ResponsePacket { 9 | private int command; 10 | private int status; 11 | private int identifier; 12 | 13 | protected ResponsePacket() { 14 | // empty 15 | } 16 | 17 | ResponsePacket(final int command, final int status, final int identifier) { 18 | this.command = command; 19 | this.status = status; 20 | this.identifier = identifier; 21 | } 22 | 23 | void linkToPushedNotification(final PushNotificationManager notificationManager) { 24 | final PushedNotification notification; 25 | try { 26 | notification = notificationManager.getPushedNotifications().get(identifier); 27 | if (notification != null) { 28 | notification.setResponse(this); 29 | } 30 | } catch (final Exception e) { 31 | // empty 32 | } 33 | } 34 | 35 | /** 36 | * Returns the response's command number. It should be 8 for all error responses. 37 | * 38 | * @return the response's command number (which should be 8) 39 | */ 40 | public int getCommand() { 41 | return command; 42 | } 43 | 44 | protected void setCommand(final int command) { 45 | this.command = command; 46 | } 47 | 48 | /** 49 | * Determine if this packet is an error-response packet. 50 | * 51 | * @return true if command number is 8, false otherwise 52 | */ 53 | private boolean isErrorResponsePacket() { 54 | return command == 8; 55 | } 56 | 57 | /** 58 | * Returns the response's status code (see getMessage() for a human-friendly status message instead). 59 | * 60 | * @return the response's status code 61 | */ 62 | public int getStatus() { 63 | return status; 64 | } 65 | 66 | protected void setStatus(final int status) { 67 | this.status = status; 68 | } 69 | 70 | /** 71 | * Determine if this packet is a valid error-response packet. 72 | * To be valid, it must be an error-response packet (command number 8) and it must have a non-zero status code. 73 | * 74 | * @return true if command number is 8 and status code is not 0, false otherwise 75 | */ 76 | public boolean isValidErrorMessage() { 77 | return isErrorResponsePacket() && status != 0; 78 | } 79 | 80 | /** 81 | * Returns the response's identifier, which matches the pushed notification's. 82 | * 83 | * @return the response's identifier 84 | */ 85 | public int getIdentifier() { 86 | return identifier; 87 | } 88 | 89 | protected void setIdentifier(final int identifier) { 90 | this.identifier = identifier; 91 | } 92 | 93 | /** 94 | * Returns a humand-friendly error message, as documented by Apple. 95 | * 96 | * @return a humand-friendly error message 97 | */ 98 | public String getMessage() { 99 | if (command == 8) { 100 | final String prefix = "APNS: [" + identifier + "] "; //APNS ERROR FOR MESSAGE ID #" + identifier + ": "; 101 | if (status == 0) { 102 | return prefix + "No errors encountered"; 103 | } 104 | if (status == 1) { 105 | return prefix + "Processing error"; 106 | } 107 | if (status == 2) { 108 | return prefix + "Missing device token"; 109 | } 110 | if (status == 3) { 111 | return prefix + "Missing topic"; 112 | } 113 | if (status == 4) { 114 | return prefix + "Missing payload"; 115 | } 116 | if (status == 5) { 117 | return prefix + "Invalid token size"; 118 | } 119 | if (status == 6) { 120 | return prefix + "Invalid topic size"; 121 | } 122 | if (status == 7) { 123 | return prefix + "Invalid payload size"; 124 | } 125 | if (status == 8) { 126 | return prefix + "Invalid token"; 127 | } 128 | if (status == 255) { 129 | return prefix + "None (unknown)"; 130 | } 131 | return prefix + "Undocumented status code: " + status; 132 | } 133 | return "APNS: Undocumented response command: " + command; 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/PushedNotifications.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collection; 5 | import java.util.List; 6 | import java.util.Vector; 7 | 8 | /** 9 | *

A list of PushedNotification objects.

10 | *

11 | *

This list can be configured to retain a maximum number of objects. When that maximum is reached, older objects are removed from the list before new ones are added.

12 | *

13 | *

Internally, this list extends Vector.

14 | * 15 | * @author Sylvain Pedneault 16 | */ 17 | public class PushedNotifications extends ArrayList implements List { 18 | private static final long serialVersionUID = 1418782231076330494L; 19 | private int maxRetained = 1000; 20 | 21 | /** 22 | * Construct an empty list of PushedNotification objects. 23 | */ 24 | public PushedNotifications() { 25 | } 26 | 27 | /** 28 | * Construct an empty list of PushedNotification objects with a suggested initial capacity. 29 | * 30 | * @param capacity 31 | */ 32 | public PushedNotifications(final int capacity) { 33 | super(capacity); 34 | } 35 | 36 | /** 37 | * Construct an empty list of PushedNotification objects, and copy the maxRetained property from the provided parent list. 38 | * 39 | * @param parent 40 | */ 41 | private PushedNotifications(final PushedNotifications parent) { 42 | this.maxRetained = parent.getMaxRetained(); 43 | } 44 | 45 | /** 46 | * Filter a list of pushed notifications and return only the ones that were successful. 47 | * 48 | * @return a filtered list containing only notifications that were succcessful 49 | */ 50 | public PushedNotifications getSuccessfulNotifications() { 51 | final PushedNotifications filteredList = new PushedNotifications(this); 52 | for (final PushedNotification notification : this) { 53 | if (notification.isSuccessful()) { 54 | filteredList.add(notification); 55 | } 56 | } 57 | return filteredList; 58 | } 59 | 60 | /** 61 | * Filter a list of pushed notifications and return only the ones that failed. 62 | * 63 | * @return a filtered list containing only notifications that were not successful 64 | */ 65 | public PushedNotifications getFailedNotifications() { 66 | final PushedNotifications filteredList = new PushedNotifications(this); 67 | for (final PushedNotification notification : this) { 68 | if (!notification.isSuccessful()) { 69 | filteredList.add(notification); 70 | } 71 | } 72 | return filteredList; 73 | } 74 | 75 | @Override 76 | public synchronized boolean add(final PushedNotification notification) { 77 | prepareAdd(1); 78 | return super.add(notification); 79 | } 80 | 81 | @Override 82 | public synchronized boolean addAll(final Collection notifications) { 83 | prepareAdd(notifications.size()); 84 | return super.addAll(notifications); 85 | } 86 | 87 | private void prepareAdd(final int n) { 88 | final int size = size(); 89 | if (size + n > maxRetained) { 90 | for (int i = 0; i < n; i++) { 91 | remove(0); 92 | } 93 | } 94 | } 95 | 96 | /** 97 | * Get the maximum number of objects that this list retains. 98 | * 99 | * @return the maximum number of objects that this list retains 100 | */ 101 | private int getMaxRetained() { 102 | return maxRetained; 103 | } 104 | 105 | /** 106 | * Set the maximum number of objects that this list retains. 107 | * When this maximum is reached, older objects are removed from the list before new ones are added. 108 | * 109 | * @param maxRetained the maxRetained value currently configured (default is 1000) 110 | */ 111 | public void setMaxRetained(final int maxRetained) { 112 | this.maxRetained = maxRetained; 113 | } 114 | 115 | @Override 116 | public boolean equals(Object o) { 117 | if (this == o) return true; 118 | if (o == null || getClass() != o.getClass()) return false; 119 | if (!super.equals(o)) return false; 120 | 121 | PushedNotifications that = (PushedNotifications) o; 122 | 123 | return maxRetained == that.maxRetained; 124 | 125 | } 126 | 127 | @Override 128 | public int hashCode() { 129 | int result = super.hashCode(); 130 | result = 31 * result + maxRetained; 131 | return result; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/main/java/javapns/feedback/FeedbackServiceManager.java: -------------------------------------------------------------------------------- 1 | package javapns.feedback; 2 | 3 | import javapns.communication.exceptions.CommunicationException; 4 | import javapns.communication.exceptions.KeystoreException; 5 | import javapns.devices.Device; 6 | import javapns.devices.DeviceFactory; 7 | import javapns.devices.implementations.basic.BasicDevice; 8 | import javapns.devices.implementations.basic.BasicDeviceFactory; 9 | import org.slf4j.Logger; 10 | import org.slf4j.LoggerFactory; 11 | 12 | import javax.net.ssl.SSLSocket; 13 | import java.io.ByteArrayOutputStream; 14 | import java.io.FileNotFoundException; 15 | import java.io.IOException; 16 | import java.io.InputStream; 17 | import java.sql.Timestamp; 18 | import java.util.LinkedList; 19 | import java.util.List; 20 | 21 | /** 22 | * Class for interacting with a specific Feedback Service. 23 | * 24 | * @author kljajo, dgardon, Sylvain Pedneault 25 | */ 26 | public class FeedbackServiceManager { 27 | 28 | private static final Logger logger = LoggerFactory.getLogger(FeedbackServiceManager.class); 29 | 30 | /* Length of the tuple sent by Apple */ 31 | private static final int FEEDBACK_TUPLE_SIZE = 38; 32 | 33 | @Deprecated 34 | private DeviceFactory deviceFactory; 35 | 36 | /** 37 | * Constructs a FeedbackServiceManager with a supplied DeviceFactory. 38 | * 39 | * @deprecated The DeviceFactory-based architecture is deprecated. 40 | */ 41 | @Deprecated 42 | private FeedbackServiceManager(final DeviceFactory deviceFactory) { 43 | this.setDeviceFactory(deviceFactory); 44 | } 45 | 46 | /** 47 | * Constructs a FeedbackServiceManager with a default basic DeviceFactory. 48 | */ 49 | @SuppressWarnings("deprecation") 50 | public FeedbackServiceManager() { 51 | this.setDeviceFactory(new BasicDeviceFactory()); 52 | } 53 | 54 | /** 55 | * Retrieve all devices which have un-installed the application w/Path to keystore 56 | * 57 | * @param server Connection information for the Apple server 58 | * @return List of Devices 59 | * @throws IOException 60 | * @throws FileNotFoundException 61 | * @throws CertificateException 62 | * @throws NoSuchAlgorithmException 63 | * @throws KeyStoreException 64 | * @throws KeyManagementException 65 | * @throws UnrecoverableKeyException 66 | */ 67 | /** 68 | * @throws KeystoreException 69 | * @throws CommunicationException 70 | */ 71 | public List getDevices(final AppleFeedbackServer server) throws KeystoreException, CommunicationException { 72 | final ConnectionToFeedbackServer connectionHelper = new ConnectionToFeedbackServer(server); 73 | final SSLSocket socket = connectionHelper.getSSLSocket(); 74 | return getDevices(socket); 75 | } 76 | 77 | /** 78 | * Retrieves the list of devices from an established SSLSocket. 79 | * 80 | * @param socket 81 | * @return Devices 82 | * @throws CommunicationException 83 | */ 84 | private LinkedList getDevices(final SSLSocket socket) throws CommunicationException { 85 | 86 | // Compute 87 | LinkedList listDev = null; 88 | try { 89 | final InputStream socketStream = socket.getInputStream(); 90 | 91 | // Read bytes 92 | final byte[] b = new byte[1024]; 93 | final ByteArrayOutputStream message = new ByteArrayOutputStream(); 94 | int nbBytes; 95 | // socketStream.available can return 0 96 | // http://forums.sun.com/thread.jspa?threadID=5428561 97 | while ((nbBytes = socketStream.read(b, 0, 1024)) != -1) { 98 | message.write(b, 0, nbBytes); 99 | } 100 | 101 | listDev = new LinkedList<>(); 102 | final byte[] listOfDevices = message.toByteArray(); 103 | final int nbTuples = listOfDevices.length / FEEDBACK_TUPLE_SIZE; 104 | logger.debug("Found: [" + nbTuples + "]"); 105 | for (int i = 0; i < nbTuples; i++) { 106 | final int offset = i * FEEDBACK_TUPLE_SIZE; 107 | 108 | // Build date 109 | final int firstByte; 110 | final int secondByte; 111 | final int thirdByte; 112 | final int fourthByte; 113 | final long anUnsignedInt; 114 | 115 | firstByte = 0x000000FF & ((int) listOfDevices[offset]); 116 | secondByte = 0x000000FF & ((int) listOfDevices[offset + 1]); 117 | thirdByte = 0x000000FF & ((int) listOfDevices[offset + 2]); 118 | fourthByte = 0x000000FF & ((int) listOfDevices[offset + 3]); 119 | anUnsignedInt = ((long) (firstByte << 24 | secondByte << 16 | thirdByte << 8 | fourthByte)) & 0xFFFFFFFFL; 120 | final Timestamp timestamp = new Timestamp(anUnsignedInt * 1000); 121 | 122 | // Build device token length 123 | final int deviceTokenLength = listOfDevices[offset + 4] << 8 | listOfDevices[offset + 5]; 124 | 125 | // Build device token 126 | String deviceToken = ""; 127 | int octet; 128 | for (int j = 0; j < 32; j++) { 129 | octet = 0x000000FF & ((int) listOfDevices[offset + 6 + j]); 130 | deviceToken = deviceToken.concat(String.format("%02x", octet)); 131 | } 132 | 133 | // Build device and add to list 134 | /* Create a basic device, as we do not want to go through the factory and create a device in the actual database... */ 135 | final Device device = new BasicDevice(); 136 | device.setToken(deviceToken); 137 | device.setLastRegister(timestamp); 138 | listDev.add(device); 139 | logger.info("FeedbackManager retrieves one device : " + timestamp + ";" + deviceTokenLength + ";" + deviceToken + "."); 140 | } 141 | 142 | // Close the socket and return the list 143 | 144 | } catch (final Exception e) { 145 | logger.debug("Caught exception fetching devices from Feedback Service"); 146 | throw new CommunicationException("Problem communicating with Feedback service", e); 147 | } finally { 148 | try { 149 | socket.close(); 150 | } catch (final Exception e) { 151 | // empty 152 | } 153 | } 154 | return listDev; 155 | } 156 | 157 | /** 158 | * @return a device factory 159 | * @deprecated The DeviceFactory-based architecture is deprecated. 160 | */ 161 | @Deprecated 162 | public DeviceFactory getDeviceFactory() { 163 | return deviceFactory; 164 | } 165 | 166 | /** 167 | * @param deviceFactory 168 | * @deprecated The DeviceFactory-based architecture is deprecated. 169 | */ 170 | @Deprecated 171 | private void setDeviceFactory(final DeviceFactory deviceFactory) { 172 | this.deviceFactory = deviceFactory; 173 | } 174 | } 175 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/ConnectionToAppleServer.java: -------------------------------------------------------------------------------- 1 | package javapns.communication; 2 | 3 | import javapns.communication.exceptions.CommunicationException; 4 | import javapns.communication.exceptions.KeystoreException; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import javax.net.ssl.*; 9 | import java.io.IOException; 10 | import java.io.InputStream; 11 | import java.io.OutputStream; 12 | import java.io.UnsupportedEncodingException; 13 | import java.net.Socket; 14 | import java.security.KeyStore; 15 | 16 | /** 17 | *

Class representing an abstract connection to an Apple server

18 | *

19 | * Communication protocol differences between Notification and Feedback servers are 20 | * implemented in {@link javapns.notification.ConnectionToNotificationServer} and {@link javapns.feedback.ConnectionToFeedbackServer}. 21 | * 22 | * @author Sylvain Pedneault 23 | */ 24 | public abstract class ConnectionToAppleServer { 25 | /* PKCS12 */ 26 | public static final String KEYSTORE_TYPE_PKCS12 = "PKCS12"; 27 | /* JKS */ 28 | public static final String KEYSTORE_TYPE_JKS = "JKS"; 29 | private static final Logger logger = LoggerFactory.getLogger(ConnectionToAppleServer.class); 30 | /* The algorithm used by KeyManagerFactory */ 31 | private static final String ALGORITHM = KeyManagerFactory.getDefaultAlgorithm(); 32 | 33 | /* The protocol used to create the SSLSocket */ 34 | private static final String PROTOCOL = "TLS"; 35 | 36 | private final AppleServer server; 37 | 38 | private KeyStore keyStore; 39 | private SSLSocketFactory socketFactory; 40 | 41 | /** 42 | * Builds a connection to an Apple server. 43 | * 44 | * @param server connection details 45 | * @throws KeystoreException thrown if an error occurs when loading the keystore 46 | */ 47 | protected ConnectionToAppleServer(final AppleServer server) throws KeystoreException { 48 | this.server = server; 49 | this.keyStore = KeystoreManager.loadKeystore(server); 50 | } 51 | 52 | /** 53 | * Builds a connection to an Apple server. 54 | * 55 | * @param server connection details 56 | * @param keystore 57 | */ 58 | protected ConnectionToAppleServer(final AppleServer server, final KeyStore keystore) { 59 | this.server = server; 60 | this.keyStore = keystore; 61 | } 62 | 63 | public AppleServer getServer() { 64 | return server; 65 | } 66 | 67 | private KeyStore getKeystore() { 68 | return keyStore; 69 | } 70 | 71 | public void setKeystore(final KeyStore ks) { 72 | this.keyStore = ks; 73 | } 74 | 75 | /** 76 | * Generic SSLSocketFactory builder 77 | * 78 | * @param trustManagers 79 | * @return SSLSocketFactory 80 | * @throws KeystoreException 81 | */ 82 | private SSLSocketFactory createSSLSocketFactoryWithTrustManagers(final TrustManager[] trustManagers) throws KeystoreException { 83 | logger.debug("Creating SSLSocketFactory"); 84 | // Get a KeyManager and initialize it 85 | try { 86 | final KeyStore keystore = getKeystore(); 87 | final KeyManagerFactory kmf = KeyManagerFactory.getInstance(ALGORITHM); 88 | try { 89 | final char[] password = KeystoreManager.getKeystorePasswordForSSL(server); 90 | kmf.init(keystore, password); 91 | } catch (Exception e) { 92 | final KeystoreException wrappedKeystoreException = KeystoreManager.wrapKeystoreException(e); 93 | throw wrappedKeystoreException; 94 | } 95 | 96 | // Get the SSLContext to help create SSLSocketFactory 97 | final SSLContext sslc = SSLContext.getInstance(PROTOCOL); 98 | sslc.init(kmf.getKeyManagers(), trustManagers, null); 99 | 100 | return sslc.getSocketFactory(); 101 | } catch (final Exception e) { 102 | throw new KeystoreException("Keystore exception: " + e.getMessage(), e); 103 | } 104 | } 105 | 106 | public abstract String getServerHost(); 107 | 108 | protected abstract int getServerPort(); 109 | 110 | /** 111 | * Return a SSLSocketFactory for creating sockets to communicate with Apple. 112 | * 113 | * @return SSLSocketFactory 114 | * @throws KeystoreException 115 | */ 116 | private SSLSocketFactory createSSLSocketFactory() throws KeystoreException { 117 | return createSSLSocketFactoryWithTrustManagers(new TrustManager[]{new ServerTrustingTrustManager()}); 118 | } 119 | 120 | private SSLSocketFactory getSSLSocketFactory() throws KeystoreException { 121 | if (socketFactory == null) { 122 | socketFactory = createSSLSocketFactory(); 123 | } 124 | return socketFactory; 125 | } 126 | 127 | /** 128 | * Create a SSLSocket which will be used to send data to Apple 129 | * 130 | * @return the SSLSocket 131 | * @throws KeystoreException 132 | * @throws CommunicationException 133 | */ 134 | public SSLSocket getSSLSocket() throws KeystoreException, CommunicationException { 135 | final SSLSocketFactory sslSocketFactory = getSSLSocketFactory(); 136 | logger.debug("Creating SSLSocket to " + getServerHost() + ":" + getServerPort()); 137 | 138 | try { 139 | if (ProxyManager.isUsingProxy(server)) { 140 | return tunnelThroughProxy(sslSocketFactory); 141 | } else { 142 | return (SSLSocket) sslSocketFactory.createSocket(getServerHost(), getServerPort()); 143 | } 144 | } catch (final Exception e) { 145 | throw new CommunicationException("Communication exception: " + e, e); 146 | } 147 | } 148 | 149 | private SSLSocket tunnelThroughProxy(final SSLSocketFactory socketFactory) throws IOException { 150 | final SSLSocket socket; 151 | 152 | // If a proxy was set, tunnel through the proxy to create the connection 153 | final String tunnelHost = ProxyManager.getProxyHost(server); 154 | final Integer tunnelPort = ProxyManager.getProxyPort(server); 155 | 156 | final Socket tunnel = new Socket(tunnelHost, tunnelPort); 157 | doTunnelHandshake(tunnel, getServerHost(), getServerPort()); 158 | 159 | /* overlay the tunnel socket with SSL */ 160 | socket = (SSLSocket) socketFactory.createSocket(tunnel, getServerHost(), getServerPort(), true); 161 | 162 | /* register a callback for handshaking completion event */ 163 | socket.addHandshakeCompletedListener(event -> { 164 | logger.debug("Handshake finished!"); 165 | logger.debug("\t CipherSuite:" + event.getCipherSuite()); 166 | logger.debug("\t SessionId " + event.getSession()); 167 | logger.debug("\t PeerHost " + event.getSession().getPeerHost()); 168 | }); 169 | 170 | return socket; 171 | } 172 | 173 | private void doTunnelHandshake(final Socket tunnel, final String host, final int port) throws IOException { 174 | 175 | final OutputStream out = tunnel.getOutputStream(); 176 | 177 | final String msg = "CONNECT " + host + ":" + port + " HTTP/1.0\n" + "User-Agent: BoardPad Server" + "\r\n\r\n"; 178 | byte[] b; 179 | try { //We really do want ASCII7 -- the http protocol doesn't change with locale. 180 | b = msg.getBytes("ASCII7"); 181 | } catch (final UnsupportedEncodingException ignored) { //If ASCII7 isn't there, something serious is wrong, but Paranoia Is Good (tm) 182 | b = msg.getBytes(); 183 | } 184 | out.write(b); 185 | out.flush(); 186 | 187 | // We need to store the reply so we can create a detailed error message to the user. 188 | final byte[] reply = new byte[200]; 189 | int replyLen = 0; 190 | int newlinesSeen = 0; 191 | boolean headerDone = false; //Done on first newline 192 | 193 | final InputStream in = tunnel.getInputStream(); 194 | 195 | while (newlinesSeen < 2) { 196 | final int i = in.read(); 197 | if (i < 0) { 198 | throw new IOException("Unexpected EOF from proxy"); 199 | } 200 | if (i == '\n') { 201 | headerDone = true; 202 | ++newlinesSeen; 203 | } else if (i != '\r') { 204 | newlinesSeen = 0; 205 | if (!headerDone && replyLen < reply.length) { 206 | reply[replyLen++] = (byte) i; 207 | } 208 | } 209 | } 210 | 211 | /* 212 | * Converting the byte array to a string is slightly wasteful 213 | * in the case where the connection was successful, but it's 214 | * insignificant compared to the network overhead. 215 | */ 216 | String replyStr; 217 | try { 218 | replyStr = new String(reply, 0, replyLen, "ASCII7"); 219 | } catch (final UnsupportedEncodingException ignored) { 220 | replyStr = new String(reply, 0, replyLen); 221 | } 222 | 223 | /* We check for Connection Established because our proxy returns HTTP/1.1 instead of 1.0 */ 224 | if (!replyStr.toLowerCase().contains("200 connection established")) { 225 | throw new IOException("Unable to tunnel through. Proxy returns \"" + replyStr + "\""); 226 | } 227 | 228 | /* tunneling Handshake was successful! */ 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/PushedNotification.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import javapns.devices.Device; 4 | import javapns.notification.exceptions.ErrorResponsePacketReceivedException; 5 | 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | 9 | /** 10 | *

An object representing the result of a push notification to a specific payload to a single device.

11 | *

12 | *

If any error occurred while trying to push the notification, an exception is attached.

13 | *

14 | *

If Apple's Push Notification Service returned an error-response packet, it is linked to the related PushedNotification 15 | * so you can find out what the actual error was.

16 | * 17 | * @author Sylvain Pedneault 18 | */ 19 | public class PushedNotification { 20 | 21 | private Payload payload; 22 | private Device device; 23 | private ResponsePacket response; 24 | 25 | private int identifier; 26 | private long expiry; 27 | private int transmissionAttempts; 28 | private boolean transmissionCompleted; 29 | 30 | private Exception exception; 31 | 32 | protected PushedNotification(final Device device, final Payload payload) { 33 | this.device = device; 34 | this.payload = payload; 35 | } 36 | 37 | PushedNotification(final Device device, final Payload payload, final int identifier) { 38 | this.device = device; 39 | this.payload = payload; 40 | this.identifier = identifier; 41 | } 42 | 43 | public PushedNotification(final Device device, final Payload payload, final Exception exception) { 44 | this.device = device; 45 | this.payload = payload; 46 | this.exception = exception; 47 | } 48 | 49 | /** 50 | * Filters a list of pushed notifications and returns only the ones that were successful. 51 | * 52 | * @param notifications a list of pushed notifications 53 | * @return a filtered list containing only notifications that were succcessful 54 | */ 55 | public static List findSuccessfulNotifications(final List notifications) { 56 | final List filteredList = new ArrayList<>(); 57 | for (final PushedNotification notification : notifications) { 58 | if (notification.isSuccessful()) { 59 | filteredList.add(notification); 60 | } 61 | } 62 | return filteredList; 63 | } 64 | 65 | /** 66 | * Filters a list of pushed notifications and returns only the ones that failed. 67 | * 68 | * @param notifications a list of pushed notifications 69 | * @return a filtered list containing only notifications that were not successful 70 | */ 71 | public static List findFailedNotifications(final List notifications) { 72 | final List filteredList = new ArrayList<>(); 73 | for (final PushedNotification notification : notifications) { 74 | if (!notification.isSuccessful()) { 75 | filteredList.add(notification); 76 | } 77 | } 78 | return filteredList; 79 | } 80 | 81 | /** 82 | * Returns the payload that was pushed. 83 | * 84 | * @return the payload that was pushed 85 | */ 86 | public Payload getPayload() { 87 | return payload; 88 | } 89 | 90 | protected void setPayload(final Payload payload) { 91 | this.payload = payload; 92 | } 93 | 94 | /** 95 | * Returns the device that the payload was pushed to. 96 | * 97 | * @return the device that the payload was pushed to 98 | */ 99 | public Device getDevice() { 100 | return device; 101 | } 102 | 103 | protected void setDevice(final Device device) { 104 | this.device = device; 105 | } 106 | 107 | /** 108 | * Returns the connection-unique identifier referred to by 109 | * error-response packets. 110 | * 111 | * @return a connection-unique identifier 112 | */ 113 | public int getIdentifier() { 114 | return identifier; 115 | } 116 | 117 | void setIdentifier(final int identifier) { 118 | this.identifier = identifier; 119 | } 120 | 121 | /** 122 | * Returns the expiration date of the push notification. 123 | * 124 | * @return the expiration date of the push notification. 125 | */ 126 | public long getExpiry() { 127 | return expiry; 128 | } 129 | 130 | void setExpiry(final long expiry) { 131 | this.expiry = expiry; 132 | } 133 | 134 | void addTransmissionAttempt() { 135 | transmissionAttempts++; 136 | } 137 | 138 | /** 139 | * Returns the number of attempts that have been made to transmit the notification. 140 | * 141 | * @return a number of attempts 142 | */ 143 | public int getTransmissionAttempts() { 144 | return transmissionAttempts; 145 | } 146 | 147 | void setTransmissionAttempts(final int transmissionAttempts) { 148 | this.transmissionAttempts = transmissionAttempts; 149 | } 150 | 151 | /** 152 | * Returns a human-friendly description of the number of attempts made to transmit the notification. 153 | * 154 | * @return a human-friendly description of the number of attempts made to transmit the notification 155 | */ 156 | public String getLatestTransmissionAttempt() { 157 | if (transmissionAttempts == 0) { 158 | return "no attempt yet"; 159 | } 160 | 161 | switch (transmissionAttempts) { 162 | case 1: 163 | return "first attempt"; 164 | case 2: 165 | return "second attempt"; 166 | case 3: 167 | return "third attempt"; 168 | case 4: 169 | return "fourth attempt"; 170 | default: 171 | return "attempt #" + transmissionAttempts; 172 | } 173 | } 174 | 175 | /** 176 | * Indicates if the notification has been streamed successfully to Apple's server. 177 | * This does not indicate if an error-response was received or not, but simply 178 | * that the library successfully completed the transmission of the notification to 179 | * Apple's server. 180 | * 181 | * @return true if the notification was successfully streamed to Apple, false otherwise 182 | */ 183 | public boolean isTransmissionCompleted() { 184 | return transmissionCompleted; 185 | } 186 | 187 | void setTransmissionCompleted(final boolean completed) { 188 | this.transmissionCompleted = completed; 189 | } 190 | 191 | /** 192 | * If a response packet regarding this notification was received, 193 | * this method returns it. Otherwise it returns null. 194 | * 195 | * @return a response packet, if one was received for this notification 196 | */ 197 | public ResponsePacket getResponse() { 198 | return response; 199 | } 200 | 201 | void setResponse(final ResponsePacket response) { 202 | this.response = response; 203 | if (response != null && exception == null) { 204 | exception = new ErrorResponsePacketReceivedException(response); 205 | } 206 | } 207 | 208 | /** 209 | *

Returns true if no response packet was received for this notification, 210 | * or if one was received but is not an error-response (ie command 8), 211 | * or if one was received but its status is 0 (no error occurred).

212 | *

213 | *

Returns false if an error-response packet is attached and has 214 | * a non-zero status code.

215 | *

216 | *

Returns false if an exception is attached.

217 | *

218 | *

Make sure you use the Feedback Service to cleanup your list of 219 | * invalid device tokens, as Apple's documentation says.

220 | * 221 | * @return true if push was successful, false otherwise 222 | */ 223 | public boolean isSuccessful() { 224 | if (!transmissionCompleted) { 225 | return false; 226 | } 227 | if (response == null) { 228 | return true; 229 | } 230 | if (!response.isValidErrorMessage()) { 231 | return true; 232 | } 233 | return false; 234 | } 235 | 236 | /** 237 | * Returns a human-friendly description of this pushed notification. 238 | */ 239 | @Override 240 | public String toString() { 241 | final StringBuilder msg = new StringBuilder(); 242 | msg.append("[").append(identifier).append("]"); 243 | msg.append(transmissionCompleted ? " transmitted " + payload + " on " + getLatestTransmissionAttempt() : " not transmitted"); 244 | msg.append(" to token ").append(device.getToken().substring(0, 5)).append("..").append(device.getToken().substring(59, 64)); 245 | if (response != null) { 246 | msg.append(" ").append(response.getMessage()); 247 | } 248 | if (exception != null) { 249 | msg.append(" ").append(exception); 250 | } 251 | return msg.toString(); 252 | } 253 | 254 | /** 255 | * Get the exception that occurred while trying to push this notification, if any. 256 | * 257 | * @return an exception (if any was thrown) 258 | */ 259 | public Exception getException() { 260 | return exception; 261 | } 262 | 263 | void setException(final Exception exception) { 264 | this.exception = exception; 265 | } 266 | 267 | } 268 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 4.0.0 6 | com.github.mlaccetti 7 | javapns 8 | 2.3.3-SNAPSHOT 9 | 10 | JavaPNS 11 | Java API for Apple Push Notification Service 12 | https://github.com/mlaccetti/JavaPNS 13 | 14 | 15 | UTF-8 16 | 17 | 18 | 19 | 20 | 21 | org.sonatype.plugins 22 | nexus-staging-maven-plugin 23 | 1.6.7 24 | true 25 | 26 | ossrh 27 | https://oss.sonatype.org/ 28 | true 29 | 30 | 31 | 32 | org.apache.maven.plugins 33 | maven-compiler-plugin 34 | 3.5.1 35 | 36 | 1.8 37 | 1.8 38 | UTF-8 39 | 40 | 41 | 42 | org.apache.maven.plugins 43 | maven-failsafe-plugin 44 | 2.19.1 45 | 46 | 47 | 48 | integration-test 49 | verify 50 | 51 | 52 | 53 | 54 | 55 | org.apache.maven.plugins 56 | maven-source-plugin 57 | 3.0.0 58 | 59 | 60 | attach-sources 61 | 62 | jar-no-fork 63 | 64 | 65 | 66 | 67 | 68 | org.apache.maven.plugins 69 | maven-javadoc-plugin 70 | 2.10.3 71 | 72 | 73 | attach-javadocs 74 | 75 | jar 76 | 77 | 78 | 79 | 80 | -Xdoclint:none 81 | 82 | 83 | 84 | org.apache.maven.plugins 85 | maven-release-plugin 86 | 2.5.3 87 | 88 | true 89 | false 90 | release 91 | deploy 92 | 93 | 94 | 95 | org.apache.maven.plugins 96 | maven-assembly-plugin 97 | 2.6 98 | 99 | 100 | jar-with-dependencies 101 | 102 | 103 | 104 | 105 | make-assembly 106 | package 107 | 108 | single 109 | 110 | 111 | 112 | 113 | 114 | org.eluder.coveralls 115 | coveralls-maven-plugin 116 | 4.1.0 117 | 118 | 119 | org.jacoco 120 | jacoco-maven-plugin 121 | 0.7.6.201602180812 122 | 123 | 124 | prepare-agent 125 | 126 | prepare-agent 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | org.codehaus.mojo 138 | findbugs-maven-plugin 139 | 3.0.3 140 | 141 | true 142 | ${artifactTargetPath} 143 | ${artifactTargetPath} 144 | 145 | 146 | 147 | 148 | 149 | 150 | scm:git:git@github.com:mlaccetti/JavaPNS.git 151 | scm:git:https://github.com/mlaccetti/JavaPNS.git 152 | https://github.com/mlaccetti/JavaPNS 153 | 154 | 155 | 156 | 157 | ossrh 158 | https://oss.sonatype.org/content/repositories/snapshots 159 | 160 | 161 | ossrh 162 | https://oss.sonatype.org/service/local/staging/deploy/maven2/ 163 | 164 | 165 | 166 | 167 | 168 | Lesser General Public License (LGPL) 169 | http://www.gnu.org/copyleft/lesser.txt 170 | repo 171 | 172 | 173 | 174 | 175 | 176 | sign-artifacts 177 | 178 | 179 | release 180 | true 181 | 182 | 183 | 184 | 185 | 186 | org.apache.maven.plugins 187 | maven-gpg-plugin 188 | 1.6 189 | 190 | 191 | sign-artifacts 192 | verify 193 | 194 | sign 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | idbill 207 | Bill Levering 208 | PlanX 209 | http://www.planx.com 210 | 211 | developer 212 | 213 | 214 | 215 | sypecom 216 | Sylvain Pedneault 217 | SYPECom 218 | 219 | developer 220 | 221 | 222 | 223 | fernandospr 224 | Fernando Sproviero 225 | 226 | developer 227 | 228 | 229 | 230 | mlaccetti 231 | Michael Laccetti 232 | 233 | developer 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | org.slf4j 242 | slf4j-api 243 | 1.7.19 244 | 245 | 246 | 247 | 248 | org.json 249 | json 250 | 20160212 251 | 252 | 253 | 254 | 255 | org.bouncycastle 256 | bcprov-jdk15on 257 | 1.54 258 | 259 | 260 | 261 | 262 | junit 263 | junit 264 | 4.12 265 | test 266 | 267 | 268 | org.slf4j 269 | slf4j-simple 270 | 1.7.19 271 | test 272 | 273 | 274 | 275 | -------------------------------------------------------------------------------- /src/test/java/javapns/test/NotificationTest.java: -------------------------------------------------------------------------------- 1 | package javapns.test; 2 | 3 | import javapns.Push; 4 | import javapns.communication.exceptions.CommunicationException; 5 | import javapns.communication.exceptions.KeystoreException; 6 | import javapns.devices.Device; 7 | import javapns.devices.implementations.basic.BasicDevice; 8 | import javapns.notification.*; 9 | import javapns.notification.transmission.NotificationProgressListener; 10 | import javapns.notification.transmission.NotificationThread; 11 | import javapns.notification.transmission.NotificationThreads; 12 | import org.json.JSONException; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | 17 | /** 18 | * A command-line test facility for the Push Notification Service. 19 | *

Example: java -cp "[required libraries]" NotificationTest keystore.p12 mypass 2ed202ac08ea9033665e853a3dc8bc4c5e78f7a6cf8d55910df230567037dcc4

20 | *

21 | *

By default, this test uses the sandbox service. To switch, add "production" as a fourth parameter:

22 | *

Example: java -cp "[required libraries]" NotificationTest keystore.p12 mypass 2ed202ac08ea9033665e853a3dc8bc4c5e78f7a6cf8d55910df230567037dcc4 production

23 | *

24 | *

Also by default, this test pushes a simple alert. To send a complex payload, add "complex" as a fifth parameter:

25 | *

Example: java -cp "[required libraries]" NotificationTest keystore.p12 mypass 2ed202ac08ea9033665e853a3dc8bc4c5e78f7a6cf8d55910df230567037dcc4 production complex

26 | *

27 | *

To send a simple payload to a large number of fake devices, add "threads" as a fifth parameter, the number of fake devices to construct, and the number of threads to use:

28 | *

Example: java -cp "[required libraries]" NotificationTest keystore.p12 mypass 2ed202ac08ea9033665e853a3dc8bc4c5e78f7a6cf8d55910df230567037dcc4 sandbox threads 1000 5

29 | * 30 | * @author Sylvain Pedneault 31 | */ 32 | class NotificationTest extends TestFoundation { 33 | /** 34 | * A NotificationProgressListener you can use to debug NotificationThreads. 35 | */ 36 | static final NotificationProgressListener DEBUGGING_PROGRESS_LISTENER = new NotificationProgressListener() { 37 | 38 | public void eventThreadStarted(final NotificationThread notificationThread) { 39 | System.out.println(" [EVENT]: thread #" + notificationThread.getThreadNumber() + " started with " + notificationThread.getDevices().size() + " devices beginning at message id #" + notificationThread.getFirstMessageIdentifier()); 40 | } 41 | 42 | public void eventThreadFinished(final NotificationThread thread) { 43 | System.out.println(" [EVENT]: thread #" + thread.getThreadNumber() + " finished: pushed messages #" + thread.getFirstMessageIdentifier() + " to " + thread.getLastMessageIdentifier() + " toward " + thread.getDevices().size() + " devices"); 44 | } 45 | 46 | public void eventConnectionRestarted(final NotificationThread thread) { 47 | System.out.println(" [EVENT]: connection restarted in thread #" + thread.getThreadNumber() + " because it reached " + thread.getMaxNotificationsPerConnection() + " notifications per connection"); 48 | } 49 | 50 | public void eventAllThreadsStarted(final NotificationThreads notificationThreads) { 51 | System.out.println(" [EVENT]: all threads started: " + notificationThreads.getThreads().size()); 52 | } 53 | 54 | public void eventAllThreadsFinished(final NotificationThreads notificationThreads) { 55 | System.out.println(" [EVENT]: all threads finished: " + notificationThreads.getThreads().size()); 56 | } 57 | 58 | public void eventCriticalException(final NotificationThread notificationThread, final Exception exception) { 59 | System.out.println(" [EVENT]: critical exception occurred: " + exception); 60 | } 61 | }; 62 | 63 | private NotificationTest() { 64 | } 65 | 66 | /** 67 | * Execute this class from the command line to run tests. 68 | * 69 | * @param args 70 | */ 71 | public static void main(final String[] args) { 72 | 73 | /* Verify that the test is being invoked */ 74 | if (!verifyCorrectUsage(NotificationTest.class, args, "keystore-path", "keystore-password", "device-token", "[production|sandbox]", "[complex|simple|threads]", "[#devices]", "[#threads]")) { 75 | return; 76 | } 77 | 78 | /* Push an alert */ 79 | try { 80 | pushTest(args); 81 | } catch (final CommunicationException | KeystoreException e) { 82 | e.printStackTrace(); 83 | } 84 | } 85 | 86 | /** 87 | * Push a test notification to a device, given command-line parameters. 88 | * 89 | * @param args 90 | * @throws KeystoreException 91 | * @throws CommunicationException 92 | */ 93 | private static void pushTest(final String[] args) throws CommunicationException, KeystoreException { 94 | final String keystore = args[0]; 95 | final String password = args[1]; 96 | final String token = args[2]; 97 | final boolean production = args.length >= 4 && args[3].equalsIgnoreCase("production"); 98 | final boolean simulation = args.length >= 4 && args[3].equalsIgnoreCase("simulation"); 99 | final boolean complex = args.length >= 5 && args[4].equalsIgnoreCase("complex"); 100 | final boolean threads = args.length >= 5 && args[4].equalsIgnoreCase("threads"); 101 | final int threadDevices = args.length >= 6 ? Integer.parseInt(args[5]) : 100; 102 | final int threadThreads = args.length >= 7 ? Integer.parseInt(args[6]) : 10; 103 | final boolean simple = !complex && !threads; 104 | 105 | verifyKeystore(keystore, password, production); 106 | 107 | if (simple) { 108 | /* Push a test alert */ 109 | final List notifications = Push.test(keystore, password, production, token); 110 | printPushedNotifications(notifications); 111 | } else if (complex) { 112 | /* Push a more complex payload */ 113 | final List notifications = Push.payload(createComplexPayload(), keystore, password, production, token); 114 | printPushedNotifications(notifications); 115 | } else { 116 | /* Push a Hello World! alert repetitively using NotificationThreads */ 117 | pushSimplePayloadUsingThreads(keystore, password, production, token, simulation, threadDevices, threadThreads); 118 | 119 | } 120 | } 121 | 122 | /** 123 | * Create a complex payload for test purposes. 124 | * 125 | * @return 126 | */ 127 | @SuppressWarnings("unchecked") 128 | private static Payload createComplexPayload() { 129 | final PushNotificationPayload complexPayload = PushNotificationPayload.complex(); 130 | try { 131 | // You can use addBody to add simple message, but we'll use 132 | // a more complex alert message so let's comment it 133 | complexPayload.addCustomAlertBody("My alert message"); 134 | complexPayload.addCustomAlertActionLocKey("Open App"); 135 | complexPayload.addCustomAlertLocKey("javapns rocks %@ %@%@"); 136 | final ArrayList parameters = new ArrayList(); 137 | parameters.add("Test1"); 138 | parameters.add("Test"); 139 | parameters.add(2); 140 | complexPayload.addCustomAlertLocArgs(parameters); 141 | complexPayload.addBadge(45); 142 | complexPayload.addSound("default"); 143 | complexPayload.addCustomDictionary("acme", "foo"); 144 | complexPayload.addCustomDictionary("acme2", 42); 145 | final ArrayList values = new ArrayList(); 146 | values.add("value1"); 147 | values.add(2); 148 | complexPayload.addCustomDictionary("acme3", values); 149 | } catch (final JSONException e) { 150 | System.out.println("Error creating complex payload:"); 151 | e.printStackTrace(); 152 | } 153 | return complexPayload; 154 | } 155 | 156 | private static void pushSimplePayloadUsingThreads(final String keystore, final String password, final boolean production, final String token, final boolean simulation, final int devices, final int threads) { 157 | try { 158 | 159 | System.out.println("Creating PushNotificationManager and AppleNotificationServer"); 160 | final AppleNotificationServer server = new AppleNotificationServerBasicImpl(keystore, password, production); 161 | System.out.println("Creating payload (simulation mode)"); 162 | // Payload payload = PushNotificationPayload.alert("Hello World!"); 163 | final Payload payload = PushNotificationPayload.test(); 164 | 165 | System.out.println("Generating " + devices + " fake devices"); 166 | final List deviceList = new ArrayList<>(devices); 167 | //noinspection Duplicates 168 | for (int i = 0; i < devices; i++) { 169 | String tokenToUse = token; 170 | if (tokenToUse == null || tokenToUse.length() != 64) { 171 | tokenToUse = "123456789012345678901234567890123456789012345678901234567" + (1000000 + i); 172 | } 173 | deviceList.add(new BasicDevice(tokenToUse)); 174 | } 175 | 176 | System.out.println("Creating " + threads + " notification threads"); 177 | final NotificationThreads work = new NotificationThreads(server, simulation ? payload.asSimulationOnly() : payload, deviceList, threads); 178 | //work.setMaxNotificationsPerConnection(10000); 179 | System.out.println("Linking notification work debugging listener"); 180 | work.setListener(DEBUGGING_PROGRESS_LISTENER); 181 | 182 | System.out.println("Starting all threads..."); 183 | final long timestamp1 = System.currentTimeMillis(); 184 | work.start(); 185 | System.out.println("All threads started, waiting for them..."); 186 | work.waitForAllThreads(); 187 | final long timestamp2 = System.currentTimeMillis(); 188 | System.out.println("All threads finished in " + (timestamp2 - timestamp1) + " milliseconds"); 189 | 190 | printPushedNotifications(work.getPushedNotifications()); 191 | 192 | } catch (final Exception e) { 193 | e.printStackTrace(); 194 | } 195 | } 196 | 197 | /** 198 | * Print to the console a comprehensive report of all pushed notifications and results. 199 | * 200 | * @param notifications a raw list of pushed notifications 201 | */ 202 | static void printPushedNotifications(final List notifications) { 203 | final List failedNotifications = PushedNotification.findFailedNotifications(notifications); 204 | final List successfulNotifications = PushedNotification.findSuccessfulNotifications(notifications); 205 | final int failed = failedNotifications.size(); 206 | final int successful = successfulNotifications.size(); 207 | 208 | if (successful > 0 && failed == 0) { 209 | printPushedNotifications("All notifications pushed successfully (" + successfulNotifications.size() + "):", successfulNotifications); 210 | } else if (successful == 0 && failed > 0) { 211 | printPushedNotifications("All notifications failed (" + failedNotifications.size() + "):", failedNotifications); 212 | } else if (successful == 0 && failed == 0) { 213 | System.out.println("No notifications could be sent, probably because of a critical error"); 214 | } else { 215 | printPushedNotifications("Some notifications failed (" + failedNotifications.size() + "):", failedNotifications); 216 | printPushedNotifications("Others succeeded (" + successfulNotifications.size() + "):", successfulNotifications); 217 | } 218 | } 219 | 220 | /** 221 | * Print to the console a list of pushed notifications. 222 | * 223 | * @param description a title for this list of notifications 224 | * @param notifications a list of pushed notifications to print 225 | */ 226 | static void printPushedNotifications(final String description, final List notifications) { 227 | System.out.println(description); 228 | for (final PushedNotification notification : notifications) { 229 | try { 230 | System.out.println(" " + notification.toString()); 231 | } catch (final Exception e) { 232 | e.printStackTrace(); 233 | } 234 | } 235 | } 236 | 237 | } 238 | -------------------------------------------------------------------------------- /src/main/java/javapns/communication/KeystoreManager.java: -------------------------------------------------------------------------------- 1 | package javapns.communication; 2 | 3 | import javapns.communication.exceptions.InvalidKeystoreFormatException; 4 | import javapns.communication.exceptions.InvalidKeystorePasswordException; 5 | import javapns.communication.exceptions.InvalidKeystoreReferenceException; 6 | import javapns.communication.exceptions.KeystoreException; 7 | 8 | import java.io.*; 9 | import java.security.KeyStore; 10 | import java.security.cert.Certificate; 11 | import java.security.cert.CertificateExpiredException; 12 | import java.security.cert.CertificateNotYetValidException; 13 | import java.security.cert.X509Certificate; 14 | import java.util.Enumeration; 15 | 16 | /** 17 | * Class responsible for dealing with keystores. 18 | * 19 | * @author Sylvain Pedneault 20 | */ 21 | public class KeystoreManager { 22 | private static final String REVIEW_MESSAGE = " Please review the procedure for generating a keystore for JavaPNS."; 23 | 24 | private KeystoreManager() {} 25 | 26 | /** 27 | * Loads a keystore. 28 | * 29 | * @param server The server the keystore is intended for 30 | * @return A loaded keystore 31 | * @throws KeystoreException 32 | */ 33 | static KeyStore loadKeystore(final AppleServer server) throws KeystoreException { 34 | return loadKeystore(server, server.getKeystoreStream()); 35 | } 36 | 37 | /** 38 | * Loads a keystore. 39 | * 40 | * @param server the server the keystore is intended for 41 | * @param keystore a keystore containing your private key and the certificate signed by Apple (File, InputStream, byte[], KeyStore or String for a file path) 42 | * @return a loaded keystore 43 | * @throws KeystoreException 44 | */ 45 | private static KeyStore loadKeystore(final AppleServer server, final Object keystore) throws KeystoreException { 46 | return loadKeystore(server, keystore, false); 47 | } 48 | 49 | /** 50 | * Loads a keystore. 51 | * 52 | * @param server the server the keystore is intended for 53 | * @param keystore a keystore containing your private key and the certificate signed by Apple (File, InputStream, byte[], KeyStore or String for a file path) 54 | * @param verifyKeystore whether or not to perform basic verifications on the keystore to detect common mistakes. 55 | * @return a loaded keystore 56 | * @throws KeystoreException 57 | */ 58 | private static synchronized KeyStore loadKeystore(final AppleServer server, final Object keystore, final boolean verifyKeystore) throws KeystoreException { 59 | if (keystore instanceof KeyStore) { 60 | return (KeyStore) keystore; 61 | } 62 | 63 | try (final InputStream keystoreStream = streamKeystore(keystore)) { 64 | if (keystoreStream instanceof WrappedKeystore) { 65 | return ((WrappedKeystore) keystoreStream).getKeystore(); 66 | } 67 | 68 | final KeyStore keyStore = KeyStore.getInstance(server.getKeystoreType()); 69 | final char[] password = KeystoreManager.getKeystorePasswordForSSL(server); 70 | keyStore.load(keystoreStream, password); 71 | return keyStore; 72 | } catch (final Exception e) { 73 | throw wrapKeystoreException(e); 74 | } 75 | } 76 | 77 | /** 78 | * Make sure that the provided keystore will be reusable. 79 | * 80 | * @param server the server the keystore is intended for 81 | * @param keystore a keystore containing your private key and the certificate signed by Apple (File, InputStream, byte[], KeyStore or String for a file path) 82 | * @return a reusable keystore 83 | * @throws KeystoreException 84 | */ 85 | static Object ensureReusableKeystore(final AppleServer server, Object keystore) throws KeystoreException { 86 | if (keystore instanceof InputStream) { 87 | return loadKeystore(server, keystore, false); 88 | } 89 | return keystore; 90 | } 91 | 92 | /** 93 | * Perform basic tests on a keystore to detect common user mistakes. 94 | * If a problem is found, a KeystoreException is thrown. 95 | * If no problem is found, this method simply returns without exceptions. 96 | * 97 | * @param server the server the keystore is intended for 98 | * @param keystore a keystore containing your private key and the certificate signed by Apple (File, InputStream, byte[], KeyStore or String for a file path) 99 | * @throws KeystoreException 100 | */ 101 | public static void verifyKeystoreContent(final AppleServer server, final Object keystore) throws KeystoreException { 102 | final KeyStore keystoreToValidate; 103 | if (keystore instanceof KeyStore) { 104 | keystoreToValidate = (KeyStore) keystore; 105 | } else { 106 | keystoreToValidate = loadKeystore(server, keystore); 107 | } 108 | verifyKeystoreContent(keystoreToValidate); 109 | } 110 | 111 | /** 112 | * Perform basic tests on a keystore to detect common user mistakes (experimental). 113 | * If a problem is found, a KeystoreException is thrown. 114 | * If no problem is found, this method simply returns without exceptions. 115 | * 116 | * @param keystore a keystore to verify 117 | * @throws KeystoreException thrown if a problem was detected 118 | */ 119 | private static void verifyKeystoreContent(final KeyStore keystore) throws KeystoreException { 120 | try { 121 | int numberOfCertificates = 0; 122 | final Enumeration aliases = keystore.aliases(); 123 | while (aliases.hasMoreElements()) { 124 | final String alias = aliases.nextElement(); 125 | final Certificate certificate = keystore.getCertificate(alias); 126 | if (certificate instanceof X509Certificate) { 127 | final X509Certificate xcert = (X509Certificate) certificate; 128 | numberOfCertificates++; 129 | 130 | /* Check validity dates */ 131 | xcert.checkValidity(); 132 | 133 | /* Check issuer */ 134 | final boolean issuerIsApple = xcert.getIssuerDN().toString().contains("Apple"); 135 | if (!issuerIsApple) { 136 | throw new KeystoreException("Certificate was not issued by Apple." + REVIEW_MESSAGE); 137 | } 138 | 139 | /* Check certificate key usage */ 140 | final boolean[] keyUsage = xcert.getKeyUsage(); 141 | if (!keyUsage[0]) { 142 | throw new KeystoreException("Certificate usage is incorrect." + REVIEW_MESSAGE); 143 | } 144 | 145 | } 146 | } 147 | if (numberOfCertificates == 0) { 148 | throw new KeystoreException("Keystore does not contain any valid certificate." + REVIEW_MESSAGE); 149 | } 150 | if (numberOfCertificates > 1) { 151 | throw new KeystoreException("Keystore contains too many certificates." + REVIEW_MESSAGE); 152 | } 153 | 154 | } catch (final KeystoreException e) { 155 | throw e; 156 | } catch (final CertificateExpiredException e) { 157 | throw new KeystoreException("Certificate is expired. A new one must be issued.", e); 158 | } catch (final CertificateNotYetValidException e) { 159 | throw new KeystoreException("Certificate is not yet valid. Wait until the validity period is reached or issue a new certificate.", e); 160 | } catch (final Exception e) { 161 | /* We ignore any other exception, as we do not want to interrupt the process because of an error we did not expect. */ 162 | } 163 | } 164 | 165 | static char[] getKeystorePasswordForSSL(final AppleServer server) { 166 | String password = server.getKeystorePassword(); 167 | if (password == null) { 168 | password = ""; 169 | } 170 | 171 | return password.toCharArray(); 172 | } 173 | 174 | static KeystoreException wrapKeystoreException(final Exception e) { 175 | if (e != null) { 176 | final String msg = e.toString(); 177 | if (msg.contains("javax.crypto.BadPaddingException")) { 178 | return new InvalidKeystorePasswordException(); 179 | } 180 | if (msg.contains("DerInputStream.getLength(): lengthTag=127, too big")) { 181 | return new InvalidKeystoreFormatException(); 182 | } 183 | if (msg.contains("java.lang.ArithmeticException: / by zero") || msg.contains("java.security.UnrecoverableKeyException: Get Key failed: / by zero")) { 184 | return new InvalidKeystorePasswordException("Blank passwords not supported (#38). You must create your keystore with a non-empty password."); 185 | } 186 | } 187 | 188 | return new KeystoreException("Keystore exception: " + (e != null ? e.getMessage() : null), e); 189 | } 190 | 191 | /** 192 | * Given an object representing a keystore, returns an actual stream for that keystore. 193 | * Allows you to provide an actual keystore as an InputStream or a byte[] array, 194 | * or a reference to a keystore file as a File object or a String path. 195 | * 196 | * @param keystore a keystore containing your private key and the certificate signed by Apple (File, InputStream, byte[], KeyStore or String for a file path) 197 | * @return A stream to the keystore. 198 | * @throws InvalidKeystoreReferenceException 199 | */ 200 | static InputStream streamKeystore(final Object keystore) throws InvalidKeystoreReferenceException { 201 | validateKeystoreParameter(keystore); 202 | try { 203 | if (keystore instanceof InputStream) { 204 | return (InputStream) keystore; 205 | } else if (keystore instanceof KeyStore) { 206 | return new WrappedKeystore((KeyStore) keystore); 207 | } else if (keystore instanceof File) { 208 | return new BufferedInputStream(new FileInputStream((File) keystore)); 209 | } else if (keystore instanceof String) { 210 | return new BufferedInputStream(new FileInputStream((String) keystore)); 211 | } else if (keystore instanceof byte[]) { 212 | return new ByteArrayInputStream((byte[]) keystore); 213 | } else { 214 | return null; // we should not get here since validateKeystore ensures that the reference is valid 215 | } 216 | } catch (final Exception e) { 217 | throw new InvalidKeystoreReferenceException("Invalid keystore reference: " + e.getMessage()); 218 | } 219 | } 220 | 221 | /** 222 | * Ensures that a keystore parameter is actually supported by the KeystoreManager. 223 | * 224 | * @param keystore a keystore containing your private key and the certificate signed by Apple (File, InputStream, byte[], KeyStore or String for a file path) 225 | * @throws InvalidKeystoreReferenceException thrown if the provided keystore parameter is not supported 226 | */ 227 | public static void validateKeystoreParameter(Object keystore) throws InvalidKeystoreReferenceException { 228 | if (keystore == null) { 229 | throw new InvalidKeystoreReferenceException((Object) null); 230 | } 231 | if (keystore instanceof KeyStore) { 232 | return; 233 | } 234 | if (keystore instanceof InputStream) { 235 | return; 236 | } 237 | if (keystore instanceof String) { 238 | validateFileKeystore(new File((String) keystore)); 239 | return; 240 | } 241 | if (keystore instanceof File) { 242 | validateFileKeystore((File) keystore); 243 | return; 244 | } 245 | if (keystore instanceof byte[]) { 246 | final byte[] bytes = (byte[]) keystore; 247 | if (bytes.length == 0) { 248 | throw new InvalidKeystoreReferenceException("Invalid keystore reference. Byte array is empty"); 249 | } 250 | return; 251 | } 252 | throw new InvalidKeystoreReferenceException(keystore); 253 | } 254 | 255 | private static void validateFileKeystore(File keystore) throws InvalidKeystoreReferenceException { 256 | final File file = keystore; 257 | if (!file.exists()) { 258 | throw new InvalidKeystoreReferenceException("Invalid keystore reference. File does not exist: " + file.getAbsolutePath()); 259 | } 260 | if (!file.isFile()) { 261 | throw new InvalidKeystoreReferenceException("Invalid keystore reference. Path does not refer to a valid file: " + file.getAbsolutePath()); 262 | } 263 | if (file.length() <= 0) { 264 | throw new InvalidKeystoreReferenceException("Invalid keystore reference. File is empty: " + file.getAbsolutePath()); 265 | } 266 | return; 267 | } 268 | 269 | } 270 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/PushNotificationPayload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import javapns.notification.exceptions.PayloadAlertAlreadyExistsException; 4 | import org.json.JSONException; 5 | import org.json.JSONObject; 6 | import org.slf4j.Logger; 7 | import org.slf4j.LoggerFactory; 8 | 9 | import java.util.IllegalFormatException; 10 | import java.util.List; 11 | 12 | /** 13 | * A payload compatible with the Apple Push Notification Service. 14 | * 15 | * @author Maxime Peron 16 | * @author Sylvain Pedneault 17 | */ 18 | public class PushNotificationPayload extends Payload { 19 | 20 | static final Logger logger = LoggerFactory.getLogger(PushNotificationPayload.class); 21 | 22 | /* Maximum total length (serialized) of a payload */ 23 | private static final int MAXIMUM_PAYLOAD_LENGTH = 256; 24 | public static final String ALERT = "alert"; 25 | 26 | /* The application Dictionary */ 27 | private JSONObject apsDictionary; 28 | 29 | /** 30 | * Create a default payload with a blank "aps" dictionary. 31 | */ 32 | PushNotificationPayload() { 33 | super(); 34 | this.apsDictionary = new JSONObject(); 35 | try { 36 | final JSONObject payload = getPayload(); 37 | if (!payload.has("aps")) { 38 | payload.put("aps", this.apsDictionary); 39 | } 40 | } catch (final JSONException e) { 41 | logger.error(e.getMessage(), e); 42 | } 43 | } 44 | 45 | /** 46 | * Construct a Payload object from a JSON-formatted string. 47 | * If an aps dictionary is not included, one will be created automatically. 48 | * 49 | * @param rawJSON a JSON-formatted string (ex: {"aps":{"alert":"Hello World!"}} ) 50 | * @throws JSONException thrown if a exception occurs while parsing the JSON string 51 | */ 52 | PushNotificationPayload(final String rawJSON) throws JSONException { 53 | super(rawJSON); 54 | try { 55 | final JSONObject payload = getPayload(); 56 | this.apsDictionary = payload.getJSONObject("aps"); 57 | if (this.apsDictionary == null) { 58 | this.apsDictionary = new JSONObject(); 59 | payload.put("aps", this.apsDictionary); 60 | } 61 | 62 | } catch (final JSONException e) { 63 | logger.error(e.getMessage(), e); 64 | } 65 | } 66 | 67 | /** 68 | * Create a payload and immediately add an alert message, a badge and a sound. 69 | * 70 | * @param alert the alert message 71 | * @param badge the badge 72 | * @param sound the name of the sound 73 | * @throws JSONException 74 | */ 75 | public PushNotificationPayload(final String alert, final int badge, final String sound) throws JSONException { 76 | this(); 77 | if (alert != null) { 78 | addAlert(alert); 79 | } 80 | addBadge(badge); 81 | if (sound != null) { 82 | addSound(sound); 83 | } 84 | } 85 | 86 | /** 87 | * Create a pre-defined payload with a simple alert message. 88 | * 89 | * @param message the alert's message 90 | * @return a ready-to-send payload 91 | */ 92 | public static PushNotificationPayload alert(final String message) { 93 | if (message == null) { 94 | throw new IllegalArgumentException("Alert cannot be null"); 95 | } 96 | final PushNotificationPayload payload = complex(); 97 | try { 98 | payload.addAlert(message); 99 | } catch (final JSONException e) { 100 | // empty 101 | } 102 | return payload; 103 | } 104 | 105 | /** 106 | * Create a pre-defined payload with a badge. 107 | * 108 | * @param badge the badge 109 | * @return a ready-to-send payload 110 | */ 111 | public static PushNotificationPayload badge(final int badge) { 112 | final PushNotificationPayload payload = complex(); 113 | try { 114 | payload.addBadge(badge); 115 | } catch (final JSONException e) { 116 | // empty 117 | } 118 | return payload; 119 | } 120 | 121 | /** 122 | * Create a pre-defined payload with a sound name. 123 | * 124 | * @param sound the name of the sound 125 | * @return a ready-to-send payload 126 | */ 127 | public static PushNotificationPayload sound(final String sound) { 128 | if (sound == null) { 129 | throw new IllegalArgumentException("Sound name cannot be null"); 130 | } 131 | final PushNotificationPayload payload = complex(); 132 | try { 133 | payload.addSound(sound); 134 | } catch (final JSONException e) { 135 | logger.error(e.getMessage(), e); 136 | } 137 | return payload; 138 | } 139 | 140 | /** 141 | * Create a pre-defined payload with a simple alert message, a badge and a sound. 142 | * 143 | * @param message the alert message 144 | * @param badge the badge 145 | * @param sound the name of the sound 146 | * @return a ready-to-send payload 147 | */ 148 | public static PushNotificationPayload combined(final String message, final int badge, final String sound) { 149 | if (message == null && badge < 0 && sound == null) { 150 | throw new IllegalArgumentException("Must provide at least one non-null argument"); 151 | } 152 | final PushNotificationPayload payload = complex(); 153 | try { 154 | if (message != null) { 155 | payload.addAlert(message); 156 | } 157 | if (badge >= 0) { 158 | payload.addBadge(badge); 159 | } 160 | if (sound != null) { 161 | payload.addSound(sound); 162 | } 163 | } catch (final JSONException e) { 164 | logger.error(e.getMessage(), e); 165 | } 166 | return payload; 167 | } 168 | 169 | /** 170 | * Create a special payload with a useful debugging alert message. 171 | * 172 | * @return a ready-to-send payload 173 | */ 174 | public static PushNotificationPayload test() { 175 | final PushNotificationPayload payload = complex(); 176 | payload.setPreSendConfiguration(1); 177 | return payload; 178 | } 179 | 180 | /** 181 | * Create an empty payload which you can configure later. 182 | * This method is usually used to create complex or custom payloads. 183 | * Note: the payload actually contains the default "aps" 184 | * dictionary required by APNS. 185 | * 186 | * @return a blank payload that can be customized 187 | */ 188 | public static PushNotificationPayload complex() { 189 | return new PushNotificationPayload(); 190 | } 191 | 192 | /** 193 | * Create a PushNotificationPayload object from a preformatted JSON payload. 194 | * 195 | * @param rawJSON a JSON-formatted string representing a payload (ex: {"aps":{"alert":"Hello World!"}} ) 196 | * @return a ready-to-send payload 197 | * @throws JSONException if any exception occurs parsing the JSON string 198 | */ 199 | public static PushNotificationPayload fromJSON(final String rawJSON) throws JSONException { 200 | return new PushNotificationPayload(rawJSON); 201 | } 202 | 203 | /** 204 | * Add a badge. 205 | * 206 | * @param badge a badge number 207 | * @throws JSONException 208 | */ 209 | public void addBadge(final int badge) throws JSONException { 210 | logger.debug("Adding badge [" + badge + "]"); 211 | put("badge", badge, this.apsDictionary, true); 212 | } 213 | 214 | /** 215 | * Add a sound. 216 | * 217 | * @param sound the name of a sound 218 | * @throws JSONException 219 | */ 220 | public void addSound(final String sound) throws JSONException { 221 | logger.debug("Adding sound [" + sound + "]"); 222 | put("sound", sound, this.apsDictionary, true); 223 | } 224 | 225 | /** 226 | * Add a simple alert message. 227 | * Note: you cannot add a simple and a custom alert in the same payload. 228 | * 229 | * @param alertMessage the alert's message 230 | * @throws JSONException 231 | */ 232 | public void addAlert(final String alertMessage) throws JSONException { 233 | final String previousAlert = getCompatibleProperty(ALERT, String.class, "A custom alert (\"%s\") was already added to this payload"); 234 | logger.debug("Adding alert [" + alertMessage + "]" + (previousAlert != null ? " replacing previous alert [" + previousAlert + "]" : "")); 235 | put(ALERT, alertMessage, this.apsDictionary, false); 236 | } 237 | 238 | /** 239 | * Get the custom alert object, creating it if it does not yet exist. 240 | * 241 | * @return the JSON object defining the custom alert 242 | * @throws JSONException if a simple alert has already been added to this payload 243 | */ 244 | private JSONObject getOrAddCustomAlert() throws JSONException { 245 | JSONObject alert = getCompatibleProperty(ALERT, JSONObject.class, "A simple alert (\"%s\") was already added to this payload"); 246 | if (alert == null) { 247 | alert = new JSONObject(); 248 | put(ALERT, alert, this.apsDictionary, false); 249 | } 250 | return alert; 251 | } 252 | 253 | /** 254 | * Get the value of a given property, but only if it is of the expected class. 255 | * If the value exists but is of a different class than expected, an 256 | * exception is thrown. 257 | *

258 | * This method simply invokes the other getCompatibleProperty method with the root aps dictionary. 259 | * 260 | * @param the property value's class 261 | * @param propertyName the name of the property to get 262 | * @param expectedClass the property value's expected (required) class 263 | * @param exceptionMessage the exception message to throw if the value is not of the expected class 264 | * @return the property's value 265 | * @throws JSONException 266 | */ 267 | private T getCompatibleProperty(final String propertyName, final Class expectedClass, final String exceptionMessage) throws JSONException { 268 | return getCompatibleProperty(propertyName, expectedClass, exceptionMessage, this.apsDictionary); 269 | } 270 | 271 | /** 272 | * Get the value of a given property, but only if it is of the expected class. 273 | * If the value exists but is of a different class than expected, an 274 | * exception is thrown. 275 | *

276 | * This method is useful for properly supporting properties that can have a simple 277 | * or complex value (such as "alert") 278 | * 279 | * @param the property value's class 280 | * @param propertyName the name of the property to get 281 | * @param expectedClass the property value's expected (required) class 282 | * @param exceptionMessage the exception message to throw if the value is not of the expected class 283 | * @param dictionary the dictionary where to get the property from 284 | * @return the property's value 285 | * @throws JSONException 286 | */ 287 | @SuppressWarnings("unchecked") 288 | private T getCompatibleProperty(final String propertyName, final Class expectedClass, String exceptionMessage, final JSONObject dictionary) throws JSONException { 289 | Object propertyValue = null; 290 | try { 291 | propertyValue = dictionary.get(propertyName); 292 | } catch (final Exception e) { 293 | // empty 294 | } 295 | if (propertyValue == null) { 296 | return null; 297 | } 298 | if (propertyValue.getClass().equals(expectedClass)) { 299 | return (T) propertyValue; 300 | } 301 | try { 302 | throw new PayloadAlertAlreadyExistsException(String.format(exceptionMessage, propertyValue)); 303 | } catch (final IllegalFormatException e) { 304 | throw new PayloadAlertAlreadyExistsException(exceptionMessage); 305 | } 306 | 307 | } 308 | 309 | /** 310 | * Create a custom alert (if none exist) and add a body to the custom alert. 311 | * 312 | * @param body the body of the alert 313 | * @throws JSONException if the custom alert cannot be added because a simple alert already exists 314 | */ 315 | public void addCustomAlertBody(final String body) throws JSONException { 316 | put("body", body, getOrAddCustomAlert(), false); 317 | } 318 | 319 | /** 320 | * Create a custom alert (if none exist) and add a custom text for the right button of the popup. 321 | * 322 | * @param actionLocKey the title of the alert's right button, or null to remove the button 323 | * @throws JSONException if the custom alert cannot be added because a simple alert already exists 324 | */ 325 | public void addCustomAlertActionLocKey(final String actionLocKey) throws JSONException { 326 | final Object value = actionLocKey != null ? actionLocKey : JSONObject.NULL; 327 | put("action-loc-key", value, getOrAddCustomAlert(), false); 328 | } 329 | 330 | /** 331 | * Create a custom alert (if none exist) and add a loc-key parameter. 332 | * 333 | * @param locKey 334 | * @throws JSONException if the custom alert cannot be added because a simple alert already exists 335 | */ 336 | public void addCustomAlertLocKey(final String locKey) throws JSONException { 337 | put("loc-key", locKey, getOrAddCustomAlert(), false); 338 | } 339 | 340 | /** 341 | * Create a custom alert (if none exist) and add sub-parameters for the loc-key parameter. 342 | * 343 | * @param args 344 | * @throws JSONException if the custom alert cannot be added because a simple alert already exists 345 | */ 346 | public void addCustomAlertLocArgs(final List args) throws JSONException { 347 | put("loc-args", args, getOrAddCustomAlert(), false); 348 | } 349 | 350 | /** 351 | * Sets the content available. 352 | * 353 | * @param available 354 | * @throws JSONException 355 | */ 356 | public void setContentAvailable(final boolean available) throws JSONException { 357 | if (available) { 358 | put("content-available", 1, this.apsDictionary, false); 359 | } else { 360 | remove("content-available", this.apsDictionary); 361 | } 362 | } 363 | 364 | /** 365 | * Return the maximum payload size in bytes. 366 | * For APNS payloads, this method returns 256. 367 | * 368 | * @return the maximum payload size in bytes (256) 369 | */ 370 | @Override 371 | public int getMaximumPayloadSize() { 372 | return MAXIMUM_PAYLOAD_LENGTH; 373 | } 374 | 375 | void verifyPayloadIsNotEmpty() { 376 | if (getPreSendConfiguration() != 0) { 377 | return; 378 | } 379 | 380 | if (toString().equals("{\"aps\":{}}")) { 381 | throw new IllegalArgumentException("Payload cannot be empty"); 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /src/main/java/javapns/notification/Payload.java: -------------------------------------------------------------------------------- 1 | package javapns.notification; 2 | 3 | import javapns.notification.exceptions.PayloadMaxSizeExceededException; 4 | import javapns.notification.exceptions.PayloadMaxSizeProbablyExceededException; 5 | import org.json.JSONException; 6 | import org.json.JSONObject; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | import java.util.List; 11 | 12 | /** 13 | * Abstract class representing a payload that can be transmitted to Apple. 14 | *

15 | * By default, this class has no payload content at all. Subclasses are 16 | * responsible for imposing specific content based on the specifications 17 | * they are intended to implement (such as the 'aps' dictionnary for APS 18 | * payloads). 19 | * 20 | * @author Sylvain Pedneault 21 | */ 22 | public abstract class Payload { 23 | static final Logger logger = LoggerFactory.getLogger(Payload.class); 24 | 25 | /* Character encoding specified by Apple documentation */ 26 | private static final String DEFAULT_CHARACTER_ENCODING = "UTF-8"; 27 | private static final String ADDING_CUSTOM_DICTIONARY = "Adding custom Dictionary ["; 28 | private static final String DELIMITER_START = "] = ["; 29 | private static final String DELIMITED_END = "]"; 30 | /* The root Payload */ 31 | private final JSONObject payload; 32 | 33 | /* Character encoding to use for streaming the payload (should be UTF-8) */ 34 | private String characterEncoding = DEFAULT_CHARACTER_ENCODING; 35 | 36 | /* Number of seconds after which this payload should expire */ 37 | @SuppressWarnings("PointlessArithmeticExpression") 38 | private int expiry = 1 * 24 * 60 * 60; 39 | 40 | private boolean payloadSizeEstimatedWhenAdding = false; 41 | 42 | private int preSendConfiguration = 0; 43 | 44 | /** 45 | * Construct a Payload object with a blank root JSONObject 46 | */ 47 | protected Payload() { 48 | super(); 49 | this.payload = new JSONObject(); 50 | } 51 | 52 | /** 53 | * Construct a Payload object from a JSON-formatted string 54 | * 55 | * @param rawJSON a JSON-formatted string (ex: {"aps":{"alert":"Hello World!"}} ) 56 | * @throws JSONException thrown if a exception occurs while parsing the JSON string 57 | */ 58 | Payload(final String rawJSON) throws JSONException { 59 | super(); 60 | this.payload = new JSONObject(rawJSON); 61 | } 62 | 63 | /** 64 | * Get the actual JSON object backing this payload. 65 | * 66 | * @return a JSONObject 67 | */ 68 | public JSONObject getPayload() { 69 | return this.payload; 70 | } 71 | 72 | /** 73 | * Add a custom dictionnary with a string value 74 | * 75 | * @param name 76 | * @param value 77 | * @throws JSONException 78 | */ 79 | public void addCustomDictionary(final String name, final String value) throws JSONException { 80 | logger.debug(ADDING_CUSTOM_DICTIONARY + name + DELIMITER_START + value + DELIMITED_END); 81 | put(name, value, payload, false); 82 | } 83 | 84 | /** 85 | * Add a custom dictionnary with a int value 86 | * 87 | * @param name 88 | * @param value 89 | * @throws JSONException 90 | */ 91 | public void addCustomDictionary(final String name, final int value) throws JSONException { 92 | logger.debug(ADDING_CUSTOM_DICTIONARY + name + DELIMITER_START + value + DELIMITED_END); 93 | put(name, value, payload, false); 94 | } 95 | 96 | /** 97 | * Add a custom dictionnary with multiple values 98 | * 99 | * @param name 100 | * @param values 101 | * @throws JSONException 102 | */ 103 | public void addCustomDictionary(final String name, final List values) throws JSONException { 104 | logger.debug(ADDING_CUSTOM_DICTIONARY + name + "] = (list)"); 105 | put(name, values, payload, false); 106 | } 107 | 108 | /** 109 | * Add a custom dictionnary with object 110 | * 111 | * @param name 112 | * @param value 113 | * @throws JSONException 114 | */ 115 | public void addCustomDictionary(final String name, final Object value) throws JSONException { 116 | logger.debug(ADDING_CUSTOM_DICTIONARY + name + DELIMITER_START + value + DELIMITED_END); 117 | put(name, value, payload, false); 118 | } 119 | 120 | /** 121 | * Get the string representation 122 | */ 123 | public String toString() { 124 | return this.payload.toString(); 125 | } 126 | 127 | void verifyPayloadIsNotEmpty() { 128 | if (getPreSendConfiguration() != 0) { 129 | return; 130 | } 131 | if (toString().equals("{}")) { 132 | throw new IllegalArgumentException("Payload cannot be empty"); 133 | } 134 | } 135 | 136 | /** 137 | * Get this payload as a byte array using the preconfigured character encoding. 138 | * 139 | * @return byte[] bytes ready to be streamed directly to Apple servers 140 | */ 141 | public byte[] getPayloadAsBytes() throws Exception { 142 | final byte[] payloadBytesUnchecked = getPayloadAsBytesUnchecked(); 143 | validateMaximumPayloadSize(payloadBytesUnchecked.length); 144 | return payloadBytesUnchecked; 145 | } 146 | 147 | /** 148 | * Get this payload as a byte array using the preconfigured character encoding. 149 | * This method does NOT check if the payload exceeds the maximum payload length. 150 | * 151 | * @return byte[] bytes ready to be streamed directly to Apple servers (but that might exceed the maximum size limit) 152 | */ 153 | private byte[] getPayloadAsBytesUnchecked() throws Exception { 154 | byte[] bytes; 155 | try { 156 | bytes = toString().getBytes(characterEncoding); 157 | } catch (final Exception ex) { 158 | bytes = toString().getBytes(); 159 | } 160 | return bytes; 161 | } 162 | 163 | /** 164 | * Get the number of bytes that the payload will occupy when streamed. 165 | * 166 | * @return a number of bytes 167 | * @throws Exception 168 | */ 169 | public int getPayloadSize() throws Exception { 170 | return getPayloadAsBytesUnchecked().length; 171 | } 172 | 173 | /** 174 | * Check if the payload exceeds the maximum size allowed. 175 | * The maximum size allowed is returned by the getMaximumPayloadSize() method. 176 | * 177 | * @return true if the payload exceeds the maximum size allowed, false otherwise 178 | */ 179 | private boolean isPayloadTooLong() { 180 | try { 181 | final byte[] bytes = getPayloadAsBytesUnchecked(); 182 | if (bytes.length > getMaximumPayloadSize()) { 183 | return true; 184 | } 185 | } catch (final Exception e) { 186 | // empty 187 | } 188 | return false; 189 | } 190 | 191 | /** 192 | * Estimate the size that this payload will take after adding a given property. 193 | * For performance reasons, this estimate is not as reliable as actually adding 194 | * the property and checking the payload size afterwards. 195 | *

196 | * Currently works well with strings and numbers. 197 | * 198 | * @param propertyName the name of the property to use for calculating the estimation 199 | * @param propertyValue the value of the property to use for calculating the estimation 200 | * @return an estimated payload size if the property were to be added to the payload 201 | */ 202 | private int estimatePayloadSizeAfterAdding(final String propertyName, final Object propertyValue) { 203 | try { 204 | int estimatedSize = getPayloadAsBytesUnchecked().length; 205 | if (propertyName != null && propertyValue != null) { 206 | estimatedSize += 6; // ,"":"" 207 | estimatedSize += propertyName.getBytes(getCharacterEncoding()).length; 208 | int estimatedValueSize = 0; 209 | 210 | if (propertyValue instanceof String || propertyValue instanceof Number) { 211 | estimatedValueSize = propertyValue.toString().getBytes(getCharacterEncoding()).length; 212 | } 213 | 214 | estimatedSize += estimatedValueSize; 215 | } 216 | return estimatedSize; 217 | } catch (final Exception e) { 218 | try { 219 | return getPayloadSize(); 220 | } catch (final Exception e1) { 221 | return 0; 222 | } 223 | } 224 | } 225 | 226 | /** 227 | * Validate if the estimated payload size after adding a given property will be allowed. 228 | * For performance reasons, this estimate is not as reliable as actually adding 229 | * the property and checking the payload size afterwards. 230 | * 231 | * @param propertyName the name of the property to use for calculating the estimation 232 | * @param propertyValue the value of the property to use for calculating the estimation 233 | * @return true if the payload size is not expected to exceed the maximum allowed, false if it might be too big 234 | */ 235 | public boolean isEstimatedPayloadSizeAllowedAfterAdding(final String propertyName, final Object propertyValue) { 236 | final int maximumPayloadSize = getMaximumPayloadSize(); 237 | final int estimatedPayloadSize = estimatePayloadSizeAfterAdding(propertyName, propertyValue); 238 | return estimatedPayloadSize <= maximumPayloadSize; 239 | } 240 | 241 | /** 242 | * Validate that the payload does not exceed the maximum size allowed. 243 | * If the limit is exceeded, a PayloadMaxSizeExceededException is thrown. 244 | * 245 | * @param currentPayloadSize the total size of the payload in bytes 246 | * @throws PayloadMaxSizeExceededException if the payload exceeds the maximum size allowed 247 | */ 248 | private void validateMaximumPayloadSize(final int currentPayloadSize) throws PayloadMaxSizeExceededException { 249 | final int maximumPayloadSize = getMaximumPayloadSize(); 250 | if (currentPayloadSize > maximumPayloadSize) { 251 | throw new PayloadMaxSizeExceededException(maximumPayloadSize, currentPayloadSize); 252 | } 253 | } 254 | 255 | /** 256 | * Puts a property in a JSONObject, while possibly checking for estimated payload size violation. 257 | * 258 | * @param propertyName the name of the property to use for calculating the estimation 259 | * @param propertyValue the value of the property to use for calculating the estimation 260 | * @param object the JSONObject to put the property in 261 | * @param opt true to use putOpt, false to use put 262 | * @throws JSONException 263 | */ 264 | void put(final String propertyName, final Object propertyValue, final JSONObject object, final boolean opt) throws JSONException { 265 | try { 266 | if (isPayloadSizeEstimatedWhenAdding()) { 267 | final int maximumPayloadSize = getMaximumPayloadSize(); 268 | final int estimatedPayloadSize = estimatePayloadSizeAfterAdding(propertyName, propertyValue); 269 | final boolean estimatedToExceed = estimatedPayloadSize > maximumPayloadSize; 270 | if (estimatedToExceed) { 271 | throw new PayloadMaxSizeProbablyExceededException(maximumPayloadSize, estimatedPayloadSize); 272 | } 273 | } 274 | } catch (final PayloadMaxSizeProbablyExceededException e) { 275 | throw e; 276 | } catch (final Exception e) { 277 | // empty 278 | } 279 | if (opt) { 280 | object.putOpt(propertyName, propertyValue); 281 | } else { 282 | object.put(propertyName, propertyValue); 283 | } 284 | } 285 | 286 | Object remove(final String propertyName, final JSONObject object) { 287 | return object.remove(propertyName); 288 | } 289 | 290 | /** 291 | * Indicates if payload size is estimated and controlled when adding properties (default is false). 292 | * 293 | * @return true to throw an exception if the estimated size is too big when adding a property, false otherwise 294 | */ 295 | public boolean isPayloadSizeEstimatedWhenAdding() { 296 | return payloadSizeEstimatedWhenAdding; 297 | } 298 | 299 | /** 300 | * Indicate if payload size should be estimated and controlled when adding properties (default is false). 301 | * 302 | * @param checked true to throw an exception if the estimated size is too big when adding a property, false otherwise 303 | */ 304 | public void setPayloadSizeEstimatedWhenAdding(final boolean checked) { 305 | this.payloadSizeEstimatedWhenAdding = checked; 306 | } 307 | 308 | /** 309 | * Return the maximum payload size in bytes. 310 | * By default, this method returns Integer.MAX_VALUE. 311 | * Subclasses should override this method to provide their own limit. 312 | * 313 | * @return the maximum payload size in bytes 314 | */ 315 | int getMaximumPayloadSize() { 316 | return Integer.MAX_VALUE; 317 | } 318 | 319 | /** 320 | * Returns the character encoding that will be used by getPayloadAsBytes(). 321 | * Default is UTF-8, as per Apple documentation. 322 | * 323 | * @return a character encoding 324 | */ 325 | public String getCharacterEncoding() { 326 | return characterEncoding; 327 | } 328 | 329 | /** 330 | * Changes the character encoding for streaming the payload. 331 | * Character encoding is preset to UTF-8, as Apple documentation specifies. 332 | * Therefore, unless you are working on a special project, you should leave it as is. 333 | * 334 | * @param characterEncoding a valid character encoding that String.getBytes(encoding) will accept 335 | */ 336 | public void setCharacterEncoding(final String characterEncoding) { 337 | this.characterEncoding = characterEncoding; 338 | } 339 | 340 | /** 341 | * Return the number of seconds after which this payload should expire. 342 | * 343 | * @return a number of seconds 344 | */ 345 | public int getExpiry() { 346 | return expiry; 347 | } 348 | 349 | /** 350 | * Set the number of seconds after which this payload should expire. 351 | * Default is one (1) day. 352 | * 353 | * @param seconds 354 | */ 355 | public void setExpiry(final int seconds) { 356 | this.expiry = seconds; 357 | } 358 | 359 | /** 360 | * Enables a special simulation mode which causes the library to behave 361 | * as usual *except* that at the precise point where the payload would 362 | * actually be streamed out to Apple, it is not. 363 | * 364 | * @return the same payload 365 | */ 366 | public Payload asSimulationOnly() { 367 | setExpiry(919191); 368 | return this; 369 | } 370 | 371 | int getPreSendConfiguration() { 372 | return preSendConfiguration; 373 | } 374 | 375 | void setPreSendConfiguration(final int preSendConfiguration) { 376 | this.preSendConfiguration = preSendConfiguration; 377 | } 378 | 379 | } 380 | -------------------------------------------------------------------------------- /src/test/java/javapns/test/SpecificNotificationTests.java: -------------------------------------------------------------------------------- 1 | package javapns.test; 2 | 3 | import javapns.Push; 4 | import javapns.communication.exceptions.CommunicationException; 5 | import javapns.communication.exceptions.KeystoreException; 6 | import javapns.devices.Device; 7 | import javapns.devices.implementations.basic.BasicDevice; 8 | import javapns.notification.*; 9 | import javapns.notification.transmission.NotificationThread; 10 | import javapns.notification.transmission.NotificationThreads; 11 | import javapns.notification.transmission.PushQueue; 12 | import org.json.JSONException; 13 | 14 | import java.io.BufferedInputStream; 15 | import java.io.FileInputStream; 16 | import java.io.InputStream; 17 | import java.util.ArrayList; 18 | import java.util.List; 19 | 20 | /** 21 | * Specific test cases intended for the project's developers. 22 | * 23 | * @author Sylvain Pedneault 24 | */ 25 | class SpecificNotificationTests extends TestFoundation { 26 | private SpecificNotificationTests() { 27 | // empty 28 | } 29 | 30 | /** 31 | * Execute this class from the command line to run tests. 32 | * 33 | * @param args 34 | */ 35 | public static void main(final String[] args) { 36 | /* Verify that the test is being invoked */ 37 | if (!verifyCorrectUsage(NotificationTest.class, args, "keystore-path", "keystore-password", "device-token", "[production|sandbox]", "[test-name]")) { 38 | return; 39 | } 40 | 41 | /* Push an alert */ 42 | runTest(args); 43 | } 44 | 45 | /** 46 | * Push a test notification to a device, given command-line parameters. 47 | * 48 | * @param args 49 | */ 50 | private static void runTest(final String[] args) { 51 | final String keystore = args[0]; 52 | final String password = args[1]; 53 | final String token = args[2]; 54 | final boolean production = args.length >= 4 && args[3].equalsIgnoreCase("production"); 55 | 56 | String testName = args.length >= 5 ? args[4] : null; 57 | if (testName == null || testName.length() == 0) { 58 | testName = "default"; 59 | } 60 | 61 | try { 62 | SpecificNotificationTests.class.getDeclaredMethod("test_" + testName, String.class, String.class, String.class, boolean.class).invoke(null, keystore, password, token, production); 63 | } catch (final NoSuchMethodException e) { 64 | System.out.println(String.format("Error: test '%s' not found. Test names are case-sensitive", testName)); 65 | } catch (final Exception e) { 66 | (e.getCause() != null ? e.getCause() : e).printStackTrace(); 67 | } 68 | } 69 | 70 | private static void test_PushHelloWorld(final String keystore, final String password, final String token, final boolean production) throws CommunicationException, KeystoreException { 71 | final List notifications = Push.alert("Hello World!", keystore, password, production, token); 72 | NotificationTest.printPushedNotifications(notifications); 73 | } 74 | 75 | private static void test_Issue74(final String keystore, final String password, final String token, final boolean production) { 76 | try { 77 | System.out.println(""); 78 | System.out.println("TESTING 257-BYTES PAYLOAD WITH SIZE ESTIMATION ENABLED"); 79 | /* Expected result: PayloadMaxSizeProbablyExceededException when the alert is added to the payload */ 80 | pushSpecificPayloadSize(keystore, password, token, production, true, 257); 81 | } catch (final Exception e) { 82 | e.printStackTrace(); 83 | } 84 | try { 85 | System.out.println(""); 86 | System.out.println("TESTING 257-BYTES PAYLOAD WITH SIZE ESTIMATION DISABLED"); 87 | /* Expected result: PayloadMaxSizeExceededException when the payload is pushed */ 88 | pushSpecificPayloadSize(keystore, password, token, production, false, 257); 89 | } catch (final Exception e) { 90 | e.printStackTrace(); 91 | } 92 | try { 93 | System.out.println(""); 94 | System.out.println("TESTING 256-BYTES PAYLOAD"); 95 | /* Expected result: no exception */ 96 | pushSpecificPayloadSize(keystore, password, token, production, false, 256); 97 | } catch (final Exception e) { 98 | e.printStackTrace(); 99 | } 100 | } 101 | 102 | private static void test_Issue75(final String keystore, final String password, final String token, final boolean production) { 103 | try { 104 | System.out.println(""); 105 | System.out.println("TESTING 257-BYTES PAYLOAD WITH SIZE ESTIMATION ENABLED"); 106 | final NewsstandNotificationPayload payload = NewsstandNotificationPayload.contentAvailable(); 107 | debugPayload(payload); 108 | 109 | final List notifications = Push.payload(payload, keystore, password, production, token); 110 | NotificationTest.printPushedNotifications(notifications); 111 | } catch (final Exception e) { 112 | e.printStackTrace(); 113 | } 114 | } 115 | 116 | private static void test_Issue82(final String keystore, final String password, final String token, final boolean production) { 117 | try { 118 | System.out.println(""); 119 | final Payload payload = PushNotificationPayload.test(); 120 | 121 | System.out.println("TESTING ISSUE #82 PART 1"); 122 | final List notifications = Push.payload(payload, keystore, password, production, 1, token); 123 | NotificationTest.printPushedNotifications(notifications); 124 | System.out.println("ISSUE #82 PART 1 TESTED"); 125 | 126 | System.out.println("TESTING ISSUE #82 PART2"); 127 | final AppleNotificationServer server = new AppleNotificationServerBasicImpl(keystore, password, production); 128 | final NotificationThread thread = new NotificationThread(new PushNotificationManager(), server, payload, token); 129 | thread.setListener(NotificationTest.DEBUGGING_PROGRESS_LISTENER); 130 | thread.start(); 131 | System.out.println("ISSUE #82 PART 2 TESTED"); 132 | 133 | } catch (final Exception e) { 134 | e.printStackTrace(); 135 | } 136 | } 137 | 138 | private static void test_Issue87(final String keystore, final String password, final String token, final boolean production) { 139 | try { 140 | System.out.println("TESTING ISSUES #87 AND #88"); 141 | 142 | final InputStream ks = new BufferedInputStream(new FileInputStream(keystore)); 143 | final PushQueue queue = Push.queue(ks, password, false, 3); 144 | queue.start(); 145 | queue.add(PushNotificationPayload.test(), token); 146 | queue.add(PushNotificationPayload.test(), token); 147 | queue.add(PushNotificationPayload.test(), token); 148 | queue.add(PushNotificationPayload.test(), token); 149 | Thread.sleep(10000); 150 | final List criticalExceptions = queue.getCriticalExceptions(); 151 | for (final Exception exception : criticalExceptions) { 152 | exception.printStackTrace(); 153 | } 154 | Thread.sleep(10000); 155 | 156 | List pushedNotifications = queue.getPushedNotifications(); 157 | NotificationTest.printPushedNotifications("BEFORE CLEAR:", pushedNotifications); 158 | 159 | queue.clearPushedNotifications(); 160 | 161 | pushedNotifications = queue.getPushedNotifications(); 162 | NotificationTest.printPushedNotifications("AFTER CLEAR:", pushedNotifications); 163 | 164 | Thread.sleep(50000); 165 | System.out.println("ISSUES #87 AND #88 TESTED"); 166 | 167 | } catch (final Exception e) { 168 | e.printStackTrace(); 169 | } 170 | } 171 | 172 | private static void test_Issue88(final String keystore, final String password, final String token, final boolean production) { 173 | try { 174 | System.out.println("TESTING ISSUES #88"); 175 | 176 | // List devices = new Vector(); 177 | // for (int i = 0; i < 5; i++) { 178 | // devices.add(token); 179 | // } 180 | // PushedNotifications notifications = Push.payload(PushNotificationPayload.test(), keystore, password, false, devices); 181 | final PushQueue queue = Push.queue(keystore, password, false, 1); 182 | queue.start(); 183 | queue.add(PushNotificationPayload.test(), token); 184 | queue.add(PushNotificationPayload.test(), token); 185 | queue.add(PushNotificationPayload.test(), token); 186 | queue.add(PushNotificationPayload.test(), token); 187 | Thread.sleep(10000); 188 | 189 | final PushedNotifications notifications = queue.getPushedNotifications(); 190 | NotificationTest.printPushedNotifications(notifications); 191 | 192 | Thread.sleep(5000); 193 | System.out.println("ISSUES #88 TESTED"); 194 | 195 | } catch (final Exception e) { 196 | e.printStackTrace(); 197 | } 198 | } 199 | 200 | private static void test_Issue99(final String keystore, final String password, final String token, final boolean production) { 201 | try { 202 | System.out.println(""); 203 | System.out.println("TESTING ISSUE #99"); 204 | final PushNotificationPayload payload = PushNotificationPayload.complex(); 205 | payload.addCustomAlertBody("Hello World!"); 206 | payload.addCustomAlertActionLocKey(null); 207 | debugPayload(payload); 208 | 209 | final List notifications = Push.payload(payload, keystore, password, production, token); 210 | NotificationTest.printPushedNotifications(notifications); 211 | System.out.println("ISSUE #99 TESTED"); 212 | } catch (final Exception e) { 213 | e.printStackTrace(); 214 | } 215 | } 216 | 217 | private static void test_Issue102(final String keystore, final String password, String token, final boolean production) { 218 | try { 219 | System.out.println(""); 220 | System.out.println("TESTING ISSUE #102"); 221 | final int devices = 10000; 222 | final int threads = 20; 223 | final boolean simulation = false; 224 | final String realToken = token; 225 | token = null; 226 | 227 | try { 228 | System.out.println("Creating PushNotificationManager and AppleNotificationServer"); 229 | final AppleNotificationServer server = new AppleNotificationServerBasicImpl(keystore, password, production); 230 | System.out.println("Creating payload (simulation mode)"); 231 | //Payload payload = PushNotificationPayload.alert("Hello World!"); 232 | final Payload payload = PushNotificationPayload.test(); 233 | 234 | System.out.println("Generating " + devices + " fake devices"); 235 | final List deviceList = new ArrayList<>(devices); 236 | 237 | //noinspection Duplicates 238 | for (int i = 0; i < devices; i++) { 239 | String tokenToUse = token; 240 | if (tokenToUse == null || tokenToUse.length() != 64) { 241 | tokenToUse = "123456789012345678901234567890123456789012345678901234567" + (1000000 + i); 242 | } 243 | deviceList.add(new BasicDevice(tokenToUse)); 244 | } 245 | deviceList.add(new BasicDevice(realToken)); 246 | System.out.println("Creating " + threads + " notification threads"); 247 | final NotificationThreads work = new NotificationThreads(server, simulation ? payload.asSimulationOnly() : payload, deviceList, threads); 248 | //work.setMaxNotificationsPerConnection(10000); 249 | //System.out.println("Linking notification work debugging listener"); 250 | //work.setListener(DEBUGGING_PROGRESS_LISTENER); 251 | 252 | System.out.println("Starting all threads..."); 253 | final long timestamp1 = System.currentTimeMillis(); 254 | work.start(); 255 | System.out.println("All threads started, waiting for them..."); 256 | work.waitForAllThreads(); 257 | final long timestamp2 = System.currentTimeMillis(); 258 | System.out.println("All threads finished in " + (timestamp2 - timestamp1) + " milliseconds"); 259 | 260 | NotificationTest.printPushedNotifications(work.getSuccessfulNotifications()); 261 | 262 | } catch (final Exception e) { 263 | e.printStackTrace(); 264 | } 265 | 266 | // List notifications = Push.payload(payload, keystore, password, production, token); 267 | // NotificationTest.printPushedNotifications(notifications); 268 | System.out.println("ISSUE #102 TESTED"); 269 | } catch (final Exception e) { 270 | e.printStackTrace(); 271 | } 272 | } 273 | 274 | private static void test_ThreadPoolFeature(final String keystore, final String password, final String token, final boolean production) throws Exception { 275 | try { 276 | System.out.println(""); 277 | System.out.println("TESTING THREAD POOL FEATURE"); 278 | 279 | final AppleNotificationServer server = new AppleNotificationServerBasicImpl(keystore, password, production); 280 | final NotificationThreads pool = new NotificationThreads(server, 3).start(); 281 | final Device device = new BasicDevice(token); 282 | 283 | System.out.println("Thread pool started and waiting..."); 284 | 285 | System.out.println("Sleeping 5 seconds before queuing payloads..."); 286 | Thread.sleep(5 * 1000); 287 | 288 | for (int i = 1; i <= 4; i++) { 289 | final Payload payload = PushNotificationPayload.alert("Test " + i); 290 | final NotificationThread threadForPayload = (NotificationThread) pool.add(new PayloadPerDevice(payload, device)); 291 | System.out.println("Queued payload " + i + " to " + threadForPayload.getThreadNumber()); 292 | System.out.println("Sleeping 10 seconds before queuing another payload..."); 293 | Thread.sleep(10 * 1000); 294 | } 295 | System.out.println("Sleeping 10 more seconds let threads enough times to push the latest payload..."); 296 | Thread.sleep(10 * 1000); 297 | } catch (final Exception e) { 298 | e.printStackTrace(); 299 | } 300 | } 301 | 302 | private static void pushSpecificPayloadSize(final String keystore, final String password, final String token, final boolean production, final boolean checkWhenAdding, final int targetPayloadSize) throws CommunicationException, KeystoreException, JSONException { 303 | final StringBuilder buf = new StringBuilder(); 304 | for (int i = 0; i < targetPayloadSize - 20; i++) { 305 | buf.append('x'); 306 | } 307 | 308 | final String alertMessage = buf.toString(); 309 | final PushNotificationPayload payload = PushNotificationPayload.complex(); 310 | if (checkWhenAdding) { 311 | payload.setPayloadSizeEstimatedWhenAdding(true); 312 | } 313 | debugPayload(payload); 314 | 315 | final boolean estimateValid = payload.isEstimatedPayloadSizeAllowedAfterAdding("alert", alertMessage); 316 | System.out.println("Payload size estimated to be allowed: " + (estimateValid ? "yes" : "no")); 317 | payload.addAlert(alertMessage); 318 | debugPayload(payload); 319 | 320 | final List notifications = Push.payload(payload, keystore, password, production, token); 321 | NotificationTest.printPushedNotifications(notifications); 322 | } 323 | 324 | private static void debugPayload(final Payload payload) { 325 | System.out.println("^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^"); 326 | try { 327 | System.out.println("Payload size: " + payload.getPayloadSize()); 328 | } catch (final Exception e) { 329 | // empty 330 | } 331 | try { 332 | System.out.println("Payload representation: " + payload); 333 | } catch (final Exception e) { 334 | // empty 335 | } 336 | System.out.println(payload.isPayloadSizeEstimatedWhenAdding() ? "Payload size is estimated when adding properties" : "Payload size is only checked when it is complete"); 337 | System.out.println("vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv"); 338 | } 339 | 340 | } 341 | --------------------------------------------------------------------------------