mlaccetti/JavaPNS

View on GitHub
src/main/java/javapns/notification/Payload.java

Summary

Maintainability
B
6 hrs
Test Coverage
package javapns.notification;

import javapns.notification.exceptions.PayloadMaxSizeExceededException;
import javapns.notification.exceptions.PayloadMaxSizeProbablyExceededException;
import org.json.JSONException;
import org.json.JSONObject;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.util.List;

/**
 * Abstract class representing a payload that can be transmitted to Apple.
 * <p>
 * By default, this class has no payload content at all.  Subclasses are
 * responsible for imposing specific content based on the specifications
 * they are intended to implement (such as the 'aps' dictionnary for APS
 * payloads).
 *
 * @author Sylvain Pedneault
 */
public abstract class Payload {
  static final Logger logger = LoggerFactory.getLogger(Payload.class);

  /* Character encoding specified by Apple documentation */
  private static final String DEFAULT_CHARACTER_ENCODING = "UTF-8";
  private static final String ADDING_CUSTOM_DICTIONARY = "Adding custom Dictionary [";
  private static final String DELIMITER_START = "] = [";
  private static final String DELIMITED_END = "]";
  /* The root Payload */
  private final JSONObject payload;

  /* Character encoding to use for streaming the payload (should be UTF-8) */
  private String characterEncoding = DEFAULT_CHARACTER_ENCODING;

  /* Number of seconds after which this payload should expire */
  @SuppressWarnings("PointlessArithmeticExpression")
  private int expiry = 1 * 24 * 60 * 60;

  private boolean payloadSizeEstimatedWhenAdding = false;

  private int preSendConfiguration = 0;

  /**
   * Construct a Payload object with a blank root JSONObject
   */
  protected Payload() {
    super();
    this.payload = new JSONObject();
  }

  /**
   * Construct a Payload object from a JSON-formatted string
   *
   * @param rawJSON a JSON-formatted string (ex: {"aps":{"alert":"Hello World!"}} )
   * @throws JSONException thrown if a exception occurs while parsing the JSON string
   */
  Payload(final String rawJSON) throws JSONException {
    super();
    this.payload = new JSONObject(rawJSON);
  }

  /**
   * Get the actual JSON object backing this payload.
   *
   * @return a JSONObject
   */
  public JSONObject getPayload() {
    return this.payload;
  }

  /**
   * Add a custom dictionnary with a string value
   *
   * @param name
   * @param value
   * @throws JSONException
   */
  public void addCustomDictionary(final String name, final String value) throws JSONException {
    logger.debug(ADDING_CUSTOM_DICTIONARY + name + DELIMITER_START + value + DELIMITED_END);
    put(name, value, payload, false);
  }

  /**
   * Add a custom dictionnary with a int value
   *
   * @param name
   * @param value
   * @throws JSONException
   */
  public void addCustomDictionary(final String name, final int value) throws JSONException {
    logger.debug(ADDING_CUSTOM_DICTIONARY + name + DELIMITER_START + value + DELIMITED_END);
    put(name, value, payload, false);
  }

  /**
   * Add a custom dictionnary with multiple values
   *
   * @param name
   * @param values
   * @throws JSONException
   */
  public void addCustomDictionary(final String name, final List values) throws JSONException {
    logger.debug(ADDING_CUSTOM_DICTIONARY + name + "] = (list)");
    put(name, values, payload, false);
  }

  /**
   * Add a custom dictionnary with object
   *
   * @param name
   * @param value
   * @throws JSONException
   */
  public void addCustomDictionary(final String name, final Object value) throws JSONException {
    logger.debug(ADDING_CUSTOM_DICTIONARY + name + DELIMITER_START + value + DELIMITED_END);
    put(name, value, payload, false);
  }

  /**
   * Get the string representation
   */
  public String toString() {
    return this.payload.toString();
  }

  void verifyPayloadIsNotEmpty() {
    if (getPreSendConfiguration() != 0) {
      return;
    }
    if (toString().equals("{}")) {
      throw new IllegalArgumentException("Payload cannot be empty");
    }
  }

  /**
   * Get this payload as a byte array using the preconfigured character encoding.
   *
   * @return byte[] bytes ready to be streamed directly to Apple servers
   */
  public byte[] getPayloadAsBytes() throws Exception {
    final byte[] payloadBytesUnchecked = getPayloadAsBytesUnchecked();
    validateMaximumPayloadSize(payloadBytesUnchecked.length);
    return payloadBytesUnchecked;
  }

  /**
   * Get this payload as a byte array using the preconfigured character encoding.
   * This method does NOT check if the payload exceeds the maximum payload length.
   *
   * @return byte[] bytes ready to be streamed directly to Apple servers (but that might exceed the maximum size limit)
   */
  private byte[] getPayloadAsBytesUnchecked() throws Exception {
    byte[] bytes;
    try {
      bytes = toString().getBytes(characterEncoding);
    } catch (final Exception ex) {
      bytes = toString().getBytes();
    }
    return bytes;
  }

  /**
   * Get the number of bytes that the payload will occupy when streamed.
   *
   * @return a number of bytes
   * @throws Exception
   */
  public int getPayloadSize() throws Exception {
    return getPayloadAsBytesUnchecked().length;
  }

  /**
   * Check if the payload exceeds the maximum size allowed.
   * The maximum size allowed is returned by the getMaximumPayloadSize() method.
   *
   * @return true if the payload exceeds the maximum size allowed, false otherwise
   */
  private boolean isPayloadTooLong() {
    try {
      final byte[] bytes = getPayloadAsBytesUnchecked();
      if (bytes.length > getMaximumPayloadSize()) {
        return true;
      }
    } catch (final Exception e) {
      // empty
    }
    return false;
  }

  /**
   * Estimate the size that this payload will take after adding a given property.
   * For performance reasons, this estimate is not as reliable as actually adding
   * the property and checking the payload size afterwards.
   * <p>
   * Currently works well with strings and numbers.
   *
   * @param propertyName  the name of the property to use for calculating the estimation
   * @param propertyValue the value of the property to use for calculating the estimation
   * @return an estimated payload size if the property were to be added to the payload
   */
  private int estimatePayloadSizeAfterAdding(final String propertyName, final Object propertyValue) {
    try {
      int estimatedSize = getPayloadAsBytesUnchecked().length;
      if (propertyName != null && propertyValue != null) {
        estimatedSize += 6; // ,"":""
        estimatedSize += propertyName.getBytes(getCharacterEncoding()).length;
        int estimatedValueSize = 0;

        if (propertyValue instanceof String || propertyValue instanceof Number) {
          estimatedValueSize = propertyValue.toString().getBytes(getCharacterEncoding()).length;
        }

        estimatedSize += estimatedValueSize;
      }
      return estimatedSize;
    } catch (final Exception e) {
      try {
        return getPayloadSize();
      } catch (final Exception e1) {
        return 0;
      }
    }
  }

  /**
   * Validate if the estimated payload size after adding a given property will be allowed.
   * For performance reasons, this estimate is not as reliable as actually adding
   * the property and checking the payload size afterwards.
   *
   * @param propertyName  the name of the property to use for calculating the estimation
   * @param propertyValue the value of the property to use for calculating the estimation
   * @return true if the payload size is not expected to exceed the maximum allowed, false if it might be too big
   */
  public boolean isEstimatedPayloadSizeAllowedAfterAdding(final String propertyName, final Object propertyValue) {
    final int maximumPayloadSize = getMaximumPayloadSize();
    final int estimatedPayloadSize = estimatePayloadSizeAfterAdding(propertyName, propertyValue);
    return estimatedPayloadSize <= maximumPayloadSize;
  }

  /**
   * Validate that the payload does not exceed the maximum size allowed.
   * If the limit is exceeded, a PayloadMaxSizeExceededException is thrown.
   *
   * @param currentPayloadSize the total size of the payload in bytes
   * @throws PayloadMaxSizeExceededException if the payload exceeds the maximum size allowed
   */
  private void validateMaximumPayloadSize(final int currentPayloadSize) throws PayloadMaxSizeExceededException {
    final int maximumPayloadSize = getMaximumPayloadSize();
    if (currentPayloadSize > maximumPayloadSize) {
      throw new PayloadMaxSizeExceededException(maximumPayloadSize, currentPayloadSize);
    }
  }

  /**
   * Puts a property in a JSONObject, while possibly checking for estimated payload size violation.
   *
   * @param propertyName  the name of the property to use for calculating the estimation
   * @param propertyValue the value of the property to use for calculating the estimation
   * @param object        the JSONObject to put the property in
   * @param opt           true to use putOpt, false to use put
   * @throws JSONException
   */
  void put(final String propertyName, final Object propertyValue, final JSONObject object, final boolean opt) throws JSONException {
    try {
      if (isPayloadSizeEstimatedWhenAdding()) {
        final int maximumPayloadSize = getMaximumPayloadSize();
        final int estimatedPayloadSize = estimatePayloadSizeAfterAdding(propertyName, propertyValue);
        final boolean estimatedToExceed = estimatedPayloadSize > maximumPayloadSize;
        if (estimatedToExceed) {
          throw new PayloadMaxSizeProbablyExceededException(maximumPayloadSize, estimatedPayloadSize);
        }
      }
    } catch (final PayloadMaxSizeProbablyExceededException e) {
      throw e;
    } catch (final Exception e) {
      // empty
    }
    if (opt) {
      object.putOpt(propertyName, propertyValue);
    } else {
      object.put(propertyName, propertyValue);
    }
  }

  Object remove(final String propertyName, final JSONObject object) {
    return object.remove(propertyName);
  }

  /**
   * Indicates if payload size is estimated and controlled when adding properties (default is false).
   *
   * @return true to throw an exception if the estimated size is too big when adding a property, false otherwise
   */
  public boolean isPayloadSizeEstimatedWhenAdding() {
    return payloadSizeEstimatedWhenAdding;
  }

  /**
   * Indicate if payload size should be estimated and controlled when adding properties (default is false).
   *
   * @param checked true to throw an exception if the estimated size is too big when adding a property, false otherwise
   */
  public void setPayloadSizeEstimatedWhenAdding(final boolean checked) {
    this.payloadSizeEstimatedWhenAdding = checked;
  }

  /**
   * Return the maximum payload size in bytes.
   * By default, this method returns Integer.MAX_VALUE.
   * Subclasses should override this method to provide their own limit.
   *
   * @return the maximum payload size in bytes
   */
  int getMaximumPayloadSize() {
    return Integer.MAX_VALUE;
  }

  /**
   * Returns the character encoding that will be used by getPayloadAsBytes().
   * Default is UTF-8, as per Apple documentation.
   *
   * @return a character encoding
   */
  public String getCharacterEncoding() {
    return characterEncoding;
  }

  /**
   * Changes the character encoding for streaming the payload.
   * Character encoding is preset to UTF-8, as Apple documentation specifies.
   * Therefore, unless you are working on a special project, you should leave it as is.
   *
   * @param characterEncoding a valid character encoding that String.getBytes(encoding) will accept
   */
  public void setCharacterEncoding(final String characterEncoding) {
    this.characterEncoding = characterEncoding;
  }

  /**
   * Return the number of seconds after which this payload should expire.
   *
   * @return a number of seconds
   */
  public int getExpiry() {
    return expiry;
  }

  /**
   * Set the number of seconds after which this payload should expire.
   * Default is one (1) day.
   *
   * @param seconds
   */
  public void setExpiry(final int seconds) {
    this.expiry = seconds;
  }

  /**
   * Enables a special simulation mode which causes the library to behave
   * as usual *except* that at the precise point where the payload would
   * actually be streamed out to Apple, it is not.
   *
   * @return the same payload
   */
  public Payload asSimulationOnly() {
    setExpiry(919191);
    return this;
  }

  int getPreSendConfiguration() {
    return preSendConfiguration;
  }

  void setPreSendConfiguration(final int preSendConfiguration) {
    this.preSendConfiguration = preSendConfiguration;
  }

}