/* * Copyright 2009, Mahmood Ali. * All rights reserved. * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions are * met: * * * Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * * Redistributions in binary form must reproduce the above * copyright notice, this list of conditions and the following disclaimer * in the documentation and/or other materials provided with the * distribution. * * Neither the name of Mahmood Ali. nor the names of its * contributors may be used to endorse or promote products derived from * this software without specific prior written permission. * * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ package com.notnoop.apns; import java.util.Arrays; import java.util.Collection; import java.util.HashMap; import java.util.Map; import com.fasterxml.jackson.databind.ObjectMapper; import com.notnoop.apns.internal.Utilities; /** * Represents a builder for constructing Payload requests, as * specified by Apple Push Notification Programming Guide. */ public final class PayloadBuilder { private static final ObjectMapper mapper = new ObjectMapper(); private final Map<String, Object> root; private final Map<String, Object> aps; private final Map<String, Object> customAlert; /** * Constructs a new instance of {@code PayloadBuilder} */ PayloadBuilder() { root = new HashMap<String, Object>(); aps = new HashMap<String, Object>(); customAlert = new HashMap<String, Object>(); } /** * Sets the alert body text, the text the appears to the user, * to the passed value * * @param alert the text to appear to the user * @return this */ public PayloadBuilder alertBody(final String alert) { customAlert.put("body", alert); return this; } /** * Sets the alert title text, the text the appears to the user, * to the passed value * * @param title the text to appear to the user * @return this */ public PayloadBuilder alertTitle(final String title) { customAlert.put("title", title); return this; } /** * Sets the alert action text * * @param action The label of the action button * @return this */ public PayloadBuilder alertAction(final String action) { customAlert.put("action", action); return this; } /** * Sets the "url-args" key that are paired with the placeholders * inside the urlFormatString value of your website.json file. * The order of the placeholders in the URL format string determines * the order of the values supplied by the url-args array. * * @param urlArgs the values to be paired with the placeholders inside * the urlFormatString value of your website.json file. * @return this */ public PayloadBuilder urlArgs(final String... urlArgs){ aps.put("url-args", urlArgs); return this; } /** * Sets the alert sound to be played. * * Passing {@code null} disables the notification sound. * * @param sound the file name or song name to be played * when receiving the notification * @return this */ public PayloadBuilder sound(final String sound) { if (sound != null) { aps.put("sound", sound); } else { aps.remove("sound"); } return this; } /** * Sets the category of the notification for iOS8 notification * actions. See 13 minutes into "What's new in iOS Notifications" * * Passing {@code null} removes the category. * * @param category the name of the category supplied to the app * when receiving the notification * @return this */ public PayloadBuilder category(final String category) { if (category != null) { aps.put("category", category); } else { aps.remove("category"); } return this; } /** * Sets the notification badge to be displayed next to the * application icon. * * The passed value is the value that should be displayed * (it will be added to the previous badge number), and * a badge of 0 clears the badge indicator. * * @param badge the badge number to be displayed * @return this */ public PayloadBuilder badge(final int badge) { aps.put("badge", badge); return this; } /** * Requests clearing of the badge number next to the application * icon. * * This is an alias to {@code badge(0)}. * * @return this */ public PayloadBuilder clearBadge() { return badge(0); } /** * Sets the value of action button (the right button to be * displayed). The default value is "View". * * The value can be either the simple String to be displayed or * a localizable key, and the iPhone will show the appropriate * localized message. * * A {@code null} actionKey indicates no additional button * is displayed, just the Cancel button. * * @param actionKey the title of the additional button * @return this */ public PayloadBuilder actionKey(final String actionKey) { customAlert.put("action-loc-key", actionKey); return this; } /** * Set the notification view to display an action button. * * This is an alias to {@code actionKey(null)} * * @return this */ public PayloadBuilder noActionButton() { return actionKey(null); } /** * Sets the notification type to be a 'newstand' notification. * * A Newstand Notification targets the Newstands app so that the app * updates the subscription info and content. * * @return this */ public PayloadBuilder forNewsstand() { aps.put("content-available", 1); return this; } /** * With iOS7 it is possible to have the application wake up before the user opens the app. * * The same key-word can also be used to send 'silent' notifications. With these 'silent' notification * a different app delegate is being invoked, allowing the app to perform background tasks. * * @return this */ public PayloadBuilder instantDeliveryOrSilentNotification() { aps.put("content-available", 1); return this; } /** * Set the notification localized key for the alert body * message. * * @param key the localizable message body key * @return this */ public PayloadBuilder localizedKey(final String key) { customAlert.put("loc-key", key); return this; } /** * Sets the arguments for the alert message localizable message. * * The iPhone doesn't localize the arguments. * * @param arguments the arguments to the localized alert message * @return this */ public PayloadBuilder localizedArguments(final Collection<String> arguments) { customAlert.put("loc-args", arguments); return this; } /** * Sets the arguments for the alert message localizable message. * * The iPhone doesn't localize the arguments. * * @param arguments the arguments to the localized alert message * @return this */ public PayloadBuilder localizedArguments(final String... arguments) { return localizedArguments(Arrays.asList(arguments)); } /** * Sets the launch image file for the push notification * * @param launchImage the filename of the image file in the * application bundle. * @return this */ public PayloadBuilder launchImage(final String launchImage) { customAlert.put("launch-image", launchImage); return this; } /** * Sets any application-specific custom fields. The values * are presented to the application and the iPhone doesn't * display them automatically. * * This can be used to pass specific values (urls, ids, etc) to * the application in addition to the notification message * itself. * * @param key the custom field name * @param value the custom field value * @return this */ public PayloadBuilder customField(final String key, final Object value) { root.put(key, value); return this; } public PayloadBuilder mdm(final String s) { return customField("mdm", s); } /** * Set any application-specific custom fields. These values * are presented to the application and the iPhone doesn't * display them automatically. * * This method *adds* the custom fields in the map to the * payload, and subsequent calls add but doesn't reset the * custom fields. * * @param values the custom map * @return this */ public PayloadBuilder customFields(final Map<String, ?> values) { root.putAll(values); return this; } /** * Returns the length of payload bytes once marshaled to bytes * * @return the length of the payload */ public int length() { return copy().buildBytes().length; } /** * Returns true if the payload built so far is larger than * the size permitted by Apple (which is 2048 bytes). * * @return true if the result payload is too long */ public boolean isTooLong() { return length() > Utilities.MAX_PAYLOAD_LENGTH; } /** * Shrinks the alert message body so that the resulting payload * message fits within the passed expected payload length. * * This method performs best-effort approach, and its behavior * is unspecified when handling alerts where the payload * without body is already longer than the permitted size, or * if the break occurs within word. * * @param payloadLength the expected max size of the payload * @return this */ public PayloadBuilder resizeAlertBody(final int payloadLength) { return resizeAlertBody(payloadLength, ""); } /** * Shrinks the alert message body so that the resulting payload * message fits within the passed expected payload length. * * This method performs best-effort approach, and its behavior * is unspecified when handling alerts where the payload * without body is already longer than the permitted size, or * if the break occurs within word. * * @param payloadLength the expected max size of the payload * @param postfix for the truncated body, e.g. "..." * @return this */ public PayloadBuilder resizeAlertBody(final int payloadLength, final String postfix) { int currLength = length(); if (currLength <= payloadLength) { return this; } // now we are sure that truncation is required String body = (String)customAlert.get("body"); final int acceptableSize = Utilities.toUTF8Bytes(body).length - (currLength - payloadLength + Utilities.toUTF8Bytes(postfix).length); body = Utilities.truncateWhenUTF8(body, acceptableSize) + postfix; // set it back customAlert.put("body", body); // calculate the length again currLength = length(); if(currLength > payloadLength) { // string is still too long, just remove the body as the body is // anyway not the cause OR the postfix might be too long customAlert.remove("body"); } return this; } /** * Shrinks the alert message body so that the resulting payload * message fits within require Apple specification (2048 bytes). * * This method performs best-effort approach, and its behavior * is unspecified when handling alerts where the payload * without body is already longer than the permitted size, or * if the break occurs within word. * * @return this */ public PayloadBuilder shrinkBody() { return shrinkBody(""); } /** * Shrinks the alert message body so that the resulting payload * message fits within require Apple specification (2048 bytes). * * This method performs best-effort approach, and its behavior * is unspecified when handling alerts where the payload * without body is already longer than the permitted size, or * if the break occurs within word. * * @param postfix for the truncated body, e.g. "..." * * @return this */ public PayloadBuilder shrinkBody(final String postfix) { return resizeAlertBody(Utilities.MAX_PAYLOAD_LENGTH, postfix); } /** * Returns the JSON String representation of the payload * according to Apple APNS specification * * @return the String representation as expected by Apple */ public String build() { if (!root.containsKey("mdm")) { insertCustomAlert(); root.put("aps", aps); } try { return mapper.writeValueAsString(root); } catch (final Exception e) { throw new RuntimeException(e); } } private void insertCustomAlert() { switch (customAlert.size()) { case 0: aps.remove("alert"); break; case 1: if (customAlert.containsKey("body")) { aps.put("alert", customAlert.get("body")); break; } // else follow through //$FALL-THROUGH$ default: aps.put("alert", customAlert); } } /** * Returns the bytes representation of the payload according to * Apple APNS specification * * @return the bytes as expected by Apple */ public byte[] buildBytes() { return Utilities.toUTF8Bytes(build()); } @Override public String toString() { return build(); } private PayloadBuilder(final Map<String, Object> root, final Map<String, Object> aps, final Map<String, Object> customAlert) { this.root = new HashMap<String, Object>(root); this.aps = new HashMap<String, Object>(aps); this.customAlert = new HashMap<String, Object>(customAlert); } /** * Returns a copy of this builder * * @return a copy of this builder */ public PayloadBuilder copy() { return new PayloadBuilder(root, aps, customAlert); } /** * @return a new instance of Payload Builder */ public static PayloadBuilder newPayload() { return new PayloadBuilder(); } }