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);
+
+       }
+
+}

Reply via email to