diff --git a/src/main/java/com/notnoop/mpns/MpnsDelegate.java b/src/main/java/com/notnoop/mpns/MpnsDelegate.java new file mode 100644 index 0000000..0d21301 --- /dev/null +++ b/src/main/java/com/notnoop/mpns/MpnsDelegate.java @@ -0,0 +1,35 @@ +/* + * Copyright 2011, Mahmood Ali. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Mahmood Ali. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.notnoop.mpns; + +public interface MpnsDelegate { + public void messageSent(String messageId, MpnsResponse response); +} diff --git a/src/main/java/com/notnoop/mpns/MpnsResponse.java b/src/main/java/com/notnoop/mpns/MpnsResponse.java new file mode 100644 index 0000000..4e87b74 --- /dev/null +++ b/src/main/java/com/notnoop/mpns/MpnsResponse.java @@ -0,0 +1,144 @@ +/* + * Copyright 2011, Mahmood Ali. + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are + * met: + * + * * Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * * Redistributions in binary form must reproduce the above + * copyright notice, this list of conditions and the following disclaimer + * in the documentation and/or other materials provided with the + * distribution. + * * Neither the name of Mahmood Ali. nor the names of its + * contributors may be used to endorse or promote products derived from + * this software without specific prior written permission. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS + * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT + * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR + * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT + * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, + * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT + * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ +package com.notnoop.mpns; + +/** + * Represents the logical response of MpnsService + */ +public enum MpnsResponse { + /* + * The notification request was accepted and queued for delivery. + */ + RECEIVED(200, "Received", "Connected", "Active"), + + /** + * The notification request was accepted and queued for delivery. However, + * the device is temporarily disconnected. + */ + QUEUED(200, "Received", "Temporarily Disconnected", "Active"), + + /** + * Queue overflow. The web service should re-send the notification later. + * A best practice is to use an exponential backoff algorithm in minute + * increments. + */ + QUEUE_FULL(200, "QueueFull", null, "Active"), + + /** + * The push notification was received and dropped by the Push Notification + * Service. The Suppressed status can occur if the notification channel + * was configured to suppress push notifications for a particular push + * notification class. + */ + SUPPRESSED(200, "Suppressed", null, "Active"), + + /** + * This error occurs when the web service sends a notification request + * with a bad XML document or malformed notification URI. + */ + BAD_REQUEST(400, null, null, null), + + /** + * Sending this notification is unauthorized. This error can occur for one + * of the following reasons: + */ + UNAUTHORIZED(401, null, null, null), + + /** + * The subscription is invalid and is not present on the Push Notification + * Service. The web service should stop sending new notifications to this + * subscription, and drop the subscription state for its corresponding + * application session. + */ + EXPIRED(404, "Dropped", null, "Expired"), + + /** + * Invalid method (PUT, DELETE, CREATE). Only POST is allowed when sending + * a notification request. + */ + METHOD_NOT_ALLOWED(405, null, null, null), + + /** + * This error occurs when an unauthenticated web service has reached the + * per day throttling limit for a subscription. The web service can try to + * re-send the push notification every one hour after receiving this + * error. The web service may need to wait up to 24 hours before normal + * notification flow will resume. + */ + OVER_LIMIT(406, "Dropped", null, "Active"), + + /** + * The device is in an inactive state. The web service may re-attempt + * sending the request one time per hour at maximum after receiving this + * error. If the web service violates the maximum of one re-attempt per + * hour, the Push Notification Service will de-register or permanently + * block the web service. + */ + INACTIVATE_STATE(412, "Dropped", "Inactive", null), + + /** + * The Push Notification Service is unable to process the request. The web + * service should re-send the notification later. A best practice is to + * use an exponential backoff algorithm in minute increments. + */ + SERVICE_UNAVAILABLE(503, null, null, null); + + //// Response Code,NotificationStatus,DeviceConnectionStatus,SubscriptionStatus,Comments + private final int responseCode; + private final String notificationStatus; + private final String deviceConnectionStatus; + private final String subscriptionStatus; + + MpnsResponse(int responseCode, String notificationStatus, + String deviceConnectionStatus, + String subscriptionStatus) { + this.responseCode = responseCode; + this.notificationStatus = notificationStatus; + this.deviceConnectionStatus = deviceConnectionStatus; + this.subscriptionStatus = subscriptionStatus; + } + + public int getResponseCode() { + return responseCode; + } + + public String getNotificationStatus() { + return notificationStatus; + } + + public String getDeviceConnectionStatus() { + return deviceConnectionStatus; + } + + public String getSubscriptionStatus() { + return subscriptionStatus; + } +} diff --git a/src/main/java/com/notnoop/mpns/MpnsServiceBuilder.java b/src/main/java/com/notnoop/mpns/MpnsServiceBuilder.java index cbd7d19..bc98684 100644 --- a/src/main/java/com/notnoop/mpns/MpnsServiceBuilder.java +++ b/src/main/java/com/notnoop/mpns/MpnsServiceBuilder.java @@ -66,6 +66,8 @@ public class MpnsServiceBuilder { private HttpClient httpClient = null; private int timeout = -1; + private MpnsDelegate delegate; + /** * Constructs a new instance of {@code MpnsServiceBuilder} */ @@ -161,6 +163,11 @@ public MpnsServiceBuilder timeout(int timeout) { return this; } + public MpnsServiceBuilder delegate(MpnsDelegate delegate) { + this.delegate = delegate; + return this; + } + /** * Returns a fully initialized instance of {@link MpnsService}, * according to the requested settings. @@ -193,9 +200,9 @@ public MpnsService build() { // Configure service AbstractMpnsService service; if (pooledMax == 1) { - service = new MpnsServiceImpl(client); + service = new MpnsServiceImpl(client, delegate); } else { - service = new MpnsPooledService(client, executor); + service = new MpnsPooledService(client, executor, delegate); } if (isQueued) { diff --git a/src/main/java/com/notnoop/mpns/internal/MpnsPooledService.java b/src/main/java/com/notnoop/mpns/internal/MpnsPooledService.java index a3a51b6..c8398c5 100644 --- a/src/main/java/com/notnoop/mpns/internal/MpnsPooledService.java +++ b/src/main/java/com/notnoop/mpns/internal/MpnsPooledService.java @@ -37,15 +37,19 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.util.EntityUtils; +import com.notnoop.mpns.MpnsDelegate; +import com.notnoop.mpns.MpnsResponse; import com.notnoop.mpns.MpnsService; public class MpnsPooledService extends AbstractMpnsService implements MpnsService { private final HttpClient httpClient; private final ExecutorService executor; + private final MpnsDelegate delegate; - public MpnsPooledService(HttpClient httpClient, ExecutorService executor) { + public MpnsPooledService(HttpClient httpClient, ExecutorService executor, MpnsDelegate delegate) { this.httpClient = httpClient; this.executor = executor; + this.delegate = delegate; } @Override @@ -54,6 +58,12 @@ protected void push(final HttpPost request) { public void run() { try { HttpResponse response = httpClient.execute(request); + if (delegate != null) { + MpnsResponse r = Utilities.logicalResponseFor(response); + String messageId = Utilities.messageIdOf(response); + + delegate.messageSent(messageId, r); + } EntityUtils.consume(response.getEntity()); } catch (Exception e) { throw new RuntimeException(e); diff --git a/src/main/java/com/notnoop/mpns/internal/MpnsServiceImpl.java b/src/main/java/com/notnoop/mpns/internal/MpnsServiceImpl.java index 3dbae3e..539ba5c 100644 --- a/src/main/java/com/notnoop/mpns/internal/MpnsServiceImpl.java +++ b/src/main/java/com/notnoop/mpns/internal/MpnsServiceImpl.java @@ -38,20 +38,30 @@ import org.apache.http.client.methods.HttpPost; import org.apache.http.util.EntityUtils; +import com.notnoop.mpns.MpnsDelegate; +import com.notnoop.mpns.MpnsResponse; import com.notnoop.mpns.MpnsService; import com.notnoop.mpns.exceptions.NetworkIOException; public class MpnsServiceImpl extends AbstractMpnsService implements MpnsService { - protected final HttpClient httpClient; + private final HttpClient httpClient; + private final MpnsDelegate delegate; - public MpnsServiceImpl(HttpClient httpClient) { + public MpnsServiceImpl(HttpClient httpClient, MpnsDelegate delegate) { this.httpClient = httpClient; + this.delegate = delegate; } @Override protected void push(HttpPost request) { try { HttpResponse response = httpClient.execute(request); + if (delegate != null) { + MpnsResponse r = Utilities.logicalResponseFor(response); + String messageId = Utilities.messageIdOf(response); + + delegate.messageSent(messageId, r); + } EntityUtils.consume(response.getEntity()); } catch (ClientProtocolException e) { throw new RuntimeException(e); diff --git a/src/main/java/com/notnoop/mpns/internal/Utilities.java b/src/main/java/com/notnoop/mpns/internal/Utilities.java index 57d8f89..109640b 100644 --- a/src/main/java/com/notnoop/mpns/internal/Utilities.java +++ b/src/main/java/com/notnoop/mpns/internal/Utilities.java @@ -30,8 +30,12 @@ */ package com.notnoop.mpns.internal; +import org.apache.http.Header; +import org.apache.http.HttpResponse; import org.apache.http.impl.conn.tsccm.ThreadSafeClientConnManager; +import com.notnoop.mpns.MpnsResponse; + import java.io.InputStream; import java.io.InputStreamReader; import java.io.Reader; @@ -109,4 +113,44 @@ public static byte[] toUTF8(String content) { throw new AssertionError("The world is coming to an end! No UTF-8 support"); } } + + private static String headerValue(HttpResponse response, String header) { + Header[] headers = response.getHeaders(header); + + return headers.length == 0 ? null : headers[0].getValue(); + } + + private static MpnsResponse[] logicalResponses = MpnsResponse.values(); + public static MpnsResponse logicalResponseFor(HttpResponse response) { + for (MpnsResponse r: logicalResponses) { + if (r.getResponseCode() != response.getStatusLine().getStatusCode()) { + continue; + } + + if (r.getNotificationStatus() != null + && !r.getNotificationStatus().equals(headerValue(response, "X-NotificationStatus"))) { + continue; + } + + if (r.getDeviceConnectionStatus() != null + && !r.getNotificationStatus().equals(headerValue(response, "X-DeviceConnectionStatus"))) { + continue; + } + + if (r.getSubscriptionStatus() != null + && !r.getSubscriptionStatus().equals(headerValue(response, "X-SubscriptionStatus"))) { + continue; + } + + return r; + } + + // Didn't find anything + assert false; + return null; + } + + public static String messageIdOf(HttpResponse response) { + return headerValue(response, "X-MessageID"); + } } diff --git a/src/test/java/com/notnoop/mpns/internal/XMLEscapingTest.java b/src/test/java/com/notnoop/mpns/internal/XMLEscapingTest.java index 4923d48..b84fe19 100644 --- a/src/test/java/com/notnoop/mpns/internal/XMLEscapingTest.java +++ b/src/test/java/com/notnoop/mpns/internal/XMLEscapingTest.java @@ -42,7 +42,7 @@ public class XMLEscapingTest { public void escapesNull() { assertThat(escapeXml(null), is((String)null)); } - + @Test public void escapeEmpty() { assertThat(escapeXml(""), is("")); @@ -58,7 +58,7 @@ public void escapeNonEntities() { public void escapeAmpt() { assertThat(escapeXml("test&"), is("test&")); } - + @Test public void escapeAllValues() { assertThat(escapeXml("AT&T called \"Johns's father\" > 2 hours ago < 5 years ago."),