Repository: jchambers/pushy
Pull request #744 (2020-02-14 21:11:12)

Proposal: drop the notion of maximum payload lengths
So waaaaaaaay back at the beginning of time when we first introduced `ApnsPayloadBuilder`, the maximum length of an APNs payload was a mere 256 bytes. Payloads with fairly short messages and just a little bit of metadata could very, very easily overrun that limit, and so we designed `ApnsPayloadBuilder` with methods like `buildWithDefaultMaximumLength()` and `buildWithMaximumLength(int)`. We'd enforce those limits by shortening the contents of the `alert` text if possible.

These days, payloads can be up to 4,096 bytes (or 5,120 for VOIP notifications), and it's much less likely that the literal text of a notification is going to be the thing that's pushing notification size over the limit. With that in mind, I propose simplifying APNs payload construction by dropping the maximum length arguments in `ApnsPayloadBuilder`.

Pros:

- Greatly simplifies payload construction
- Has the potential to speed things up a bit
- Smooths the path to using alternative/customizable JSON serializers (see #678, #696)

Cons:

- Breaking API change (but as the README says, "the public API may change significantly before a 1.0 release")
- Users can construct larger-than-allowed payloads which will be rejected by the APNs server

Feedback is very welcome! I'd especially call on known JSON enthusiasts (@filiphr, @amir20001) for your input. Thank you!


Patch:
diff --git a/README.md b/README.md
index 4073cf8e..31cee3d0 100644
--- a/README.md
+++ b/README.md
@@ -85,7 +85,7 @@ final SimpleApnsPushNotification pushNotification;
     final ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
     payloadBuilder.setAlertBody("Example!");
 
-    final String payload = payloadBuilder.buildWithDefaultMaximumLength();
+    final String payload = payloadBuilder.build();
     final String token = TokenUtil.sanitizeTokenString("<efc7492 bdbd8209>");
 
     pushNotification = new SimpleApnsPushNotification(token, "com.example.myApp", payload);
diff --git a/benchmark/src/main/java/com/eatthepath/pushy/apns/ApnsClientBenchmark.java b/benchmark/src/main/java/com/eatthepath/pushy/apns/ApnsClientBenchmark.java
index 5bd8594a..3954452b 100644
--- a/benchmark/src/main/java/com/eatthepath/pushy/apns/ApnsClientBenchmark.java
+++ b/benchmark/src/main/java/com/eatthepath/pushy/apns/ApnsClientBenchmark.java
@@ -105,8 +105,8 @@ public class ApnsClientBenchmark {
             final ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
 
             for (int i = 0; i < this.notificationCount; i++) {
-                final String payload = payloadBuilder.setAlertBody(RandomStringUtils.randomAlphanumeric(MESSAGE_BODY_LENGTH))
-                        .buildWithDefaultMaximumLength();
+                final String payload =
+                        payloadBuilder.setAlertBody(RandomStringUtils.randomAlphanumeric(MESSAGE_BODY_LENGTH)).build();
 
                 this.pushNotifications.add(new SimpleApnsPushNotification(generateRandomDeviceToken(), TOPIC, payload));
             }
diff --git a/benchmark/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderBenchmark.java b/benchmark/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderBenchmark.java
index 667ce80c..837f3611 100644
--- a/benchmark/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderBenchmark.java
+++ b/benchmark/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderBenchmark.java
@@ -22,24 +22,18 @@
 
 package com.eatthepath.pushy.apns.util;
 
+import org.apache.commons.lang3.RandomStringUtils;
+import org.openjdk.jmh.annotations.*;
+
 import java.util.ArrayList;
 import java.util.List;
 
-import org.apache.commons.lang3.RandomStringUtils;
-import org.openjdk.jmh.annotations.Benchmark;
-import org.openjdk.jmh.annotations.Param;
-import org.openjdk.jmh.annotations.Scope;
-import org.openjdk.jmh.annotations.Setup;
-import org.openjdk.jmh.annotations.State;
-
 @State(Scope.Thread)
 public class ApnsPayloadBuilderBenchmark {
 
     private ApnsPayloadBuilder apnsPayloadBuilder;
 
-    private static final int MAXIMUM_PAYLOAD_SIZE = 4096;
-
-    @Param({"512", "8192"})
+    @Param({"512", "4096"})
     public int messageBodyLength;
 
     @Param({"BASIC_LATIN", "CJK_UNIFIED_IDEOGRAPHS"})
@@ -73,8 +67,8 @@ public class ApnsPayloadBuilderBenchmark {
     }
 
     @Benchmark
-    public String testBuildWithMaximumLength() {
+    public String testBuild() {
         this.apnsPayloadBuilder.setAlertBody(this.messageBody);
-        return this.apnsPayloadBuilder.buildWithMaximumLength(MAXIMUM_PAYLOAD_SIZE);
+        return this.apnsPayloadBuilder.build();
     }
 }
diff --git a/pushy/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilder.java b/pushy/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilder.java
index d6b450f3..30749209 100644
--- a/pushy/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilder.java
+++ b/pushy/src/main/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilder.java
@@ -25,8 +25,6 @@ package com.eatthepath.pushy.apns.util;
 import com.google.gson.Gson;
 import com.google.gson.GsonBuilder;
 
-import java.io.CharArrayWriter;
-import java.nio.charset.StandardCharsets;
 import java.util.*;
 
 /**
@@ -76,8 +74,6 @@ public class ApnsPayloadBuilder {
 
     private boolean preferStringRepresentationForAlerts = false;
 
-    private final CharArrayWriter buffer = new CharArrayWriter(DEFAULT_MAXIMUM_PAYLOAD_SIZE / 4);
-
     private static final String APS_KEY = "aps";
     private static final String ALERT_KEY = "alert";
     private static final String BADGE_KEY = "badge";
@@ -108,8 +104,6 @@ public class ApnsPayloadBuilder {
 
     private final HashMap<String, Object> customProperties = new HashMap<>();
 
-    private static final String ABBREVIATION_SUBSTRING = "…";
-
     private static final Gson GSON = new GsonBuilder().serializeNulls().disableHtmlEscaping().create();
 
     private static class SoundForCriticalAlert {
@@ -132,12 +126,6 @@ public class ApnsPayloadBuilder {
     public static final String DEFAULT_SOUND_FILENAME = "default";
 
     /**
-     * The default maximum size, in bytes, for a push notification payload.
-     *
-     * @see ApnsPayloadBuilder#buildWithDefaultMaximumLength()
-     */
-    public static final int DEFAULT_MAXIMUM_PAYLOAD_SIZE = 4096;
-
     /**
      * Sets whether this payload builder will attempt to represent alerts as strings when possible. Older versions of
      * the APNs specification recommended representing alerts as strings when only a literal alert body was provided,
@@ -700,33 +688,11 @@ public class ApnsPayloadBuilder {
     }
 
     /**
-     * <p>Returns a JSON representation of the push notification payload under construction. If the payload length is
-     * longer than the default maximum ({@value ApnsPayloadBuilder#DEFAULT_MAXIMUM_PAYLOAD_SIZE}
-     * bytes), the literal alert body will be shortened if possible. If the alert body cannot be shortened or is not
-     * present, an {@code IllegalArgumentException} is thrown.</p>
-     *
-     * @return a JSON representation of the payload under construction (possibly with an abbreviated alert body)
-     *
-     * @throws IllegalArgumentException if the payload is too large and cannot be compressed by truncating its literal
-     * alert message
-     */
-    public String buildWithDefaultMaximumLength() {
-        return this.buildWithMaximumLength(DEFAULT_MAXIMUM_PAYLOAD_SIZE);
-    }
-
-    /**
-     * <p>Returns a JSON representation of the push notification payload under construction. If the payload length is
-     * longer than the given maximum, the literal alert body will be shortened if possible. If the alert body cannot be
-     * shortened or is not present, an {@code IllegalArgumentException} is thrown.</p>
-     *
-     * @param maximumPayloadSize the maximum length of the payload in bytes
+     * Returns a JSON representation of the push notification payload under construction.
      *
      * @return a JSON representation of the payload under construction (possibly with an abbreviated alert body)
-     *
-     * @throws IllegalArgumentException if the payload is too large and cannot be compressed by truncating its literal
-     * alert message
      */
-    public String buildWithMaximumLength(final int maximumPayloadSize) {
+    public String build() {
         final Map<String, Object> payload = new HashMap<>();
 
         {
@@ -843,49 +809,7 @@ public class ApnsPayloadBuilder {
             payload.put(entry.getKey(), entry.getValue());
         }
 
-        this.buffer.reset();
-        GSON.toJson(payload, this.buffer);
-
-        final String payloadString = this.buffer.toString();
-        final int initialPayloadSize = payloadString.getBytes(StandardCharsets.UTF_8).length;
-
-        final String fittedPayloadString;
-
-        if (initialPayloadSize <= maximumPayloadSize) {
-            fittedPayloadString = payloadString;
-        } else {
-            if (this.alertBody != null) {
-                this.replaceMessageBody(payload, "");
-
-                this.buffer.reset();
-                GSON.toJson(payload, this.buffer);
-
-                final int payloadSizeWithEmptyMessage = this.buffer.toString().getBytes(StandardCharsets.UTF_8).length;
-
-                if (payloadSizeWithEmptyMessage >= maximumPayloadSize) {
-                    throw new IllegalArgumentException("Payload exceeds maximum size even with an empty message body.");
-                }
-
-                final int maximumEscapedMessageBodySize = maximumPayloadSize - payloadSizeWithEmptyMessage -
-                        ABBREVIATION_SUBSTRING.getBytes(StandardCharsets.UTF_8).length;
-
-                final String fittedMessageBody = this.alertBody.substring(0,
-                        ApnsPayloadBuilder.getLengthOfJsonEscapedUtf8StringFittingSize(this.alertBody, maximumEscapedMessageBodySize));
-
-                this.replaceMessageBody(payload, fittedMessageBody + ABBREVIATION_SUBSTRING);
-
-                this.buffer.reset();
-                GSON.toJson(payload, this.buffer);
-
-                fittedPayloadString = this.buffer.toString();
-            } else {
-                throw new IllegalArgumentException(String.format(
-                        "Payload size is %d bytes (with a maximum of %d bytes) and cannot be shortened.",
-                        initialPayloadSize, maximumPayloadSize));
-            }
-        }
-
-        return fittedPayloadString;
+        return GSON.toJson(payload);
     }
 
     /**
@@ -906,76 +830,4 @@ public class ApnsPayloadBuilder {
     public static String buildMdmPayload(final String pushMagicValue) {
         return GSON.toJson(java.util.Collections.singletonMap(MDM_KEY, pushMagicValue));
     }
-
-    private void replaceMessageBody(final Map<String, Object> payload, final String messageBody) {
-        @SuppressWarnings("unchecked")
-        final Map<String, Object> aps = (Map<String, Object>) payload.get(APS_KEY);
-        final Object alert = aps.get(ALERT_KEY);
-
-        if (alert != null) {
-            if (alert instanceof String) {
-                aps.put(ALERT_KEY, messageBody);
-            } else {
-                @SuppressWarnings("unchecked")
-                final Map<String, Object> alertObject = (Map<String, Object>) alert;
-
-                if (alertObject.get(ALERT_BODY_KEY) != null) {
-                    alertObject.put(ALERT_BODY_KEY, messageBody);
-                } else {
-                    throw new IllegalArgumentException("Payload has no message body.");
-                }
-            }
-        } else {
-            throw new IllegalArgumentException("Payload has no message body.");
-        }
-    }
-
-    static int getLengthOfJsonEscapedUtf8StringFittingSize(final String string, final int maximumSize) {
-        int i = 0;
-        int cumulativeSize = 0;
-
-        for (i = 0; i < string.length(); i++) {
-            final char c = string.charAt(i);
-            final int charSize = getSizeOfJsonEscapedUtf8Character(c);
-
-            if (cumulativeSize + charSize > maximumSize) {
-                // The next character would put us over the edge; bail out here.
-                break;
-            }
-
-            cumulativeSize += charSize;
-
-            if (Character.isHighSurrogate(c)) {
-                // Skip the next character
-                i++;
-            }
-        }
-
-        return i;
-    }
-
-    static int getSizeOfJsonEscapedUtf8Character(final char c) {
-        final int charSize;
-
-        if (c == '"' || c == '\\' || c == '\b' || c == '\f' || c == '\n' || c == '\r' || c == '\t') {
-            // Character is backslash-escaped in JSON
-            charSize = 2;
-        } else if (c <= 0x001F || c == '\u2028' || c == '\u2029') {
-            // Character will be represented as an escaped control character
-            charSize = 6;
-        } else {
-            // The character will be represented as an un-escaped UTF8 character
-            if (c <= 0x007F) {
-                charSize = 1;
-            } else if (c <= 0x07FF) {
-                charSize = 2;
-            } else if (Character.isHighSurrogate(c)) {
-                charSize = 4;
-            } else {
-                charSize = 3;
-            }
-        }
-
-        return charSize;
-    }
 }


Test patch:
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java
index 701bf46c..78b19956 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/AbstractClientServerTest.java
@@ -80,7 +80,7 @@ public class AbstractClientServerTest {
     protected Map<ApnsVerificationKey, Set<String>> topicsByVerificationKey;
 
     @BeforeClass
-    public static void setUpBeforeClass() throws Exception {
+    public static void setUpBeforeClass() {
         CLIENT_EVENT_LOOP_GROUP = new NioEventLoopGroup(2);
         SERVER_EVENT_LOOP_GROUP = new NioEventLoopGroup(2);
     }
@@ -168,6 +168,6 @@ public class AbstractClientServerTest {
         final ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
         payloadBuilder.setAlertBody(UUID.randomUUID().toString());
 
-        return payloadBuilder.buildWithDefaultMaximumLength();
+        return payloadBuilder.build();
     }
 }
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/ExampleApp.java b/pushy/src/test/java/com/eatthepath/pushy/apns/ExampleApp.java
index 7fc2d26b..f015138d 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/ExampleApp.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/ExampleApp.java
@@ -80,7 +80,7 @@ public class ExampleApp {
             final ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
             payloadBuilder.setAlertBody("Example!");
 
-            final String payload = payloadBuilder.buildWithDefaultMaximumLength();
+            final String payload = payloadBuilder.build();
             final String token = TokenUtil.sanitizeTokenString("<efc7492 bdbd8209>");
 
             pushNotification = new SimpleApnsPushNotification(token, "com.example.myApp", payload);
diff --git a/pushy/src/test/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderTest.java b/pushy/src/test/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderTest.java
index 7c885950..8ee4b205 100644
--- a/pushy/src/test/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderTest.java
+++ b/pushy/src/test/java/com/eatthepath/pushy/apns/util/ApnsPayloadBuilderTest.java
@@ -32,8 +32,6 @@ import org.junit.Test;
 import org.junit.runner.RunWith;
 
 import java.lang.reflect.Type;
-import java.nio.charset.CharsetEncoder;
-import java.nio.charset.StandardCharsets;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -61,7 +59,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setAlertBody(alertBody);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -72,7 +70,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setPreferStringRepresentationForAlerts(true);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
             assertEquals(alertBody, aps.get("alert"));
         }
 
@@ -81,7 +79,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setShowActionButton(false);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -101,7 +99,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setLocalizedAlertMessage(alertKey);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
 
             assertEquals(alertKey, alert.get("loc-key"));
@@ -116,7 +114,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setLocalizedAlertMessage(alertKey, alertArgs);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
 
             assertEquals(alertKey, alert.get("loc-key"));
@@ -135,7 +133,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setAlertTitle(alertTitle);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -154,7 +152,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setLocalizedAlertTitle(localizedAlertTitleKey);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -171,7 +169,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setLocalizedAlertTitle(localizedAlertTitleKey, alertArgs);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -193,7 +191,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setAlertSubtitle(alertSubtitle);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -213,7 +211,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setLocalizedAlertSubtitle(subtitleKey);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
 
             assertEquals(subtitleKey, alert.get("subtitle-loc-key"));
@@ -228,7 +226,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setLocalizedAlertSubtitle(subtitleKey, subtitleArgs);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
 
             assertEquals(subtitleKey, alert.get("subtitle-loc-key"));
@@ -248,7 +246,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setAlertTitle(alertTitle);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -263,7 +261,7 @@ public class ApnsPayloadBuilderTest {
         final String launchImageFilename = "launch.png";
         this.builder.setLaunchImageFileName(launchImageFilename);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
         @SuppressWarnings("unchecked")
         final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -276,7 +274,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setShowActionButton(true);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             assertNull(aps.get("alert"));
         }
@@ -284,12 +282,12 @@ public class ApnsPayloadBuilderTest {
         this.builder.setShowActionButton(false);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
 
-            assertTrue(alert.keySet().contains("action-loc-key"));
+            assertTrue(alert.containsKey("action-loc-key"));
             assertNull(alert.get("action-loc-key"));
         }
 
@@ -298,7 +296,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setShowActionButton(true);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -309,12 +307,12 @@ public class ApnsPayloadBuilderTest {
         this.builder.setShowActionButton(false);
 
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             @SuppressWarnings("unchecked")
             final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
 
-            assertTrue(alert.keySet().contains("action-loc-key"));
+            assertTrue(alert.containsKey("action-loc-key"));
             assertNull(alert.get("action-loc-key"));
         }
     }
@@ -326,7 +324,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setLocalizedActionButtonKey("action.key");
         this.builder.setActionButtonLabel(actionButtonLabel);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
         @SuppressWarnings("unchecked")
         final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -342,7 +340,7 @@ public class ApnsPayloadBuilderTest {
         this.builder.setActionButtonLabel("Literal action button label");
         this.builder.setLocalizedActionButtonKey(actionButtonKey);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
         @SuppressWarnings("unchecked")
         final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -356,7 +354,7 @@ public class ApnsPayloadBuilderTest {
         final int badgeNumber = 4;
         this.builder.setBadgeNumber(badgeNumber);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
         assertEquals(badgeNumber, ((Number) aps.get("badge")).intValue());
     }
@@ -366,7 +364,7 @@ public class ApnsPayloadBuilderTest {
         final String soundFileName = "dying-giraffe.aiff";
         this.builder.setSound(soundFileName);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
         assertEquals(soundFileName, aps.get("sound"));
     }
@@ -379,7 +377,7 @@ public class ApnsPayloadBuilderTest {
 
         this.builder.setSound(soundFileName, isCriticalAlert, soundVolume);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
         @SuppressWarnings("unchecked")
         final Map<String, Object> soundDictionary = (Map<String, Object>) aps.get("sound");
@@ -409,7 +407,7 @@ public class ApnsPayloadBuilderTest {
         final String categoryName = "INVITE";
         this.builder.setCategoryName(categoryName);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
         assertEquals(categoryName, aps.get("category"));
     }
@@ -417,14 +415,14 @@ public class ApnsPayloadBuilderTest {
     @Test
     public void testSetContentAvailable() {
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
             assertNull(aps.get("content-available"));
         }
 
         {
             this.builder.setContentAvailable(true);
 
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
             assertEquals(1, ((Number) aps.get("content-available")).intValue());
         }
     }
@@ -432,14 +430,14 @@ public class ApnsPayloadBuilderTest {
     @Test
     public void testSetMutableContent() {
         {
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
             assertNull(aps.get("mutable-content"));
         }
 
         {
             this.builder.setMutableContent(true);
 
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
             assertEquals(1, ((Number) aps.get("mutable-content")).intValue());
         }
     }
@@ -450,7 +448,7 @@ public class ApnsPayloadBuilderTest {
 
         this.builder.setThreadId(threadId);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
         assertEquals(threadId, aps.get("thread-id"));
     }
 
@@ -460,7 +458,7 @@ public class ApnsPayloadBuilderTest {
 
         this.builder.setTargetContentId(targetContentId);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
         assertEquals(targetContentId, aps.get("target-content-id"));
     }
 
@@ -470,7 +468,7 @@ public class ApnsPayloadBuilderTest {
 
         this.builder.setSummaryArgument(summaryArgument);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
         @SuppressWarnings("unchecked")
         final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -484,7 +482,7 @@ public class ApnsPayloadBuilderTest {
 
         this.builder.setSummaryArgumentCount(argumentSummaryCount);
 
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
         @SuppressWarnings("unchecked")
         final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
@@ -504,8 +502,7 @@ public class ApnsPayloadBuilderTest {
 
         this.builder.addCustomProperty(customKey, customValue);
 
-        @SuppressWarnings("unchecked")
-        final Map<String, Object> payload = GSON.fromJson(this.builder.buildWithDefaultMaximumLength(), MAP_OF_STRING_TO_OBJECT);
+        final Map<String, Object> payload = GSON.fromJson(this.builder.build(), MAP_OF_STRING_TO_OBJECT);
 
         assertEquals(customValue, payload.get(customKey));
     }
@@ -520,7 +517,7 @@ public class ApnsPayloadBuilderTest {
 
             this.builder.setUrlArguments(arguments);
 
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             assertEquals(arguments, aps.get("url-args"));
         }
@@ -529,7 +526,7 @@ public class ApnsPayloadBuilderTest {
             final List<String> arguments = new ArrayList<>();
             this.builder.setUrlArguments(arguments);
 
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             assertEquals(arguments, aps.get("url-args"));
         }
@@ -537,7 +534,7 @@ public class ApnsPayloadBuilderTest {
         {
             this.builder.setUrlArguments((List<String>) null);
 
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             assertNull(aps.get("url-args"));
         }
@@ -549,7 +546,7 @@ public class ApnsPayloadBuilderTest {
         {
             this.builder.setUrlArguments("first", "second", "third");
 
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             final List<String> argumentsFromPayload = (List<String>) aps.get("url-args");
 
@@ -563,7 +560,7 @@ public class ApnsPayloadBuilderTest {
             final String[] arguments = new String[0];
             this.builder.setUrlArguments(arguments);
 
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             assertTrue(((List<String>) aps.get("url-args")).isEmpty());
         }
@@ -571,82 +568,12 @@ public class ApnsPayloadBuilderTest {
         {
             this.builder.setUrlArguments((String[]) null);
 
-            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithDefaultMaximumLength());
+            final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.build());
 
             assertNull(aps.get("url-args"));
         }
     }
 
-    @Test
-    public void testBuildWithMaximumLength() {
-        final String reallyLongAlertMessage = "All non-glanded recruited mercenaries now engaging in training " +
-                "excercises are herefore and forever ordered to desist. Aforementioned activities have resulted in " +
-                "cost-defective damage to training areas.";
-
-        final int maxLength = 128;
-
-        this.builder.setAlertBody(reallyLongAlertMessage);
-
-        final String payloadString = this.builder.buildWithMaximumLength(maxLength);
-
-        assertTrue(reallyLongAlertMessage.getBytes(StandardCharsets.UTF_8).length > maxLength);
-        assertEquals(payloadString.getBytes(StandardCharsets.UTF_8).length, maxLength);
-    }
-
-    @Test
-    public void testBuildWithMaximumLengthAndAlreadyFittingMessageBody() {
-        final String shortAlertMessage = "This should just fit.";
-
-        this.builder.setAlertBody(shortAlertMessage);
-        final Map<String, Object> aps = this.extractApsObjectFromPayloadString(this.builder.buildWithMaximumLength(Integer.MAX_VALUE));
-
-        @SuppressWarnings("unchecked")
-        final Map<String, Object> alert = (Map<String, Object>) aps.get("alert");
-
-        assertEquals(shortAlertMessage, alert.get("body"));
-    }
-
-    @Test(expected = IllegalArgumentException.class)
-    public void testBuildWithMaximumLengthAndUnshortenablePayload() {
-        final String reallyLongAlertKey =
-                "All non-glanded recruited mercenaries now engaging in training excercises are herefore and forever ordered to desist. Aforementioned activities have resulted in cost-defective damage to training areas.";
-
-        final int maxLength = 128;
-
-        this.builder.setLocalizedAlertMessage(reallyLongAlertKey);
-
-        final String payloadString = this.builder.buildWithMaximumLength(maxLength);
-
-        assertTrue(payloadString.getBytes(StandardCharsets.UTF_8).length <= maxLength);
-    }
-
-    @Test
-    public void testGetSizeOfJsonEscapedUtf8Character() {
-        final CharsetEncoder encoder = StandardCharsets.UTF_8.newEncoder();
-
-        for (int codePoint = Character.MIN_CODE_POINT; codePoint < Character.MAX_CODE_POINT; codePoint++) {
-            if (encoder.canEncode((char) codePoint)) {
-                // We subtract 2 here for the quotes that will appear on either end of a JSON string
-                assertEquals("Escaped/encoded lengths should match for code point " + codePoint,
-                        GSON.toJson(String.valueOf((char) codePoint)).getBytes(StandardCharsets.UTF_8).length - 2,
-                        ApnsPayloadBuilder.getSizeOfJsonEscapedUtf8Character((char) codePoint));
-            }
-        }
-    }
-
-    @Test
-    public void testGetLengthOfJsonEscapedUtf8StringFittingSize() {
-        {
-            final String stringThatFits = "Test!";
-
-            assertEquals(stringThatFits.length(),
-                    ApnsPayloadBuilder.getLengthOfJsonEscapedUtf8StringFittingSize(stringThatFits, Integer.MAX_VALUE));
-        }
-
-        assertEquals(4, ApnsPayloadBuilder.getLengthOfJsonEscapedUtf8StringFittingSize("This string is too long.", 4));
-        assertEquals(2, ApnsPayloadBuilder.getLengthOfJsonEscapedUtf8StringFittingSize("\n\tThis string has escaped characters.", 4));
-    }
-
     @Test
     public void testBuildMdmPayload() {
         assertEquals("{\"mdm\":\"Magic!\"}", ApnsPayloadBuilder.buildMdmPayload("Magic!"));