http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Message.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Message.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Message.java new file mode 100644 index 0000000..34234dc --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Message.java @@ -0,0 +1,317 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.domain; + +import java.io.Serializable; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.apache.fineract.infrastructure.gcm.GcmConstants; + +/** + * GCM message. + * + * <p> + * Instances of this class are immutable and should be created using a + * {@link Builder}. Examples: + * + * <strong>Simplest message:</strong> + * + * <pre> + * <code> + * Message message = new Message.Builder().build(); + * </pre> + * + * </code> + * + * <strong>Message with optional attributes:</strong> + * + * <pre> + * <code> + * Message message = new Message.Builder() + * .collapseKey(collapseKey) + * .timeToLive(3) + * .delayWhileIdle(true) + * .dryRun(true) + * .restrictedPackageName(restrictedPackageName) + * .build(); + * </pre> + * + * </code> + * + * <strong>Message with optional attributes and payload data:</strong> + * + * <pre> + * <code> + * Message message = new Message.Builder() + * .priority("normal") + * .collapseKey(collapseKey) + * .timeToLive(3) + * .delayWhileIdle(true) + * .dryRun(true) + * .restrictedPackageName(restrictedPackageName) + * .addData("key1", "value1") + * .addData("key2", "value2") + * .build(); + * </pre> + * + * </code> + */ +public final class Message implements Serializable { + + private final String collapseKey; + private final Boolean delayWhileIdle; + private final Integer timeToLive; + private final Map<String, String> data; + private final Boolean dryRun; + private final String restrictedPackageName; + private final String priority; + private final Boolean contentAvailable; + private final Notification notification; + + public enum Priority { + NORMAL, HIGH + } + + public static final class Builder { + + private final Map<String, String> data; + + // optional parameters + private String collapseKey; + private Boolean delayWhileIdle; + private Integer timeToLive; + private Boolean dryRun; + private String restrictedPackageName; + private String priority; + private Boolean contentAvailable; + private Notification notification; + + public Builder() { + this.data = new LinkedHashMap<>(); + } + + /** + * Sets the collapseKey property. + */ + public Builder collapseKey(String value) { + collapseKey = value; + return this; + } + + /** + * Sets the delayWhileIdle property (default value is {@literal false}). + */ + public Builder delayWhileIdle(boolean value) { + delayWhileIdle = value; + return this; + } + + /** + * Sets the time to live, in seconds. + */ + public Builder timeToLive(int value) { + timeToLive = value; + return this; + } + + /** + * Adds a key/value pair to the payload data. + */ + public Builder addData(String key, String value) { + data.put(key, value); + return this; + } + + /** + * Sets the dryRun property (default value is {@literal false}). + */ + public Builder dryRun(boolean value) { + dryRun = value; + return this; + } + + /** + * Sets the restrictedPackageName property. + */ + public Builder restrictedPackageName(String value) { + restrictedPackageName = value; + return this; + } + + /** + * Sets the priority property. + */ + public Builder priority(Priority value) { + switch (value) { + case NORMAL: + priority = GcmConstants.MESSAGE_PRIORITY_NORMAL; + break; + case HIGH: + priority = GcmConstants.MESSAGE_PRIORITY_HIGH; + break; + } + return this; + } + + /** + * Sets the notification property. + */ + public Builder notification(Notification value) { + notification = value; + return this; + } + + /** + * Sets the contentAvailable property + */ + public Builder contentAvailable(Boolean value) { + contentAvailable = value; + return this; + } + + public Message build() { + return new Message(this); + } + + } + + private Message(Builder builder) { + collapseKey = builder.collapseKey; + delayWhileIdle = builder.delayWhileIdle; + data = Collections.unmodifiableMap(builder.data); + timeToLive = builder.timeToLive; + dryRun = builder.dryRun; + restrictedPackageName = builder.restrictedPackageName; + priority = builder.priority; + contentAvailable = builder.contentAvailable; + notification = builder.notification; + } + + /** + * Gets the collapse key. + */ + public String getCollapseKey() { + return collapseKey; + } + + /** + * Gets the delayWhileIdle flag. + */ + public Boolean isDelayWhileIdle() { + return delayWhileIdle; + } + + /** + * Gets the time to live (in seconds). + */ + public Integer getTimeToLive() { + return timeToLive; + } + + /** + * Gets the dryRun flag. + */ + public Boolean isDryRun() { + return dryRun; + } + + /** + * Gets the restricted package name. + */ + public String getRestrictedPackageName() { + return restrictedPackageName; + } + + /** + * Gets the message priority value. + */ + public String getPriority() { + return priority; + } + + /** + * Gets the contentAvailable value + */ + public Boolean getContentAvailable() { + return contentAvailable; + } + + /** + * Gets the payload data, which is immutable. + */ + public Map<String, String> getData() { + return data; + } + + /** + * Gets notification payload, which is immutable. + */ + public Notification getNotification() { + return notification; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("Message("); + if (priority != null) { + builder.append("priority=").append(priority).append(", "); + } + if (contentAvailable != null) { + builder.append("contentAvailable=").append(contentAvailable) + .append(", "); + } + if (collapseKey != null) { + builder.append("collapseKey=").append(collapseKey).append(", "); + } + if (timeToLive != null) { + builder.append("timeToLive=").append(timeToLive).append(", "); + } + if (delayWhileIdle != null) { + builder.append("delayWhileIdle=").append(delayWhileIdle) + .append(", "); + } + if (dryRun != null) { + builder.append("dryRun=").append(dryRun).append(", "); + } + if (restrictedPackageName != null) { + builder.append("restrictedPackageName=") + .append(restrictedPackageName).append(", "); + } + if (notification != null) { + builder.append("notification: ").append(notification).append(", "); + } + if (!data.isEmpty()) { + builder.append("data: {"); + for (Map.Entry<String, String> entry : data.entrySet()) { + builder.append(entry.getKey()).append("=") + .append(entry.getValue()).append(","); + } + builder.delete(builder.length() - 1, builder.length()); + builder.append("}"); + } + if (builder.charAt(builder.length() - 1) == ' ') { + builder.delete(builder.length() - 2, builder.length()); + } + builder.append(")"); + return builder.toString(); + } + +}
http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/MulticastResult.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/MulticastResult.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/MulticastResult.java new file mode 100644 index 0000000..c58de9f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/MulticastResult.java @@ -0,0 +1,151 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.domain; + +import java.io.Serializable; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +/** + * Result of a GCM multicast message request . + */ +public final class MulticastResult implements Serializable { + + private final int success; + private final int failure; + private final int canonicalIds; + private final long multicastId; + private final List<Result> results; + private final List<Long> retryMulticastIds; + + public static final class Builder { + + private final List<Result> results = new ArrayList<>(); + + // required parameters + private final int success; + private final int failure; + private final int canonicalIds; + private final long multicastId; + + // optional parameters + private List<Long> retryMulticastIds; + + public Builder(int success, int failure, int canonicalIds, + long multicastId) { + this.success = success; + this.failure = failure; + this.canonicalIds = canonicalIds; + this.multicastId = multicastId; + } + + public Builder addResult(Result result) { + results.add(result); + return this; + } + + public Builder retryMulticastIds(List<Long> retryMulticastIds) { + this.retryMulticastIds = retryMulticastIds; + return this; + } + + public MulticastResult build() { + return new MulticastResult(this); + } + } + + private MulticastResult(Builder builder) { + success = builder.success; + failure = builder.failure; + canonicalIds = builder.canonicalIds; + multicastId = builder.multicastId; + results = Collections.unmodifiableList(builder.results); + List<Long> tmpList = builder.retryMulticastIds; + if (tmpList == null) { + tmpList = Collections.emptyList(); + } + retryMulticastIds = Collections.unmodifiableList(tmpList); + } + + /** + * Gets the multicast id. + */ + public long getMulticastId() { + return multicastId; + } + + /** + * Gets the number of successful messages. + */ + public int getSuccess() { + return success; + } + + /** + * Gets the total number of messages sent, regardless of the status. + */ + public int getTotal() { + return success + failure; + } + + /** + * Gets the number of failed messages. + */ + public int getFailure() { + return failure; + } + + /** + * Gets the number of successful messages that also returned a canonical + * registration id. + */ + public int getCanonicalIds() { + return canonicalIds; + } + + /** + * Gets the results of each individual message, which is immutable. + */ + public List<Result> getResults() { + return results; + } + + /** + * Gets additional ids if more than one multicast message was sent. + */ + public List<Long> getRetryMulticastIds() { + return retryMulticastIds; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("MulticastResult(") + .append("multicast_id=").append(multicastId).append(",") + .append("total=").append(getTotal()).append(",") + .append("success=").append(success).append(",") + .append("failure=").append(failure).append(",") + .append("canonical_ids=").append(canonicalIds).append(","); + if (!results.isEmpty()) { + builder.append("results: " + results); + } + return builder.toString(); + } + +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Notification.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Notification.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Notification.java new file mode 100644 index 0000000..589e772 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Notification.java @@ -0,0 +1,330 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.domain; + +import java.io.Serializable; +import java.util.Collections; +import java.util.List; + +/** + * GCM message notification part. + * + * <p> + * Instances of this class are immutable and should be created using a + * {@link Builder}. Examples: + * + * <strong>Simplest notification:</strong> + * + * <pre> + * <code> + * Notification notification = new Notification.Builder("myicon").build(); + * </pre> + * + * </code> + * + * <strong>Notification with optional attributes:</strong> + * + * <pre> + * <code> + * Notification notification = new Notification.Builder("myicon") + * .title("Hello world!") + * .body("Here is a more detailed description") + * .build(); + * </pre> + * + * </code> + */ +public final class Notification implements Serializable { + + private final String title; + private final String body; + private final String icon; + private final String sound; + private final Integer badge; + private final String tag; + private final String color; + private final String clickAction; + private final String bodyLocKey; + private final List<String> bodyLocArgs; + private final String titleLocKey; + private final List<String> titleLocArgs; + + public static final class Builder { + + // required parameters + private final String icon; + + // optional parameters + private String title; + private String body; + private String sound; + private Integer badge; + private String tag; + private String color; + private String clickAction; + private String bodyLocKey; + private List<String> bodyLocArgs; + private String titleLocKey; + private List<String> titleLocArgs; + + public Builder(String icon) { + this.icon = icon; + this.sound = "default"; // the only currently supported value + } + + /** + * Sets the title property. + */ + public Builder title(String value) { + title = value; + return this; + } + + /** + * Sets the body property. + */ + public Builder body(String value) { + body = value; + return this; + } + + /** + * Sets the sound property (default value is {@literal default}). + */ + public Builder sound(String value) { + sound = value; + return this; + } + + /** + * Sets the badge property. + */ + public Builder badge(int value) { + badge = value; + return this; + } + + /** + * Sets the tag property. + */ + public Builder tag(String value) { + tag = value; + return this; + } + + /** + * Sets the color property in {@literal #rrggbb} format. + */ + public Builder color(String value) { + color = value; + return this; + } + + /** + * Sets the click action property. + */ + public Builder clickAction(String value) { + clickAction = value; + return this; + } + + /** + * Sets the body localization key property. + */ + public Builder bodyLocKey(String value) { + bodyLocKey = value; + return this; + } + + /** + * Sets the body localization values property. + */ + public Builder bodyLocArgs(List<String> value) { + bodyLocArgs = Collections.unmodifiableList(value); + return this; + } + + /** + * Sets the title localization key property. + */ + public Builder titleLocKey(String value) { + titleLocKey = value; + return this; + } + + /** + * Sets the title localization values property. + */ + public Builder titleLocArgs(List<String> value) { + titleLocArgs = Collections.unmodifiableList(value); + return this; + } + + public Notification build() { + return new Notification(this); + } + + } + + private Notification(Builder builder) { + title = builder.title; + body = builder.body; + icon = builder.icon; + sound = builder.sound; + badge = builder.badge; + tag = builder.tag; + color = builder.color; + clickAction = builder.clickAction; + bodyLocKey = builder.bodyLocKey; + bodyLocArgs = builder.bodyLocArgs; + titleLocKey = builder.titleLocKey; + titleLocArgs = builder.titleLocArgs; + } + + /** + * Gets the title. + */ + public String getTitle() { + return title; + } + + /** + * Gets the body. + */ + public String getBody() { + return body; + } + + /** + * Gets the icon. + */ + public String getIcon() { + return icon; + } + + /** + * Gets the sound. + */ + public String getSound() { + return sound; + } + + /** + * Gets the badge. + */ + public Integer getBadge() { + return badge; + } + + /** + * Gets the tag. + */ + public String getTag() { + return tag; + } + + /** + * Gets the color. + */ + public String getColor() { + return color; + } + + /** + * Gets the click action. + */ + public String getClickAction() { + return clickAction; + } + + /** + * Gets the body localization key. + */ + public String getBodyLocKey() { + return bodyLocKey; + } + + /** + * Gets the body localization values list, which is immutable. + */ + public List<String> getBodyLocArgs() { + return bodyLocArgs; + } + + /** + * Gets the title localization key. + */ + public String getTitleLocKey() { + return titleLocKey; + } + + /** + * Gets the title localization values list, which is immutable. + */ + public List<String> getTitleLocArgs() { + return titleLocArgs; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("Notification("); + if (title != null) { + builder.append("title=").append(title).append(", "); + } + if (body != null) { + builder.append("body=").append(body).append(", "); + } + if (icon != null) { + builder.append("icon=").append(icon).append(", "); + } + if (sound != null) { + builder.append("sound=").append(sound).append(", "); + } + if (badge != null) { + builder.append("badge=").append(badge).append(", "); + } + if (tag != null) { + builder.append("tag=").append(tag).append(", "); + } + if (color != null) { + builder.append("color=").append(color).append(", "); + } + if (clickAction != null) { + builder.append("clickAction=").append(clickAction).append(", "); + } + if (bodyLocKey != null) { + builder.append("bodyLocKey=").append(bodyLocKey).append(", "); + } + if (bodyLocArgs != null) { + builder.append("bodyLocArgs=").append(bodyLocArgs).append(", "); + } + if (titleLocKey != null) { + builder.append("titleLocKey=").append(titleLocKey).append(", "); + } + if (titleLocArgs != null) { + builder.append("titleLocArgs=").append(titleLocArgs).append(", "); + } + if (builder.charAt(builder.length() - 1) == ' ') { + builder.delete(builder.length() - 2, builder.length()); + } + builder.append(")"); + return builder.toString(); + } + +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/NotificationConfigurationData.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/NotificationConfigurationData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/NotificationConfigurationData.java new file mode 100644 index 0000000..2d4acc9 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/NotificationConfigurationData.java @@ -0,0 +1,49 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.domain; + +public class NotificationConfigurationData { + + private final Long id; + private final String serverKey; + private final String gcmEndPoint; + private final String fcmEndPoint; + public NotificationConfigurationData(Long id, String serverKey,final String gcmEndPoint,final String fcmEndPoint) { + this.id = id; + this.serverKey = serverKey; + this.gcmEndPoint = gcmEndPoint; + this.fcmEndPoint = fcmEndPoint; + } + public Long getId() { + return id; + } + public String getServerKey() { + return serverKey; + } + + public String getGcmEndPoint() { + return gcmEndPoint; + } + public String getFcmEndPoint() { + return fcmEndPoint; + } + + + +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Result.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Result.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Result.java new file mode 100644 index 0000000..76aafa8 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Result.java @@ -0,0 +1,187 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.domain; + +import java.io.Serializable; +import java.util.List; + +/** + * Result of a GCM message request that returned HTTP status code 200. + * + * <p> + * If the message is successfully created, the {@link #getMessageId()} returns + * the message id and {@link #getErrorCodeName()} returns {@literal null}; + * otherwise, {@link #getMessageId()} returns {@literal null} and + * {@link #getErrorCodeName()} returns the code of the error. + * + * <p> + * There are cases when a request is accept and the message successfully + * created, but GCM has a canonical registration id for that device. In this + * case, the server should update the registration id to avoid rejected requests + * in the future. + * + * <p> + * In a nutshell, the workflow to handle a result is: + * + * <pre> + * - Call {@link #getMessageId()}: + * - {@literal null} means error, call {@link #getErrorCodeName()} + * - non-{@literal null} means the message was created: + * - Call {@link #getCanonicalRegistrationId()} + * - if it returns {@literal null}, do nothing. + * - otherwise, update the server datastore with the new id. + * </pre> + */ +public final class Result implements Serializable { + + private final String messageId; + private final String canonicalRegistrationId; + private final String errorCode; + private final Integer success; + private final Integer failure; + private final List<String> failedRegistrationIds; + private final int status; + + public static final class Builder { + + // optional parameters + private String messageId; + private String canonicalRegistrationId; + private String errorCode; + private Integer success; + private Integer failure; + private List<String> failedRegistrationIds; + private int status; + + public Builder canonicalRegistrationId(String value) { + canonicalRegistrationId = value; + return this; + } + + public Builder messageId(String value) { + messageId = value; + return this; + } + + public Builder errorCode(String value) { + errorCode = value; + return this; + } + + public Builder success(Integer value) { + success = value; + return this; + } + + public Builder failure(Integer value) { + failure = value; + return this; + } + + public Builder status(int value) { + status = value; + return this; + } + + public Builder failedRegistrationIds(List<String> value) { + failedRegistrationIds = value; + return this; + } + + public Result build() { + return new Result(this); + } + } + + private Result(Builder builder) { + canonicalRegistrationId = builder.canonicalRegistrationId; + messageId = builder.messageId; + errorCode = builder.errorCode; + success = builder.success; + failure = builder.failure; + failedRegistrationIds = builder.failedRegistrationIds; + status = builder.status; + } + + /** + * Gets the message id, if any. + */ + public String getMessageId() { + return messageId; + } + + /** + * Gets the canonical registration id, if any. + */ + public String getCanonicalRegistrationId() { + return canonicalRegistrationId; + } + + /** + * Gets the error code, if any. + */ + public String getErrorCodeName() { + return errorCode; + } + + public Integer getSuccess() { + return success; + } + + public Integer getFailure() { + return failure; + } + + public List<String> getFailedRegistrationIds() { + return failedRegistrationIds; + } + + @Override + public String toString() { + StringBuilder builder = new StringBuilder("["); + if (messageId != null) { + builder.append(" messageId=").append(messageId); + } + if (canonicalRegistrationId != null) { + builder.append(" canonicalRegistrationId=").append( + canonicalRegistrationId); + } + if (errorCode != null) { + builder.append(" errorCode=").append(errorCode); + } + if (success != null) { + builder.append(" groupSuccess=").append(success); + } + if (failure != null) { + builder.append(" groupFailure=").append(failure); + } + if (failedRegistrationIds != null) { + builder.append(" failedRegistrationIds=").append( + failedRegistrationIds); + } + return builder.append(" ]").toString(); + } + + public int getStatus() { + return this.status; + } + + + +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Sender.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Sender.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Sender.java new file mode 100644 index 0000000..cc9970e --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Sender.java @@ -0,0 +1,832 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.domain; + +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_CANONICAL_IDS; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_ERROR; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_FAILURE; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_MESSAGE_ID; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_MULTICAST_ID; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_BADGE; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_BODY; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_BODY_LOC_ARGS; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_BODY_LOC_KEY; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_CLICK_ACTION; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_COLOR; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_ICON; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_SOUND; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_TAG; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_TITLE; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_TITLE_LOC_ARGS; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_NOTIFICATION_TITLE_LOC_KEY; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_PAYLOAD; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_REGISTRATION_IDS; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_TO; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_RESULTS; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.JSON_SUCCESS; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_COLLAPSE_KEY; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_DELAY_WHILE_IDLE; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_DRY_RUN; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_PRIORITY; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_CONTENT_AVAILABLE; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_RESTRICTED_PACKAGE_NAME; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.PARAM_TIME_TO_LIVE; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.TOKEN_CANONICAL_REG_ID; +import static org.apache.fineract.infrastructure.gcm.GcmConstants.TOPIC_PREFIX; + +import org.apache.fineract.infrastructure.gcm.GcmConstants; +import org.apache.fineract.infrastructure.gcm.exception.InvalidRequestException; +/*import org.json.simple.JSONArray; +import org.json.simple.JSONObject; +import org.json.simple.JSONValue; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException;*/ + + + + + + +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonObject; +import com.google.gson.JsonParser; + +import java.io.BufferedReader; +import java.io.Closeable; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.logging.Level; +import java.util.logging.Logger; + +/** + * Helper class to send messages to the GCM service using an API Key. + */ +public class Sender { + + protected static final String UTF8 = "UTF-8"; + + /** + * Initial delay before first retry, without jitter. + */ + protected static final int BACKOFF_INITIAL_DELAY = 1000; + /** + * Maximum delay before a retry. + */ + protected static final int MAX_BACKOFF_DELAY = 1024000; + + protected final Random random = new Random(); + protected static final Logger logger = Logger.getLogger(Sender.class + .getName()); + + private final String key; + + private String endpoint; + + private int connectTimeout; + private int readTimeout; + + /** + * Full options constructor. + * + * @param key + * FCM Server Key obtained through the Firebase Web Console. + * @param endpoint + * Endpoint to use when sending the message. + */ + public Sender(String key, String endpoint) { + this.key = nonNull(key); + this.endpoint = nonNull(endpoint); + } + + public String getEndpoint() { + return endpoint; + } + + /** + * Set the underlying URLConnection's connect timeout (in milliseconds). A + * timeout value of 0 specifies an infinite timeout. + * <p> + * Default is the system's default timeout. + * + * @see java.net.URLConnection#setConnectTimeout(int) + */ + public final void setConnectTimeout(int connectTimeout) { + if (connectTimeout < 0) { + throw new IllegalArgumentException("timeout can not be negative"); + } + this.connectTimeout = connectTimeout; + } + + /** + * Set the underlying URLConnection's read timeout (in milliseconds). A + * timeout value of 0 specifies an infinite timeout. + * <p> + * Default is the system's default timeout. + * + * @see java.net.URLConnection#setReadTimeout(int) + */ + public final void setReadTimeout(int readTimeout) { + if (readTimeout < 0) { + throw new IllegalArgumentException("timeout can not be negative"); + } + this.readTimeout = readTimeout; + } + + /** + * Sends a message to one device, retrying in case of unavailability. + * + * <p> + * <strong>Note: </strong> this method uses exponential back-off to retry in + * case of service unavailability and hence could block the calling thread + * for many seconds. + * + * @param message + * message to be sent, including the device's registration id. + * @param to + * registration token, notification key, or topic where the + * message will be sent. + * @param retries + * number of retries in case of service unavailability errors. + * + * @return result of the request (see its javadoc for more details). + * + * @throws IllegalArgumentException + * if to is {@literal null}. + * @throws InvalidRequestException + * if GCM didn't returned a 200 or 5xx status. + * @throws IOException + * if message could not be sent. + */ + public Result send(Message message, String to, int retries) + throws IOException { + int attempt = 0; + Result result; + int backoff = BACKOFF_INITIAL_DELAY; + boolean tryAgain; + do { + attempt++; + if (logger.isLoggable(Level.FINE)) { + logger.fine("Attempt #" + attempt + " to send message " + + message + " to regIds " + to); + } + result = sendNoRetry(message, to); + tryAgain = result == null && attempt <= retries; + if (tryAgain) { + int sleepTime = backoff / 2 + random.nextInt(backoff); + sleep(sleepTime); + if (2 * backoff < MAX_BACKOFF_DELAY) { + backoff *= 2; + } + } + } while (tryAgain); + if (result == null) { + throw new IOException("Could not send message after " + attempt + + " attempts"); + } + return result; + } + + /** + * Sends a message without retrying in case of service unavailability. See + * {@link #send(Message, String, int)} for more info. + * + * @return result of the post, or {@literal null} if the GCM service was + * unavailable or any network exception caused the request to fail, + * or if the response contains more than one result. + * + * @throws InvalidRequestException + * if GCM didn't returned a 200 status. + * @throws IllegalArgumentException + * if to is {@literal null}. + */ + public Result sendNoRetry(Message message, String to) throws IOException { + nonNull(to); + Map<Object, Object> jsonRequest = new HashMap<>(); + messageToMap(message, jsonRequest); + jsonRequest.put(JSON_TO, to); + Map<String , Object> responseMap = makeGcmHttpRequest(jsonRequest); + String responseBody = null; + if (responseMap.get("responseBody") != null) { + responseBody = (String) responseMap.get("responseBody"); + } + int status = (int) responseMap.get("status"); + //responseBody + if (responseBody == null) { + return null; + } + JsonParser jsonParser = new JsonParser(); + JsonObject jsonResponse; + try { + jsonResponse = (JsonObject) jsonParser.parse(responseBody); + Result.Builder resultBuilder = new Result.Builder(); + if (jsonResponse.has("results")) { + // Handle response from message sent to specific device. + JsonArray jsonResults = (JsonArray) jsonResponse.get("results"); + if (jsonResults.size() == 1) { + JsonObject jsonResult = (JsonObject) jsonResults.get(0); + String messageId = null; + String canonicalRegId = null; + String error = null; + if(jsonResult.has(JSON_MESSAGE_ID)){ + messageId = jsonResult.get(JSON_MESSAGE_ID).getAsString(); + } + if(jsonResult.has(TOKEN_CANONICAL_REG_ID)){ + canonicalRegId = jsonResult + .get(TOKEN_CANONICAL_REG_ID).getAsString(); + } + if(jsonResult.has(JSON_ERROR)){ + error = (String) jsonResult.get(JSON_ERROR).getAsString(); + } + int success = 0; + int failure = 0; + if(jsonResponse.get("success") != null){ + success = Integer.parseInt(jsonResponse.get("success").toString()); + } + if(jsonResponse.get("failure") != null){ + failure = Integer.parseInt(jsonResponse.get("failure").toString()); + } + resultBuilder.messageId(messageId) + .canonicalRegistrationId(canonicalRegId) + .success(success) + .failure(failure) + .status(status) + .errorCode(error); + } else { + logger.log(Level.WARNING, + "Found null or " + jsonResults.size() + + " results, expected one"); + return null; + } + } else if (to.startsWith(TOPIC_PREFIX)) { + if (jsonResponse.has(JSON_MESSAGE_ID)) { + // message_id is expected when this is the response from a + // topic message. + Long messageId = jsonResponse.get(JSON_MESSAGE_ID).getAsLong(); + resultBuilder.messageId(messageId.toString()); + } else if (jsonResponse.has(JSON_ERROR)) { + String error = jsonResponse.get(JSON_ERROR).getAsString(); + resultBuilder.errorCode(error); + } else { + logger.log(Level.WARNING, "Expected " + JSON_MESSAGE_ID + + " or " + JSON_ERROR + " found: " + responseBody); + return null; + } + } else if (jsonResponse.has(JSON_SUCCESS) + && jsonResponse.has(JSON_FAILURE)) { + // success and failure are expected when response is from group + // message. + int success = getNumber(responseMap, JSON_SUCCESS).intValue(); + int failure = getNumber(responseMap, JSON_FAILURE).intValue(); + List<String> failedIds = null; + if (jsonResponse.has("failed_registration_ids")) { + JsonArray jFailedIds = (JsonArray) jsonResponse + .get("failed_registration_ids").getAsJsonArray(); + failedIds = new ArrayList<>(); + for (int i = 0; i < jFailedIds.size(); i++) { + failedIds.add(jFailedIds.get(i).getAsString()); + } + } + resultBuilder.success(success).failure(failure) + .failedRegistrationIds(failedIds); + } else { + logger.warning("Unrecognized response: " + responseBody); + throw newIoException(responseBody, new Exception( + "Unrecognized response.")); + } + return resultBuilder.build(); + } catch (CustomParserException e) { + throw newIoException(responseBody, e); + } + } + + /** + * Sends a message to many devices, retrying in case of unavailability. + * + * <p> + * <strong>Note: </strong> this method uses exponential back-off to retry in + * case of service unavailability and hence could block the calling thread + * for many seconds. + * + * @param message + * message to be sent. + * @param regIds + * registration id of the devices that will receive the message. + * @param retries + * number of retries in case of service unavailability errors. + * + * @return combined result of all requests made. + * + * @throws IllegalArgumentException + * if registrationIds is {@literal null} or empty. + * @throws InvalidRequestException + * if GCM didn't returned a 200 or 503 status. + * @throws IOException + * if message could not be sent. + */ + public MulticastResult send(Message message, List<String> regIds, + int retries) throws IOException { + int attempt = 0; + MulticastResult multicastResult; + int backoff = BACKOFF_INITIAL_DELAY; + // Map of results by registration id, it will be updated after each + // attempt + // to send the messages + Map<String, Result> results = new HashMap<>(); + List<String> unsentRegIds = new ArrayList<>(regIds); + boolean tryAgain; + List<Long> multicastIds = new ArrayList<>(); + do { + multicastResult = null; + attempt++; + if (logger.isLoggable(Level.FINE)) { + logger.fine("Attempt #" + attempt + " to send message " + + message + " to regIds " + unsentRegIds); + } + try { + multicastResult = sendNoRetry(message, unsentRegIds); + } catch (IOException e) { + // no need for WARNING since exception might be already logged + logger.log(Level.FINEST, "IOException on attempt " + attempt, e); + } + if (multicastResult != null) { + long multicastId = multicastResult.getMulticastId(); + logger.fine("multicast_id on attempt # " + attempt + ": " + + multicastId); + multicastIds.add(multicastId); + unsentRegIds = updateStatus(unsentRegIds, results, + multicastResult); + tryAgain = !unsentRegIds.isEmpty() && attempt <= retries; + } else { + tryAgain = attempt <= retries; + } + if (tryAgain) { + int sleepTime = backoff / 2 + random.nextInt(backoff); + sleep(sleepTime); + if (2 * backoff < MAX_BACKOFF_DELAY) { + backoff *= 2; + } + } + } while (tryAgain); + if (multicastIds.isEmpty()) { + // all JSON posts failed due to GCM unavailability + throw new IOException("Could not post JSON requests to GCM after " + + attempt + " attempts"); + } + // calculate summary + int success = 0, failure = 0, canonicalIds = 0; + for (Result result : results.values()) { + if (result.getMessageId() != null) { + success++; + if (result.getCanonicalRegistrationId() != null) { + canonicalIds++; + } + } else { + failure++; + } + } + // build a new object with the overall result + long multicastId = multicastIds.remove(0); + MulticastResult.Builder builder = new MulticastResult.Builder(success, + failure, canonicalIds, multicastId) + .retryMulticastIds(multicastIds); + // add results, in the same order as the input + for (String regId : regIds) { + Result result = results.get(regId); + builder.addResult(result); + } + return builder.build(); + } + + /** + * Updates the status of the messages sent to devices and the list of + * devices that should be retried. + * + * @param unsentRegIds + * list of devices that are still pending an update. + * @param allResults + * map of status that will be updated. + * @param multicastResult + * result of the last multicast sent. + * + * @return updated version of devices that should be retried. + */ + private List<String> updateStatus(List<String> unsentRegIds, + Map<String, Result> allResults, MulticastResult multicastResult) { + List<Result> results = multicastResult.getResults(); + if (results.size() != unsentRegIds.size()) { + // should never happen, unless there is a flaw in the algorithm + throw new RuntimeException("Internal error: sizes do not match. " + + "currentResults: " + results + "; unsentRegIds: " + + unsentRegIds); + } + List<String> newUnsentRegIds = new ArrayList<>(); + for (int i = 0; i < unsentRegIds.size(); i++) { + String regId = unsentRegIds.get(i); + Result result = results.get(i); + allResults.put(regId, result); + String error = result.getErrorCodeName(); + if (error != null + && (error.equals(GcmConstants.ERROR_UNAVAILABLE) || error + .equals(GcmConstants.ERROR_INTERNAL_SERVER_ERROR))) { + newUnsentRegIds.add(regId); + } + } + return newUnsentRegIds; + } + + /** + * Sends a message without retrying in case of service unavailability. See + * {@link #send(Message, List, int)} for more info. + * + * @return multicast results if the message was sent successfully, + * {@literal null} if it failed but could be retried. + * + * @throws IllegalArgumentException + * if registrationIds is {@literal null} or empty. + * @throws InvalidRequestException + * if GCM didn't returned a 200 status. + * @throws IOException + * if there was a JSON parsing error + */ + public MulticastResult sendNoRetry(Message message, + List<String> registrationIds) throws IOException { + if (nonNull(registrationIds).isEmpty()) { + throw new IllegalArgumentException( + "registrationIds cannot be empty"); + } + Map<Object, Object> jsonRequest = new HashMap<>(); + messageToMap(message, jsonRequest); + jsonRequest.put(JSON_REGISTRATION_IDS, registrationIds); + Map<String , Object> responseMap = makeGcmHttpRequest(jsonRequest); + String responseBody = null; + if (responseMap.get("responseBody") != null) { + responseBody = (String) responseMap.get("responseBody"); + } + if (responseBody == null) { + return null; + } + + JsonParser parser = new JsonParser(); + JsonObject jsonResponse; + try { + jsonResponse = (JsonObject) parser.parse(responseBody); + int success = getNumber(responseMap, JSON_SUCCESS).intValue(); + int failure = getNumber(responseMap, JSON_FAILURE).intValue(); + int canonicalIds = getNumber(responseMap, JSON_CANONICAL_IDS) + .intValue(); + long multicastId = getNumber(responseMap, JSON_MULTICAST_ID) + .longValue(); + MulticastResult.Builder builder = new MulticastResult.Builder( + success, failure, canonicalIds, multicastId); + @SuppressWarnings("unchecked") + List<Map<String, Object>> results = (List<Map<String, Object>>) jsonResponse + .get(JSON_RESULTS); + if (results != null) { + for (Map<String, Object> jsonResult : results) { + String messageId = (String) jsonResult.get(JSON_MESSAGE_ID); + String canonicalRegId = (String) jsonResult + .get(TOKEN_CANONICAL_REG_ID); + String error = (String) jsonResult.get(JSON_ERROR); + Result result = new Result.Builder().messageId(messageId) + .canonicalRegistrationId(canonicalRegId) + .errorCode(error).build(); + builder.addResult(result); + } + } + return builder.build(); + } catch (CustomParserException e) { + throw newIoException(responseBody, e); + } + } + + private Map<String , Object> makeGcmHttpRequest(Map<Object, Object> jsonRequest) + throws InvalidRequestException { + String requestBody = new Gson().toJson(jsonRequest); + logger.finest("JSON request: " + requestBody); + HttpURLConnection conn; + int status; + try { + conn = post(getEndpoint(), "application/json", requestBody); + status = conn.getResponseCode(); + } catch (IOException e) { + logger.log(Level.FINE, "IOException posting to GCM", e); + return null; + } + String responseBody; + if (status != 200) { + try { + responseBody = getAndClose(conn.getErrorStream()); + logger.finest("JSON error response: " + responseBody); + } catch (IOException e) { + // ignore the exception since it will thrown an + // InvalidRequestException + // anyways + responseBody = "N/A"; + logger.log(Level.FINE, "Exception reading response: ", e); + } + throw new InvalidRequestException(status, responseBody); + } + try { + responseBody = getAndClose(conn.getInputStream()); + } catch (IOException e) { + logger.log(Level.WARNING, "IOException reading response", e); + return null; + } + logger.finest("JSON response: " + responseBody); + Map<String , Object> map = new HashMap<>(); + map.put("responseBody", responseBody); + map.put("status", status); + + return map; + } + + /** + * Populate Map with message. + * + * @param message + * Message used to populate Map. + * @param mapRequest + * Map populated by Message. + */ + private void messageToMap(Message message, Map<Object, Object> mapRequest) { + if (message == null || mapRequest == null) { + return; + } + setJsonField(mapRequest, PARAM_PRIORITY, message.getPriority()); + setJsonField(mapRequest, PARAM_CONTENT_AVAILABLE, + message.getContentAvailable()); + setJsonField(mapRequest, PARAM_TIME_TO_LIVE, message.getTimeToLive()); + setJsonField(mapRequest, PARAM_COLLAPSE_KEY, message.getCollapseKey()); + setJsonField(mapRequest, PARAM_RESTRICTED_PACKAGE_NAME, + message.getRestrictedPackageName()); + setJsonField(mapRequest, PARAM_DELAY_WHILE_IDLE, + message.isDelayWhileIdle()); + setJsonField(mapRequest, PARAM_DRY_RUN, message.isDryRun()); + Map<String, String> payload = message.getData(); + if (!payload.isEmpty()) { + mapRequest.put(JSON_PAYLOAD, payload); + } + if (message.getNotification() != null) { + Notification notification = message.getNotification(); + Map<Object, Object> nMap = new HashMap<>(); + if (notification.getBadge() != null) { + setJsonField(nMap, JSON_NOTIFICATION_BADGE, notification + .getBadge().toString()); + } + setJsonField(nMap, JSON_NOTIFICATION_BODY, notification.getBody()); + setJsonField(nMap, JSON_NOTIFICATION_BODY_LOC_ARGS, + notification.getBodyLocArgs()); + setJsonField(nMap, JSON_NOTIFICATION_BODY_LOC_KEY, + notification.getBodyLocKey()); + setJsonField(nMap, JSON_NOTIFICATION_CLICK_ACTION, + notification.getClickAction()); + setJsonField(nMap, JSON_NOTIFICATION_COLOR, notification.getColor()); + setJsonField(nMap, JSON_NOTIFICATION_ICON, notification.getIcon()); + setJsonField(nMap, JSON_NOTIFICATION_SOUND, notification.getSound()); + setJsonField(nMap, JSON_NOTIFICATION_TAG, notification.getTag()); + setJsonField(nMap, JSON_NOTIFICATION_TITLE, notification.getTitle()); + setJsonField(nMap, JSON_NOTIFICATION_TITLE_LOC_ARGS, + notification.getTitleLocArgs()); + setJsonField(nMap, JSON_NOTIFICATION_TITLE_LOC_KEY, + notification.getTitleLocKey()); + mapRequest.put(JSON_NOTIFICATION, nMap); + } + } + + private IOException newIoException(String responseBody, Exception e) { + // log exception, as IOException constructor that takes a message and + // cause + // is only available on Java 6 + String msg = "Error parsing JSON response (" + responseBody + ")"; + logger.log(Level.WARNING, msg, e); + return new IOException(msg + ":" + e); + } + + private static void close(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + // ignore error + logger.log(Level.FINEST, "IOException closing stream", e); + } + } + } + + /** + * Sets a JSON field, but only if the value is not {@literal null}. + */ + private void setJsonField(Map<Object, Object> json, String field, + Object value) { + if (value != null) { + json.put(field, value); + } + } + + private Number getNumber(Map<?, ?> json, String field) { + Object value = json.get(field); + if (value == null) { + throw new CustomParserException("Missing field: " + field); + } + if (!(value instanceof Number)) { + throw new CustomParserException("Field " + field + + " does not contain a number: " + value); + } + return (Number) value; + } + + class CustomParserException extends RuntimeException { + CustomParserException(String message) { + super(message); + } + } + + /** + * Make an HTTP post to a given URL. + * + * @return HTTP response. + */ + protected HttpURLConnection post(String url, String body) + throws IOException { + return post(url, "application/x-www-form-urlencoded;charset=UTF-8", + body); + } + + /** + * Makes an HTTP POST request to a given endpoint. + * + * <p> + * <strong>Note: </strong> the returned connected should not be + * disconnected, otherwise it would kill persistent connections made using + * Keep-Alive. + * + * @param url + * endpoint to post the request. + * @param contentType + * type of request. + * @param body + * body of the request. + * + * @return the underlying connection. + * + * @throws IOException + * propagated from underlying methods. + */ + protected HttpURLConnection post(String url, String contentType, String body) + throws IOException { + if (url == null || contentType == null || body == null) { + throw new IllegalArgumentException("arguments cannot be null"); + } + if (!url.startsWith("https://")) { + logger.warning("URL does not use https: " + url); + } + logger.fine("Sending POST to " + url); + logger.finest("POST body: " + body); + byte[] bytes = body.getBytes(UTF8); + HttpURLConnection conn = getConnection(url); + conn.setDoOutput(true); + conn.setUseCaches(false); + conn.setFixedLengthStreamingMode(bytes.length); + conn.setRequestMethod("POST"); + conn.setRequestProperty("Content-Type", contentType); + conn.setRequestProperty("Authorization", "key=" + key); + OutputStream out = conn.getOutputStream(); + try { + out.write(bytes); + } finally { + close(out); + } + return conn; + } + + /** + * Creates a map with just one key-value pair. + */ + protected static final Map<String, String> newKeyValues(String key, + String value) { + Map<String, String> keyValues = new HashMap<>(1); + keyValues.put(nonNull(key), nonNull(value)); + return keyValues; + } + + /** + * Creates a {@link StringBuilder} to be used as the body of an HTTP POST. + * + * @param name + * initial parameter for the POST. + * @param value + * initial value for that parameter. + * @return StringBuilder to be used an HTTP POST body. + */ + protected static StringBuilder newBody(String name, String value) { + return new StringBuilder(nonNull(name)).append('=').append( + nonNull(value)); + } + + /** + * Adds a new parameter to the HTTP POST body. + * + * @param body + * HTTP POST body. + * @param name + * parameter's name. + * @param value + * parameter's value. + */ + protected static void addParameter(StringBuilder body, String name, + String value) { + nonNull(body).append('&').append(nonNull(name)).append('=') + .append(nonNull(value)); + } + + /** + * Gets an {@link HttpURLConnection} given an URL. + */ + protected HttpURLConnection getConnection(String url) throws IOException { + HttpURLConnection conn = (HttpURLConnection) new URL(url) + .openConnection(); + conn.setConnectTimeout(connectTimeout); + conn.setReadTimeout(readTimeout); + return conn; + } + + /** + * Convenience method to convert an InputStream to a String. + * <p> + * If the stream ends in a newline character, it will be stripped. + * <p> + * If the stream is {@literal null}, returns an empty string. + */ + protected static String getString(InputStream stream) throws IOException { + if (stream == null) { + return ""; + } + BufferedReader reader = new BufferedReader( + new InputStreamReader(stream)); + StringBuilder content = new StringBuilder(); + String newLine; + do { + newLine = reader.readLine(); + if (newLine != null) { + content.append(newLine).append('\n'); + } + } while (newLine != null); + if (content.length() > 0) { + // strip last newline + content.setLength(content.length() - 1); + } + return content.toString(); + } + + private static String getAndClose(InputStream stream) throws IOException { + try { + return getString(stream); + } finally { + if (stream != null) { + close(stream); + } + } + } + + static <T> T nonNull(T argument) { + if (argument == null) { + throw new IllegalArgumentException("argument cannot be null"); + } + return argument; + } + + void sleep(long millis) { + try { + Thread.sleep(millis); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + } + } +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/DeviceRegistrationNotFoundException.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/DeviceRegistrationNotFoundException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/DeviceRegistrationNotFoundException.java new file mode 100644 index 0000000..cc730a0 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/DeviceRegistrationNotFoundException.java @@ -0,0 +1,38 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformResourceNotFoundException; + +public class DeviceRegistrationNotFoundException extends + AbstractPlatformResourceNotFoundException { + + public DeviceRegistrationNotFoundException(final Long id) { + super("error.msg.device.registration.id.invalid", + "Device registration with identifier " + id + " does not exist", + id); + } + + public DeviceRegistrationNotFoundException(final Long clientId, String value) { + super("error.msg.device.registration." + value + ".invalid", + "Device registration with " + value + " identifier " + clientId + + " does not exist", clientId); + } + +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/InvalidRequestException.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/InvalidRequestException.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/InvalidRequestException.java new file mode 100644 index 0000000..2194b43 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/InvalidRequestException.java @@ -0,0 +1,66 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.exception; + +import java.io.IOException; + +/** + * Exception thrown when GCM returned an error due to an invalid request. + * <p> + * This is equivalent to GCM posts that return an HTTP error different of 200. + */ +public final class InvalidRequestException extends IOException { + + private final int status; + private final String description; + + public InvalidRequestException(int status) { + this(status, null); + } + + public InvalidRequestException(int status, String description) { + super(getMessage(status, description)); + this.status = status; + this.description = description; + } + + private static String getMessage(int status, String description) { + StringBuilder base = new StringBuilder("HTTP Status Code: ") + .append(status); + if (description != null) { + base.append("(").append(description).append(")"); + } + return base.toString(); + } + + /** + * Gets the HTTP Status Code. + */ + public int getHttpStatusCode() { + return status; + } + + /** + * Gets the error description. + */ + public String getDescription() { + return description; + } + +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformService.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformService.java new file mode 100644 index 0000000..1b4ec42 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformService.java @@ -0,0 +1,32 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.service; + +import java.util.Collection; + +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationData; + +public interface DeviceRegistrationReadPlatformService { + + Collection<DeviceRegistrationData> retrieveAllDeviceRegiistrations(); + + DeviceRegistrationData retrieveDeviceRegiistration(Long id); + + DeviceRegistrationData retrieveDeviceRegiistrationByClientId(Long clientId); +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformServiceImpl.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformServiceImpl.java new file mode 100644 index 0000000..8c64fe3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformServiceImpl.java @@ -0,0 +1,125 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.service; + +import java.sql.ResultSet; +import java.sql.SQLException; +import java.util.Collection; + +import org.apache.fineract.infrastructure.core.domain.JdbcSupport; +import org.apache.fineract.infrastructure.core.service.RoutingDataSource; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationData; +import org.apache.fineract.infrastructure.gcm.exception.DeviceRegistrationNotFoundException; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.client.data.ClientData; +import org.joda.time.LocalDate; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.EmptyResultDataAccessException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Service; + +@Service +public class DeviceRegistrationReadPlatformServiceImpl implements + DeviceRegistrationReadPlatformService { + + private final JdbcTemplate jdbcTemplate; + private final PlatformSecurityContext context; + + @Autowired + public DeviceRegistrationReadPlatformServiceImpl( + final PlatformSecurityContext context, + final RoutingDataSource dataSource) { + this.context = context; + this.jdbcTemplate = new JdbcTemplate(dataSource); + } + + private static final class DeviceRegistrationDataMapper implements + RowMapper<DeviceRegistrationData> { + + private final String schema; + + public DeviceRegistrationDataMapper() { + final StringBuilder sqlBuilder = new StringBuilder(200); + sqlBuilder + .append(" cdr.id as id, cdr.registration_id as registrationId, cdr.updatedon_date as updatedOnDate, "); + sqlBuilder + .append(" c.id as clientId, c.display_name as clientName "); + sqlBuilder.append(" from client_device_registration cdr "); + sqlBuilder.append(" left join m_client c on c.id = cdr.client_id "); + this.schema = sqlBuilder.toString(); + } + + public String schema() { + return this.schema; + } + + @Override + public DeviceRegistrationData mapRow(final ResultSet rs, + @SuppressWarnings("unused") final int rowNum) + throws SQLException { + + final Long id = JdbcSupport.getLong(rs, "id"); + final LocalDate updatedOnDate = JdbcSupport.getLocalDate(rs, + "updatedOnDate"); + final String registrationId = rs.getString("registrationId"); + final Long clientId = rs.getLong("clientId"); + final String clientName = rs.getString("clientName"); + ClientData clientData = ClientData.instance(clientId, clientName); + return DeviceRegistrationData.instance(id, clientData, + registrationId, updatedOnDate.toDate()); + } + } + + @Override + public Collection<DeviceRegistrationData> retrieveAllDeviceRegiistrations() { + this.context.authenticatedUser(); + DeviceRegistrationDataMapper drm = new DeviceRegistrationDataMapper(); + String sql = "select " + drm.schema(); + return this.jdbcTemplate.query(sql, drm, new Object[] {}); + } + + @Override + public DeviceRegistrationData retrieveDeviceRegiistration(Long id) { + try { + this.context.authenticatedUser(); + DeviceRegistrationDataMapper drm = new DeviceRegistrationDataMapper(); + String sql = "select " + drm.schema() + " where cdr.id = ? "; + return this.jdbcTemplate.queryForObject(sql, drm, + new Object[] { id }); + } catch (final EmptyResultDataAccessException e) { + throw new DeviceRegistrationNotFoundException(id); + } + } + + @Override + public DeviceRegistrationData retrieveDeviceRegiistrationByClientId( + Long clientId) { + try { + this.context.authenticatedUser(); + DeviceRegistrationDataMapper drm = new DeviceRegistrationDataMapper(); + String sql = "select " + drm.schema() + " where c.id = ? "; + return this.jdbcTemplate.queryForObject(sql, drm, + new Object[] { clientId }); + } catch (final EmptyResultDataAccessException e) { + throw new DeviceRegistrationNotFoundException(clientId, "client"); + } + } + +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformService.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformService.java new file mode 100644 index 0000000..2391fdd --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformService.java @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.service; + +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration; + +public interface DeviceRegistrationWritePlatformService { + + public DeviceRegistration registerDevice(final Long clientId, final String registrationId); + + public DeviceRegistration updateDeviceRegistration(final Long id, final Long clientId, final String registrationId); + + public void deleteDeviceRegistration(final Long id); +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformServiceImpl.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformServiceImpl.java new file mode 100644 index 0000000..52653ea --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformServiceImpl.java @@ -0,0 +1,123 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.service; + +import javax.persistence.PersistenceException; + +import org.apache.commons.lang.exception.ExceptionUtils; +import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationRepositoryWrapper; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.apache.fineract.portfolio.client.domain.Client; +import org.apache.fineract.portfolio.client.domain.ClientRepositoryWrapper; +import org.apache.openjpa.persistence.EntityExistsException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class DeviceRegistrationWritePlatformServiceImpl implements + DeviceRegistrationWritePlatformService { + + private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository; + private final ClientRepositoryWrapper clientRepositoryWrapper; + private final PlatformSecurityContext context; + + @Autowired + public DeviceRegistrationWritePlatformServiceImpl( + final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository, + final ClientRepositoryWrapper clientRepositoryWrapper, + final PlatformSecurityContext context) { + this.deviceRegistrationRepository = deviceRegistrationRepository; + this.clientRepositoryWrapper = clientRepositoryWrapper; + this.context = context; + } + + @Transactional + @Override + public DeviceRegistration registerDevice(Long clientId, + String registrationId) { + this.context.authenticatedUser(); + Client client = this.clientRepositoryWrapper + .findOneWithNotFoundDetection(clientId); + try { + DeviceRegistration deviceRegistration = DeviceRegistration + .instance(client, registrationId); + this.deviceRegistrationRepository.save(deviceRegistration); + return deviceRegistration; + } catch (final EntityExistsException dve) { + handleDataIntegrityIssues(registrationId, dve, dve); + return null; + } catch (final DataIntegrityViolationException dve) { + handleDataIntegrityIssues(registrationId, + dve.getMostSpecificCause(), dve); + return null; + } catch (final PersistenceException dve) { + Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); + handleDataIntegrityIssues(registrationId, throwable, dve); + return null; + } catch (final Exception dve) { + Throwable throwable = ExceptionUtils.getRootCause(dve.getCause()); + handleDataIntegrityIssues(registrationId, throwable, dve); + return null; + } + + } + + private void handleDataIntegrityIssues(final String registrationId, + final Throwable realCause, + @SuppressWarnings("unused") final Exception dve) { + + if (realCause.getMessage().contains("registration_key")) { + throw new PlatformDataIntegrityException( + "error.msg.duplicate.device.registration.id", + "Registration id : " + registrationId + " already exist.", + "name", registrationId); + } + + throw new PlatformDataIntegrityException( + "error.msg.charge.unknown.data.integrity.issue", + "Unknown data integrity issue with resource: " + + realCause.getMessage()); + } + + @Override + public DeviceRegistration updateDeviceRegistration(Long id, Long clientId, + String registrationId) { + DeviceRegistration deviceRegistration = this.deviceRegistrationRepository + .findOneWithNotFoundDetection(id); + Client client = this.clientRepositoryWrapper + .findOneWithNotFoundDetection(clientId); + deviceRegistration.setClient(client); + deviceRegistration.setRegistrationId(registrationId); + deviceRegistration.setUpdatedOnDate(DateUtils + .getLocalDateTimeOfTenant().toDate()); + return deviceRegistration; + } + + @Override + public void deleteDeviceRegistration(Long id) { + DeviceRegistration deviceRegistration = this.deviceRegistrationRepository.findOneWithNotFoundDetection(id); + this.deviceRegistrationRepository.delete(deviceRegistration); + } + +} http://git-wip-us.apache.org/repos/asf/fineract/blob/8f30c210/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/NotificationSenderService.java ---------------------------------------------------------------------- diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/NotificationSenderService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/NotificationSenderService.java new file mode 100644 index 0000000..67fb072 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/NotificationSenderService.java @@ -0,0 +1,133 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.infrastructure.gcm.service; + +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.apache.fineract.infrastructure.configuration.service.ExternalServicesPropertiesReadPlatformService; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.gcm.GcmConstants; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationRepositoryWrapper; +import org.apache.fineract.infrastructure.gcm.domain.Message; +import org.apache.fineract.infrastructure.gcm.domain.Message.Builder; +import org.apache.fineract.infrastructure.gcm.domain.Message.Priority; +import org.apache.fineract.infrastructure.gcm.domain.Notification; +import org.apache.fineract.infrastructure.gcm.domain.NotificationConfigurationData; +import org.apache.fineract.infrastructure.gcm.domain.Result; +import org.apache.fineract.infrastructure.gcm.domain.Sender; +import org.apache.fineract.infrastructure.sms.domain.SmsMessage; +import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository; +import org.apache.fineract.infrastructure.sms.domain.SmsMessageStatusType; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class NotificationSenderService { + + private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepositoryWrapper; + private final SmsMessageRepository smsMessageRepository; + private ExternalServicesPropertiesReadPlatformService propertiesReadPlatformService; + + @Autowired + public NotificationSenderService( + final DeviceRegistrationRepositoryWrapper deviceRegistrationRepositoryWrapper, + final SmsMessageRepository smsMessageRepository, final ExternalServicesPropertiesReadPlatformService propertiesReadPlatformService) { + this.deviceRegistrationRepositoryWrapper = deviceRegistrationRepositoryWrapper; + this.smsMessageRepository = smsMessageRepository; + this.propertiesReadPlatformService = propertiesReadPlatformService; + } + + public void sendNotification(List<SmsMessage> smsMessages) { + Map<Long, List<SmsMessage>> notificationByEachClient = getNotificationListByClient(smsMessages); + for (Map.Entry<Long, List<SmsMessage>> entry : notificationByEachClient + .entrySet()) { + this.sendNotifiaction(entry.getKey(), entry.getValue()); + } + } + + public Map<Long, List<SmsMessage>> getNotificationListByClient( + List<SmsMessage> smsMessages) { + Map<Long, List<SmsMessage>> notificationByEachClient = new HashMap<>(); + for (SmsMessage smsMessage : smsMessages) { + if (smsMessage.getClient() != null) { + Long clientId = smsMessage.getClient().getId(); + if (notificationByEachClient.containsKey(clientId)) { + notificationByEachClient.get(clientId).add(smsMessage); + } else { + List<SmsMessage> msgList = new ArrayList<>( + Arrays.asList(smsMessage)); + notificationByEachClient.put(clientId, msgList); + } + + } + } + return notificationByEachClient; + } + + public void sendNotifiaction(Long clientId, List<SmsMessage> smsList) { + + DeviceRegistration deviceRegistration = this.deviceRegistrationRepositoryWrapper + .findDeviceRegistrationByClientId(clientId); + NotificationConfigurationData notificationConfigurationData = this.propertiesReadPlatformService.getNotificationConfiguration(); + String registrationId = null; + if (deviceRegistration != null) { + registrationId = deviceRegistration.getRegistrationId(); + } + for (SmsMessage smsMessage : smsList) { + try { + Notification notification = new Notification.Builder( + GcmConstants.defaultIcon).title(GcmConstants.title) + .body(smsMessage.getMessage()).build(); + Builder b = new Builder(); + b.notification(notification); + b.dryRun(false); + b.contentAvailable(true); + b.timeToLive(GcmConstants.TIME_TO_LIVE); + b.priority(Priority.HIGH); + b.delayWhileIdle(true); + Message msg = b.build(); + Sender s = new Sender(notificationConfigurationData.getServerKey(),notificationConfigurationData.getFcmEndPoint()); + Result res; + + res = s.send(msg, registrationId, 3); + if (res.getSuccess() != null && res.getSuccess()>0) { + smsMessage.setStatusType(SmsMessageStatusType.SENT + .getValue()); + smsMessage.setDeliveredOnDate(DateUtils.getLocalDateOfTenant().toDate()); + } else if (res.getFailure() != null && res.getFailure()>0) { + smsMessage.setStatusType(SmsMessageStatusType.FAILED + .getValue()); + } + } catch (IOException e) { + smsMessage + .setStatusType(SmsMessageStatusType.FAILED.getValue()); + } + } + + this.smsMessageRepository.save(smsList); + + } + +}