public class GcmResponse {

    @SerializedName("multicast_id")
    public String messageId;

    @SerializedName("success")
    public int successCount;

    @SerializedName("failure")
    public int failCount;

    @SerializedName("canonical_ids")
    public int canonicalIdCount;

    @SerializedName("results")
    public List<ResultData> results;

    public class ResultData {
        @SerializedName("message_id")
        public String messageId;

        @SerializedName("registration_id")
        public String registrationId;

        @SerializedName("error")
        public String error;
    }
} public enum FailureType {

    /**
     * [GCM] [APNS]
     * The sender account used to send a message couldn't be authenticated. Possible causes are:
     * <p>
     * - Authorization header missing or with invalid syntax in HTTP request.
     * - Invalid project number sent as key.
     * - Key valid but with GCM service disabled.
     * - Request originated from a server not whitelisted in the Server Key IPs.
     * - Check that the token you're sending inside the Authentication header is the correct API key
     * associated with your project. See Checking the validity of an API Key for details.
     */
    @EnumValue("PLATFORM_AUTH_INVALID")
    PLATFORM_AUTH_INVALID(true),

    /**
     * [GCM] [APNS]
     * A registration token is tied to a certain group of senders. When a client app registers for GCM,
     * it must specify which senders are allowed to send messages. You should use one of those sender
     * IDs when sending messages to the client app. If you switch to a different sender,
     * the existing registration tokens won't work.
     */
    @EnumValue("PLATFORM_AUTH_MISMATCHED")
    PLATFORM_AUTH_MISMATCHED(true),

    /**
     * [GCM] [APNS]
     *
     * The rate of messages to a particular device is too high. Reduce the number of
     * messages sent to this device and do not immediately retry sending to this device.
     */
    @EnumValue("PLATFORM_LIMIT_EXCEEDED")
    PLATFORM_LIMIT_EXCEEDED(false),

    /**
     * [GCM] [APNS]
     * <p>
     * The server encountered an error while trying to process the request. You could retry the same
     * request following the requirements listed in "Timeout" (see row above). If the error persists,
     * please report the problem in the android-gcm group.
     */
    @EnumValue("TEMPORARILY_UNAVAILABLE")
    TEMPORARILY_UNAVAILABLE(false),

    /**
     * [GCM] [APNS]
     * <p>
     * Check that the total size of the payload data included in a message does not exceed GCM limits:
     * 4096 bytes for most messages, or 2048 bytes in the case of messages to topics or notification
     * messages on iOS. This includes both the keys and the values.
     */
    @EnumValue("MESSAGE_TOO_LARGE")
    MESSAGE_TOO_LARGE(true),

    /**
     * [GCM] [APNS]
     *
     * Check that the request contains a registration token (in the registration_id
     * in a plain text message, or in the to or registration_ids field in JSON).
     */
    @EnumValue("MESSAGE_REGISTRATIONS_MISSING")
    MESSAGE_REGISTRATIONS_MISSING(true),

    /**
     * [GCM] [APNS]
     *
     * Make sure the message was addressed to a registration token whose package
     * name matches the value passed in the request.
     */

    @EnumValue("MESSAGE_PACKAGE_INVALID")
    MESSAGE_PACKAGE_INVALID(true),

    /**
     * [GCM] [APNS]
     *
     * Check that the payload data does not contain a key (such as from, or gcm, or any value prefixed
     * by google) that is used internally by GCM. Note that some words (such as collapse_key) are also
     * used by GCM but are allowed in the payload, in which case the payload value will be
     * overridden by the GCM value.
     */
    @EnumValue("MESSAGE_PAYLOAD_INVALID")
    MESSAGE_PAYLOAD_INVALID(true),

    /**
     * [GCM] [APNS]
     */
    @EnumValue("MESSAGE_TTL_INVALID")
    MESSAGE_TTL_INVALID(true),

    /**
     * [GCM] [APNS]
     *
     * You are sending too many messages. TaskQueue will exponentially retry until message max
     * retries has been reached.
     */
    @EnumValue("RECIPIENT_RATE_EXCEEDED")
    RECIPIENT_RATE_EXCEEDED(false),

    /**
     * [GCM] [ANS]
     * <p>
     * An existing registration token may cease to be valid in a number of scenarios, including:
     * - If the client app unregisters with GCM.
     * - If the client app is automatically unregistered, which can happen if the user uninstalls
     * the application. For example, on iOS, if the APNS Feedback Service reported the APNS
     * token as invalid.
     * - If the registration token expires (for example, Google might decide to refresh registration
     * tokens, or the APNS token has expired for iOS devices).
     * - If the client app is updated but the new version is not configured to receive messages.
     * <p>
     * For all these cases, remove this registration token from the app server and stop using it
     * to send messages.
     */
    @EnumValue("RECIPIENT_NOT_REGISTERED")
    RECIPIENT_NOT_REGISTERED(true),

    /**
     * [GCM] [APNS]
     * <p>
     * Check the format of the registration token you pass to the server. Make sure it
     * matches the registration token the client app receives from registering with GCM.
     * Do not truncate or add additional characters.
     */
    @EnumValue("RECIPIENT_REGISTRATION_INVALID")
    RECIPIENT_REGISTRATION_INVALID(true),

    /**
     * [GCM [APNS]
     *
     * Unknown Error. Check the {@link PlatformFailure}
     * error message.
     */
    @EnumValue("ERROR_UNKNOWN")
    ERROR_UNKNOWN(true);

    /**
     * true if the error non recoverable and no messages were delivered.
     */
    public boolean isFatal;
    FailureType(boolean critical) {
        isFatal = critical;
    }
} public enum PlatformType {
    @EnumValue("GCM")
    SERVICE_GCM("GCM", "https://fcm.googleapis.com/fcm/send"),

    @EnumValue( "APNS")
    SERVICE_APNS("APNS", "https://gateway.sandbox.push.apple.com");

    public String name;
    public String url;
    PlatformType(String name, String url) {
        this.name = name;
        this.url = url;
    }
} public enum RecipientState {
    @EnumValue("IDLE")
    STATE_IDLE,

    @EnumValue("PROCESSING")
    STATE_PROCESSING,

    @EnumValue("FAILED")
    STATE_FAILED,

    @EnumValue("WAITING_RETRY")
    STATE_WAITING_RETRY,

    @EnumValue("COMPLETE")
    STATE_COMPLETE;

    RecipientState() {
    }
} public class PlatformEndpointException extends CompletionException {
    public int statusCode = 0;
    public String errorMessage = "";

    public PlatformEndpointException(int statusCode, String errorMessage) {
        super(String.format("GCM Endpoint returned a failing %d status code.", statusCode));
        this.statusCode = statusCode;
        this.errorMessage = errorMessage;
    }

    private PlatformEndpointException() {
    }
} public class MessageHelper {

    /**
     * Verifies a message and throws an exception on fatal errors with the message.
     *
     * @param message The message to validate.
     * @throws MessageValidationException fired when there are irrecoverable problems with the message.
     */
    public static void verifyMessage(Message message) throws MessageValidationException {
        if (message == null) {
            throw new MessageValidationException("message is null.");
        }

        if (message.getId() != null) {
            throw new MessageValidationException("message.id should not be set. A message and all children behave atomically.");
        }

        if (message.getCredentials() == null) {
            throw new MessageValidationException("message is missing a credentials model.");
        }

        if (message.getCredentials().getMessage() != null) {
            throw new MessageValidationException("credentials.message should not be set.");
        }

        if (message.getCredentials().getId() != null) {
            throw new MessageValidationException("credentials.id should not be set. A message and all children behave atomically.");
        }

        if (message.getCredentials().getPlatformType() == null) {
            throw new MessageValidationException("credentials is missing a PlatformType.");
        }

        if (message.getRecipients() == null || message.getRecipients().isEmpty()) {
            throw new MessageValidationException("message has no recipients. did you forget to add tokens using the builder?");
        }

        for (Recipient recipient : message.getRecipients()) {
            if (recipient.getId() != null) {
                throw new MessageValidationException("recipient.id should not be set. A message and all children behave atomically.");
            }

            if (recipient.getMessage() != null) {
                throw new MessageValidationException("recipient.message should not be set.");
            }

            if (StringUtil.isNullOrEmpty(recipient.getToken())) {
                throw new MessageValidationException(String.format("recipient %d has no device token.", recipient.getId()));
            }

            if (recipient.getPlatformFailure() != null) {
                throw new MessageValidationException("message must not have a PlatformFailure.");
            }
        }

        if (StringUtil.isNullOrEmpty(message.getCredentials().getAuthKey()) && StringUtil.isNullOrEmpty(message.getCredentials().getCertBody())) {
            throw new MessageValidationException("A Message's Credentials has no AuthorisationKey or CertificateBody.");
        }
    }

    /**
     * Returns true if a recipient is ready to send a message. (not in backoff and is in
     * a non-complete send state).
     *
     * @param recipient recipient to check.
     * @return true if the recipient is ready to be included in a platform message.
     */
    public static boolean isRecipientPending(@Nonnull Recipient recipient) {
        return recipient.getState() == null ||
                (recipient.getState().equals(RecipientState.STATE_WAITING_RETRY) ||
                        recipient.getState().equals(RecipientState.STATE_PROCESSING) ||
                        recipient.getState().equals(RecipientState.STATE_IDLE));
    }

    /**
     * Returns true if the recipient is within the cool-down backoff period, and is
     * not yet ready to be included in a message dispatch.
     *
     * @param recipient recipient to check.
     * @return true if the recipient is still within the backoff period. False if the
     * recipient is ready to be included in message.
     */
    public static boolean isRecipientCoolingOff(@Nonnull Recipient recipient) {
        Date currentTime = new Date();

        return recipient.getState() != null &&
                recipient.getState().equals(RecipientState.STATE_WAITING_RETRY) &&
                recipient.getNextAttempt() != null &&
                recipient.getNextAttempt().getTime() >= currentTime.getTime();
    }

    /**
     * Marks a recipient as cooling-off. The next cooling-off time will be set on a factor *2 compared
     * to the previous cooling off minutes wait.
     *
     * @param recipient recipient to mark as cooling off..
     */
    public static void setRecipientRetry(PlatformType platformType, @Nonnull Recipient recipient, int retryLimit) {
        if (recipient.getSendAttemptCount() >= retryLimit) {
            recipient.setState(RecipientState.STATE_FAILED);
            recipient.setNextAttempt(null);

        } else if (!recipient.getState().equals(RecipientState.STATE_WAITING_RETRY)) {
            int newRetryCount = recipient.getSendAttemptCount() + 1;
            Calendar nextSendDate = Calendar.getInstance();
            nextSendDate.add(Calendar.MINUTE, 2 * newRetryCount);

            recipient.setSendAttemptCount(newRetryCount);
            recipient.setState(RecipientState.STATE_WAITING_RETRY);
            recipient.setNextAttempt(nextSendDate.getTime());
        }
    }

    /**
     * Check to see if the message has completed (every recipient has failed or succeeded).
     */
    public static boolean hasMessageCompleted(@Nonnull Message message) {
        if (message.getRecipients() != null) {
            for (Recipient recipient : message.getRecipients()) {
                if (recipient.getState() != null &&
                        (recipient.getState().equals(RecipientState.STATE_PROCESSING) ||
                        recipient.getState().equals(RecipientState.STATE_WAITING_RETRY) ||
                        recipient.getState().equals(RecipientState.STATE_IDLE))) {
                    return false;
                }
            }
        }

        return true;
    }

    /**
     * Fail all recipients in a message with a failure.
     *
     * @param message the {@link Message} to fail.
     */
    public static void failMessage(Message message, PlatformFailure failure) {
        if (message != null && message.getId() != null) {

            if (message.getRecipients() != null) {
                for (Recipient recipient : message.getRecipients()) {
                    recipient.setState(RecipientState.STATE_FAILED);
                    if (recipient.getPlatformFailure() == null && failure != null) {
                        recipient.setFailure(failure);
                    }
                }
            }
        }
    }


    /**
     * Returns true if any Message Recipients have states that have not failed or coompleted).
     *
     * @param message The message to check if ready to dispatch.
     * @return true if the message can be processed. false if it is invalid or has Completed..
     */
    public static boolean isMessageReady(@Nonnull Message message) throws MessageValidationException {
        int readyRecipients = 0;
        if (message.getRecipients() != null && !message.getRecipients().isEmpty()) {
            for (Recipient recipient : message.getRecipients()) {

                if (recipient.getToken() == null || recipient.getToken().isEmpty()) {
                    throw new MessageValidationException(String.format("No Recipient token for %d.", recipient.getId()));
                }

                if (isRecipientPending(recipient) && recipient.getSendAttemptCount() <= message.getMaximumRetries()) {
                    readyRecipients += 1;
                }
            }
        }
        return readyRecipients > 0;
    }

} public class PlatformHelper {

    /**
     * Get a GCM Response failure type for a given GCM error.
     *
     * @param error GCM response error.
     * @return failureType error for Message or Recipient.
     */
    public static FailureType getGcmFailureType(@Nonnull String error) {
        FailureType failureType;

        switch (error) {
            case "MissingRegistration":
                failureType = FailureType.MESSAGE_REGISTRATIONS_MISSING;
                break;
            case "InvalidRegistration":
                failureType = FailureType.RECIPIENT_REGISTRATION_INVALID;
                break;
            case "NotRegistered":
                failureType = FailureType.RECIPIENT_NOT_REGISTERED;
                break;
            case "DeviceMessageRate Exceeded":
                failureType = FailureType.RECIPIENT_RATE_EXCEEDED;
                break;
            case "InvalidPackageName":
                failureType = FailureType.MESSAGE_PACKAGE_INVALID;
                break;
            case "MismatchSenderId":
                failureType = FailureType.PLATFORM_AUTH_MISMATCHED;
                break;
            case "MessageTooBig":
                failureType = FailureType.MESSAGE_TOO_LARGE;
                break;
            case "InvalidDataKey":
                failureType = FailureType.MESSAGE_PAYLOAD_INVALID;
                break;
            case "InvalidTtl":
                failureType = FailureType.MESSAGE_TTL_INVALID;
                break;
            case "DeviceMessageRate":
                failureType = FailureType.PLATFORM_LIMIT_EXCEEDED;
                break;
            case "Unavailable":
                failureType = FailureType.TEMPORARILY_UNAVAILABLE;
                break;
            default:
                failureType = FailureType.ERROR_UNKNOWN;
                break;
        }

        if (error.contains("401") || error.contains("Authentication")) {
            return FailureType.PLATFORM_AUTH_INVALID;
        }
        return failureType;
    }

    /**
     * Get a human readable error for a {@link FailureType}.
     * @param failureType Failure to get message string for.
     * @return Error string.
     */
    public static String getGcmFailureName(@Nonnull FailureType failureType) {
        if (failureType == FailureType.MESSAGE_REGISTRATIONS_MISSING) {
            return "MissingRegistration";

        } else if (failureType == FailureType.RECIPIENT_REGISTRATION_INVALID) {
            return "InvalidRegistration";

        } else if (failureType == FailureType.RECIPIENT_NOT_REGISTERED) {
            return "NotRegistered";

        } else if (failureType == FailureType.RECIPIENT_RATE_EXCEEDED) {
            return "DeviceMessageRate Exceeded";

        } else if (failureType == FailureType.MESSAGE_PACKAGE_INVALID) {
            return "InvalidPackageName";

        } else if (failureType == FailureType.PLATFORM_AUTH_MISMATCHED) {
            return "MismatchSenderId";

        } else if (failureType == FailureType.MESSAGE_TOO_LARGE) {
            return "MessageTooBig";

        } else if (failureType == FailureType.MESSAGE_PAYLOAD_INVALID) {
            return "InvalidDataKey";

        } else if (failureType == FailureType.MESSAGE_TTL_INVALID) {
            return "InvalidTtl";

        } else if (failureType == FailureType.PLATFORM_LIMIT_EXCEEDED) {
            return "DeviceMessageRate";

        } else if (failureType == FailureType.TEMPORARILY_UNAVAILABLE) {
            return "Unavailable";

        } else if (failureType == FailureType.PLATFORM_AUTH_INVALID) {
            return "401 Authentication Error";

        } else {
            return "Unknown Error";
        }
    }
} public interface PlatformResponse {

    /**
     * A callback for an unrecoverable Platform message dispatch failure.
     *
     * @param message The original message send to the push-services
     * @param failure The Message Failure.
     */
    void messageFailure(@Nonnull Message message, @Nonnull PlatformFailure failure);

    /**
     * A raw, unsorted callback for results returned for a message send from the push-services.
     *
     * @param message             The original message sent to the push-services
     * @param completedRecipients The list of completed Recipients
     * @param failedRecipients    The map of failed Recipients with the Failure.
     * @param recipientsToRetry   The list of recipients to exponentially retry.
     **/
    void messageSuccess(@Nonnull Message message,
                        @Nonnull List<Recipient> completedRecipients,
                        @Nonnull List<Recipient> failedRecipients,
                        @Nonnull List<UpdatedRecipient> recipientsToUpdate,
                        @Nonnull List<Recipient> recipientsToRetry);
} public class UpdatedRecipient {
    private Recipient mStaleRecipient;
    private Recipient mUpdatedRecipient;

    // Private access for immutability.
    private UpdatedRecipient() {
    }

    public UpdatedRecipient(@Nonnull Recipient staleRecipient, @Nonnull Recipient updatedRecipient) {
        mStaleRecipient = staleRecipient;
        mUpdatedRecipient = updatedRecipient;
    }

    @Nonnull
    public Recipient getStaleRecipient() {
        return mStaleRecipient;
    }

    @Nonnull
    public Recipient getUpdatedRecipient() {
        return mUpdatedRecipient;
    }
} @Entity
@Table(name = "messages", schema = "pushservices")
public class Message extends Model {
    public static Finder<Long, Message> find = new Finder<>(Message.class);
    private static final int TTL_SECONDS_DEFAULT = 60 * 60 * 24 * 7;

    @Id
    @JsonIgnore
    @Column(name = "id")
    @SequenceGenerator(name = "gen", sequenceName = "pushservices.message_id_seq", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gen")
    private Long id;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private List<Recipient> recipients;

    @OneToOne(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    @JoinColumn(name = "credentials_id")
    private Credentials credentials;

    @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER)
    private List<PayloadElement> payloadData;

    @Column(name = "collapse_key")
    private String collapseKey;

    @Column(name = "priority")
    @Enumerated(EnumType.STRING)
    private MessagePriority messagePriority = MessagePriority.PRIORITY_NORMAL;

    @Column(name = "ttl_seconds")
    private int ttlSeconds = TTL_SECONDS_DEFAULT;

    @Column(name = "delay_while_idle")
    private boolean shouldDelayWhileIdle;

    @Column(name = "dry_run")
    private boolean dryRun;

    @Column(name = "maximum_retries")
    private int maximumRetries = 10;

    @Basic
    @Column(name = "added_time", columnDefinition = "timestamp without time zone")
    @Temporal(TemporalType.TIMESTAMP)
    private Date addedTime;

    @PrePersist
    public void updatedTime() {
        setAddedTime(new Date());
    }

    public Long getId() {
        return id;
    }

    public List<Recipient> getRecipients() {
        return recipients;
    }

    public void setRecipients(List<Recipient> recipients) {
        this.recipients = recipients;
    }

    public Credentials getCredentials() {
        return credentials;
    }

    public void setCredentials(Credentials credentials) {
        this.credentials = credentials;
    }

    public List<PayloadElement> getPayloadData() {
        return payloadData;
    }

    public void setPayloadData(List<PayloadElement> payloadData) {
        this.payloadData = payloadData;
    }

    public String getCollapseKey() {
        return collapseKey;
    }

    public void setCollapseKey(String collapseKey) {
        this.collapseKey = collapseKey;
    }

    public MessagePriority getMessagePriority() {
        return messagePriority;
    }

    public void setMessagePriority(MessagePriority messagePriority) {
        this.messagePriority = messagePriority;
    }

    public int getTtlSeconds() {
        return ttlSeconds;
    }

    public void setTtlSeconds(int ttlSeconds) {
        this.ttlSeconds = ttlSeconds;
    }

    public boolean isShouldDelayWhileIdle() {
        return shouldDelayWhileIdle;
    }

    public void setShouldDelayWhileIdle(boolean shouldDelayWhileIdle) {
        this.shouldDelayWhileIdle = shouldDelayWhileIdle;
    }

    public boolean isDryRun() {
        return dryRun;
    }

    public void setDryRun(boolean dryRun) {
        this.dryRun = dryRun;
    }

    public int getMaximumRetries() {
        return maximumRetries;
    }

    public void setMaximumRetries(int maximumRetries) {
        this.maximumRetries = maximumRetries;
    }

    public Date getAddedTime() {
        return addedTime;
    }

    private void setAddedTime(Date addedTime) {
        this.addedTime = addedTime;
    }
} @Entity
@Table(name = "recipient_failures", schema = "pushservices")
public class PlatformFailure extends Model {
    public static Finder<Long, PlatformFailure> find = new Finder<>(PlatformFailure.class);

    @Id
    @JsonIgnore
    @Column(name = "id")
    @SequenceGenerator(name = "gen", sequenceName = "pushservices.failure_id_seq", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gen")
    private Long id;

    @OneToOne(mappedBy = "failure")
    private Recipient recipient;

    @Column(name = "type")
    @Enumerated(EnumType.STRING)
    private FailureType failureType;

    @Column(name = "message")
    private String failureMessage;

    @Basic
    @Column(name = "fail_time", columnDefinition = "timestamp without time zone")
    @Temporal(TemporalType.TIMESTAMP)
    private Date failTime;

    public PlatformFailure(String failureMessage) {
        setFailureMessage(failureMessage);
    }

    public PlatformFailure(FailureType failureType, String failureMessage) {
        setFailureType(failureType);
        setFailureMessage(failureMessage);
    }

    public PlatformFailure(FailureType failureType, String failureMessage, Date failTime) {
        setFailureType(failureType);
        setFailureMessage(failureMessage);
        setFailTime(failTime);
    }

    @PrePersist
    public void prePersist() {
        if (failTime == null) {
            setFailTime(new Date());
        }
    }

    public Long getId() {
        return id;
    }

    public Recipient getRecipient() {
        return recipient;
    }

    public void setRecipient(Recipient recipient) {
        this.recipient = recipient;
    }

    public FailureType getFailureType() {
        return failureType;
    }

    public void setFailureType(FailureType failureType) {
        this.failureType = failureType;
    }

    public String getFailureMessage() {
        return failureMessage;
    }

    public void setFailureMessage(String failureMessage) {
        this.failureMessage = failureMessage;
    }

    public Date getFailTime() {
        return failTime;
    }

    public void setFailTime(Date failTime) {
        this.failTime = failTime;
    }
} @Entity
@Table(name = "recipients", schema = "pushservices")
public class Recipient extends Model {
    public static Finder<Long, Recipient> find = new Finder<>(Recipient.class);

    @Id
    @JsonIgnore
    @Column(name = "id")
    @SequenceGenerator(name = "gen", sequenceName = "pushservices.recipient_id_seq", allocationSize = 1)
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "gen")
    private Long id;

    @OneToOne(fetch = FetchType.EAGER, cascade = CascadeType.ALL)
    private PlatformFailure failure;

    @Column(name = "token", columnDefinition = "TEXT")
    private String token;

    @ManyToOne
    private Message message;

    @Column(name = "state")
    private RecipientState state;

    @Column(name = "send_attempts")
    private int sendAttemptCount;

    @Basic
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "last_attempt", columnDefinition = "timestamp without time zone")
    private Date lastSendAttempt;

    @Basic
    @Temporal(TemporalType.TIMESTAMP)
    @Column(name = "next_attempt", columnDefinition = "timestamp without time zone")
    private Date nextAttempt;

    @PrePersist
    public void prePersist() {
        state = RecipientState.STATE_IDLE;
        sendAttemptCount = 0;
    }

    public Recipient(@Nonnull String token) {
        setToken(token);
    }

    public Long getId() {
        return id;
    }

    public PlatformFailure getPlatformFailure() {
        return failure;
    }

    public void setFailure(PlatformFailure failure) {
        this.failure = failure;
    }

    public String getToken() {
        return token;
    }

    private void setToken(String token) {
        this.token = token;
    }

    public Message getMessage() {
        return message;
    }

    public void setMessage(Message message) {
        this.message = message;
    }

    public RecipientState getState() {
        return state;
    }

    public void setState(RecipientState state) {
        this.state = state;
    }

    public int getSendAttemptCount() {
        return sendAttemptCount;
    }

    public void setSendAttemptCount(int sendAttemptCount) {
        this.sendAttemptCount = sendAttemptCount;
    }

    public Date getLastSendAttempt() {
        return lastSendAttempt;
    }

    public void setLastSendAttempt(Date lastSendAttempt) {
        this.lastSendAttempt = lastSendAttempt;
    }

    public Date getNextAttempt() {
        return nextAttempt;
    }

    public void setNextAttempt(Date nextAttempt) {
        this.nextAttempt = nextAttempt;
    }
} public class GcmMessageSerializer implements JsonSerializer<Message> {
    private JsonArray mJsonRegistrationIds = new JsonArray();

    public GcmMessageSerializer(@Nonnull List<Recipient> recipients) {
        for (Recipient recipient : recipients) {

            // Extra safeguard to ensure we don't add invalid recipients.
            JsonPrimitive jsonRecipientToken = new JsonPrimitive(recipient.getToken() != null ? recipient.getToken() : "");
            mJsonRegistrationIds.add(jsonRecipientToken);
        }
    }

    @Override
    public JsonElement serialize(Message message, Type typeOfSrc, JsonSerializationContext context) {

        // Serialise the payload data into Json Elements.
        JsonObject jsonPayloadData = new JsonObject();
        if (message.getPayloadData() != null) {
            for (PayloadElement payloadElement : message.getPayloadData()) {
                jsonPayloadData.add(payloadElement.getKey(), new JsonPrimitive(payloadElement.getValue()));
            }
        }

        // Serialise the main elements.
        JsonObject jsonMessage = new JsonObject();
        jsonMessage.add("collapse_key", new JsonPrimitive(message.getCollapseKey()));
        jsonMessage.add("time_to_live", new JsonPrimitive(message.getTtlSeconds()));
        jsonMessage.add("dry_run", new JsonPrimitive(message.isDryRun()));
        jsonMessage.add("registration_ids", mJsonRegistrationIds);
        jsonMessage.add("data", jsonPayloadData);

        // Add a restricted package attribute.
        if (message.getCredentials() != null && message.getCredentials().getPackageUri() != null) {
            jsonMessage.add("restricted_package_name", new JsonPrimitive(message.getCredentials().getPackageUri()));
        }

        return jsonMessage;
    }
}
import appmodels.pushservices.GcmResponse;
import com.google.gson.Gson;
import com.google.gson.GsonBuilder;
import com.google.inject.Inject;
import enums.pushservices.FailureType;
import enums.pushservices.PlatformType;
import enums.pushservices.RecipientState;
import exceptions.pushservices.PlatformEndpointException;
import helpers.pushservices.MessageHelper;
import helpers.pushservices.PlatformHelper;
import interfaces.pushservices.PlatformResponse;
import models.pushservices.app.UpdatedRecipient;
import models.pushservices.db.Message;
import models.pushservices.db.PlatformFailure;
import models.pushservices.db.Recipient;
import play.Logger;
import play.libs.ws.WSClient;
import play.libs.ws.WSResponse;
import serializers.pushservices.GcmMessageSerializer;
import javax.annotation.Nonnull;
import java.util.*;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.CompletionStage;
import java.util.concurrent.ConcurrentHashMap;
package services.pushservices;



/**
 * Sends GCM alerts to a batch of given tokens and data.
 * <p>
 * - This class just deals with GCM concepts:
 * - A list of registration_ids
 * - A collapse_key
 * - The keys and string data to send.
 */
public class GcmMessageDispatcher extends PlatformMessageDispatcher {
    private static final int ENDPOINT_REQUEST_TIMEOUT_SECONDS = 60 * 1000;
    private static final int MESSAGE_RECIPIENT_BATCH_SIZE = 1000;
    private WSClient mWsClient;

    private GcmMessageDispatcher() {
    }

    @Inject
    protected GcmMessageDispatcher(WSClient wsClient) {
        mWsClient = wsClient;
    }

    /**
     * Dispatch a message synchronously to the Google GCM service.
     *
     * @param message          The message to send.
     * @param responseListener The response listener.
     */
    public void dispatchMessage(@Nonnull Message message, @Nonnull PlatformResponse responseListener) {
        CompletableFuture.runAsync(() -> dispatchMessageInternal(message, responseListener));
    }

    @Override
    public PlatformType getPlatform() {
        return PlatformType.SERVICE_GCM;
    }

    /**
     * Dispatch a message synchronously to the Google GCM service.
     *
     * @param message          The message to send.
     * @param responseListener The response listener.
     */
    private void dispatchMessageInternal(@Nonnull Message message, @Nonnull PlatformResponse responseListener) {
        final Date currentTime = new Date();

        // Return error on no recipients.
        if (message.getRecipients() == null || message.getRecipients().isEmpty()) {
            PlatformFailure failure = new PlatformFailure(FailureType.MESSAGE_REGISTRATIONS_MISSING,
                    PlatformHelper.getGcmFailureName(FailureType.MESSAGE_REGISTRATIONS_MISSING), currentTime);

            for (Recipient recipient : message.getRecipients()) {
                recipient.setState(RecipientState.STATE_FAILED);
                recipient.setFailure(failure);
            }
            responseListener.messageFailure(message, failure);
            return;
        }

        // Return error on no platform.
        if (message.getCredentials() == null || message.getCredentials().getPlatformType() == null) {
            PlatformFailure failure = new PlatformFailure(FailureType.PLATFORM_AUTH_INVALID,
                    PlatformHelper.getGcmFailureName(FailureType.PLATFORM_AUTH_INVALID), currentTime);

            for (Recipient recipient : message.getRecipients()) {
                recipient.setState(RecipientState.STATE_FAILED);
                recipient.setFailure(failure);
            }
            responseListener.messageFailure(message, failure);
            return;
        }

        // Split the recipients into "batches" of 1000 as it could be over the max size for a GCM message.
        final Map<Integer, List<Recipient>> recipientBatches = batchMessageRecipients(message);
        Set<Integer> processedBatches = new HashSet<>();
        ConcurrentHashMap<Integer, GcmResponse> messageBatchResponses = new ConcurrentHashMap<>();

        // Iterate through each recipient batch and get a GcmResponse for it. Then join all batches.
        for (Map.Entry<Integer, List<Recipient>> batchEntry : recipientBatches.entrySet()) {
            final int batchNumber = batchEntry.getKey();
            final List<Recipient> batch = batchEntry.getValue();

            // Send each message and save the response. When all responses are returned, combine them and call listener.
            sendMessage(message, batch)
                    .thenAccept(response -> {
                        processedBatches.add(batchNumber);

                        GcmResponse gcmResponse = parseMessageResponse(response);
                        messageBatchResponses.put(batchNumber, gcmResponse);
                        Logger.debug(String.format("Finished parsing GCM Response for batch %d", batchNumber));

                        // If all the provider responses have returned, combine all and return the result.
                        if (processedBatches.size() == recipientBatches.size()) {
                            MessageDispatchResult result = combineResponses(recipientBatches, messageBatchResponses, message.getMaximumRetries());
                            responseListener.messageSuccess(message, result.completedRecipients, result.failedRecipients,
                                    result.recipientsToUpdate, result.recipientsToRetry);
                        }

                    }).exceptionally(

                    e -> {
                        PlatformFailure newFailure = new PlatformFailure(FailureType.ERROR_UNKNOWN,
                                PlatformHelper.getGcmFailureName(FailureType.ERROR_UNKNOWN), new Date());

                        if (e instanceof PlatformEndpointException) {
                            PlatformEndpointException exception = (PlatformEndpointException) e;

                            for (Recipient recipient : message.getRecipients()) {
                                int statusCode = exception.statusCode;

                                if (statusCode == 400) {
                                    recipient.setState(RecipientState.STATE_FAILED);
                                    newFailure = new PlatformFailure(FailureType.MESSAGE_PAYLOAD_INVALID,
                                            PlatformHelper.getGcmFailureName(FailureType.MESSAGE_PAYLOAD_INVALID));

                                } else if (statusCode == 401) {
                                    recipient.setState(RecipientState.STATE_FAILED);
                                    newFailure = new PlatformFailure(FailureType.PLATFORM_AUTH_INVALID,
                                            PlatformHelper.getGcmFailureName(FailureType.PLATFORM_AUTH_INVALID));

                                } else if (statusCode == 420) {
                                    MessageHelper.setRecipientRetry(getPlatform(), recipient, message.getMaximumRetries());