From 8f30c2102f9f3526ac274f48e11ab2a5f91a86ff Mon Sep 17 00:00:00 2001 From: nazeer shaik Date: Thu, 2 Nov 2017 16:24:28 +0530 Subject: [PATCH] notification sms --- .../campaigns/constants/CampaignType.java | 12 +- .../constants/SmsCampaignEnumerations.java | 3 + .../campaigns/sms/data/SmsCampaignData.java | 16 +- .../campaigns/sms/domain/SmsCampaign.java | 36 +- .../sms/domain/SmsCampaignRepository.java | 2 + .../serialization/SmsCampaignValidator.java | 38 +- .../service/SmsCampaignDomainServiceImpl.java | 23 +- ...mpaignDropdownReadPlatformServiceImpl.java | 3 - .../SmsCampaignReadPlatformServiceImpl.java | 7 +- ...msCampaignWritePlatformServiceJpaImpl.java | 49 +- ...pertiesCommandFromApiJsonDeserializer.java | 6 + .../service/ExternalServicesConstants.java | 36 + ...ServicesPropertiesReadPlatformService.java | 3 + ...icesPropertiesReadPlatformServiceImpl.java | 35 + ...ternalServicesReadPlatformServiceImpl.java | 4 + .../infrastructure/gcm/GcmConstants.java | 242 +++++ .../api/DeviceRegistrationApiConstants.java | 25 + .../api/DeviceRegistrationApiResource.java | 160 ++++ .../gcm/domain/DeviceRegistration.java | 85 ++ .../gcm/domain/DeviceRegistrationData.java | 47 + .../domain/DeviceRegistrationRepository.java | 36 + .../DeviceRegistrationRepositoryWrapper.java | 61 ++ .../infrastructure/gcm/domain/Message.java | 317 +++++++ .../gcm/domain/MulticastResult.java | 151 ++++ .../gcm/domain/Notification.java | 330 +++++++ .../domain/NotificationConfigurationData.java | 49 ++ .../infrastructure/gcm/domain/Result.java | 187 ++++ .../infrastructure/gcm/domain/Sender.java | 832 ++++++++++++++++++ .../DeviceRegistrationNotFoundException.java | 38 + .../exception/InvalidRequestException.java | 66 ++ ...DeviceRegistrationReadPlatformService.java | 32 + ...ceRegistrationReadPlatformServiceImpl.java | 125 +++ ...eviceRegistrationWritePlatformService.java | 30 + ...eRegistrationWritePlatformServiceImpl.java | 123 +++ .../service/NotificationSenderService.java | 133 +++ .../infrastructure/sms/domain/SmsMessage.java | 32 +- .../sms/domain/SmsMessageAssembler.java | 4 +- .../SmsMessageScheduledJobServiceImpl.java | 36 +- .../portfolio/client/data/ClientData.java | 6 + ...eRegistrationWritePlatformServiceImpl.java | 3 +- .../V336__sms_campaign_notification.sql | 48 + 41 files changed, 3409 insertions(+), 62 deletions(-) create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/GcmConstants.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiConstants.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiResource.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistration.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepository.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepositoryWrapper.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Message.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/MulticastResult.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Notification.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/NotificationConfigurationData.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Result.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/Sender.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/DeviceRegistrationNotFoundException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/exception/InvalidRequestException.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationReadPlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/DeviceRegistrationWritePlatformServiceImpl.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/service/NotificationSenderService.java create mode 100644 fineract-provider/src/main/resources/sql/migrations/core_db/V336__sms_campaign_notification.sql diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/constants/CampaignType.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/constants/CampaignType.java index b6a0aaf7c..421882a34 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/constants/CampaignType.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/constants/CampaignType.java @@ -21,7 +21,7 @@ package org.apache.fineract.infrastructure.campaigns.constants; import org.apache.fineract.infrastructure.core.data.EnumOptionData; public enum CampaignType { - INVALID(0, "campaignType.invalid"), SMS(1, "campaignType.sms"); + INVALID(0, "campaignType.invalid"), SMS(1, "campaignType.sms"), NOTIFICATION(2, "campaignType.notification"); private Integer value; private String code; @@ -48,6 +48,9 @@ public enum CampaignType { case 1: type = SMS; break; + case 2: + type = NOTIFICATION; + break; } return type; } @@ -66,6 +69,9 @@ public enum CampaignType { case SMS: optionData = new EnumOptionData(CampaignType.SMS.getValue().longValue(), CampaignType.SMS.getCode(), "SMS"); break; + case NOTIFICATION: + optionData = new EnumOptionData(CampaignType.NOTIFICATION.getValue().longValue(), CampaignType.NOTIFICATION.getCode(), "NOTIFICATION"); + break; } return optionData; } @@ -73,4 +79,8 @@ public enum CampaignType { public boolean isSms() { return this.value.equals(CampaignType.SMS.getValue()); } + + public boolean isNotificaion() { + return this.value.equals(CampaignType.NOTIFICATION.getValue()); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/constants/SmsCampaignEnumerations.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/constants/SmsCampaignEnumerations.java index 99de55ecc..6e74932e9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/constants/SmsCampaignEnumerations.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/constants/SmsCampaignEnumerations.java @@ -61,6 +61,9 @@ public class SmsCampaignEnumerations { case SMS: optionData = new EnumOptionData(CampaignType.SMS.getValue().longValue(), CampaignType.SMS.getCode(), "SMS"); break; + case NOTIFICATION: + optionData = new EnumOptionData(CampaignType.NOTIFICATION.getValue().longValue(), CampaignType.NOTIFICATION.getCode(), "NOTIFICATION"); + break; } return optionData; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/data/SmsCampaignData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/data/SmsCampaignData.java index 8a6e6a310..cbd9bd550 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/data/SmsCampaignData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/data/SmsCampaignData.java @@ -44,6 +44,8 @@ public class SmsCampaignData { private final DateTime recurrenceStartDate; private final String recurrence; private final Long providerId; + private final boolean isNotification; + private final Collection smsProviderOptions; @@ -70,7 +72,7 @@ public class SmsCampaignData { final Collection smsProviderOptions, final Collection campaignTypeOptions, final Collection triggerTypeOptions, final Collection months, final Collection weekDays, final Collection frequencyTypeOptions, - final Collection periodFrequencyOptions) { + final Collection periodFrequencyOptions, final boolean isNotification) { this.id = id; this.campaignName = campaignName; this.campaignType = campaignType; @@ -90,6 +92,7 @@ public class SmsCampaignData { } else { this.lastTriggerDate = null; } + this.isNotification = isNotification; this.smsCampaignTimeLine = smsCampaignTimeLine; this.recurrenceStartDate = recurrenceStartDate; this.recurrence = recurrence; @@ -109,7 +112,7 @@ public class SmsCampaignData { final Long runReportId, final String reportName, final String paramValue, final EnumOptionData campaignStatus, final String message, final DateTime nextTriggerDate, final LocalDate lastTriggerDate, final SmsCampaignTimeLine smsCampaignTimeLine, final DateTime recurrenceStartDate, final String recurrence, - final Long providerId) { + final Long providerId, final boolean isNotification) { final Collection businessRulesOptions = null; final Collection smsProviderOptions = null; final Collection campaignTypeOptions = null; @@ -122,7 +125,7 @@ public class SmsCampaignData { return new SmsCampaignData(id, campaignName, campaignType, triggerType, runReportId, reportName, paramValue, campaignStatus, message, nextTriggerDate, lastTriggerDate, smsCampaignTimeLine, recurrenceStartDate, recurrence, providerId, businessRulesOptions, smsProviderOptions, campaignTypeOptions, - triggerTypeOptions, months, weekDays, frequencyTypeOptions, periodFrequencyOptions); + triggerTypeOptions, months, weekDays, frequencyTypeOptions, periodFrequencyOptions, isNotification); } public static SmsCampaignData template(final Collection smsProviderOptions, @@ -145,10 +148,11 @@ public class SmsCampaignData { final EnumOptionData triggerType = null; final String reportName = null; final Long providerId = null; + final boolean isNotification = false; return new SmsCampaignData(id, campaignName, campaignType, triggerType, runReportId, reportName, paramValue, campaignStatus, message, nextTriggerDate, lastTriggerDate, smsCampaignTimeLine, recurrenceStartDate, recurrence, providerId, businessRulesOptions, smsProviderOptions, campaignTypeOptions, - triggerTypeOptions, months, weekDays, frequencyTypeOptions, periodFrequencyOptions); + triggerTypeOptions, months, weekDays, frequencyTypeOptions, periodFrequencyOptions, isNotification); } public Long getId() { @@ -203,4 +207,8 @@ public class SmsCampaignData { return this.providerId; } + public boolean isNotification() { + return this.isNotification; + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaign.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaign.java index 3e1610e89..1c023e441 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaign.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaign.java @@ -67,7 +67,7 @@ public class SmsCampaign extends AbstractPersistableCustom { @Column(name = "campaign_trigger_type", nullable = false) private Integer triggerType; //defines direct, scheduled, transaction - @Column(name = "provider_id", nullable = false) + @Column(name = "provider_id", nullable = true)//null for notifications private Long providerId; // defined provider details @ManyToOne @@ -124,13 +124,16 @@ public class SmsCampaign extends AbstractPersistableCustom { @Column(name = "is_visible", nullable = true) private boolean isVisible; + + @Column(name = "is_notification", nullable = true) + private boolean isNotification; public SmsCampaign() {} private SmsCampaign(final String campaignName, final Integer campaignType, final Integer triggerType, final Report businessRuleId, final Long providerId, final String paramValue, final String message, final LocalDate submittedOnDate, final AppUser submittedBy, final String recurrence, - final LocalDateTime localDateTime) { + final LocalDateTime localDateTime, final boolean isNotification) { this.campaignName = campaignName; this.campaignType = campaignType; this.triggerType = SmsCampaignTriggerType.fromInt(triggerType).getValue(); @@ -149,6 +152,7 @@ public class SmsCampaign extends AbstractPersistableCustom { } else { this.recurrenceStartDate = recurrenceStartDate.toDate(); } + this.isNotification = isNotification; } public static SmsCampaign instance(final AppUser submittedBy, final Report report, final JsonCommand command) { @@ -156,7 +160,15 @@ public class SmsCampaign extends AbstractPersistableCustom { final String campaignName = command.stringValueOfParameterNamed(SmsCampaignValidator.campaignName); final Long campaignType = command.longValueOfParameterNamed(SmsCampaignValidator.campaignType); final Long triggerType = command.longValueOfParameterNamed(SmsCampaignValidator.triggerType); - final Long providerId = command.longValueOfParameterNamed(SmsCampaignValidator.providerId); + boolean isNotification = false; + if(command.parameterExists(SmsCampaignValidator.isNotificationParamName)){ + isNotification = command.booleanPrimitiveValueOfParameterNamed(SmsCampaignValidator.isNotificationParamName); + } + Long providerId = null; + if(!isNotification){ + providerId = command.longValueOfParameterNamed(SmsCampaignValidator.providerId); + } + final String paramValue = command.jsonFragment(SmsCampaignValidator.paramValue); final String message = command.stringValueOfParameterNamed(SmsCampaignValidator.message); @@ -165,6 +177,7 @@ public class SmsCampaign extends AbstractPersistableCustom { submittedOnDate = command.localDateValueOfParameterNamed(SmsCampaignValidator.submittedOnDateParamName); } String recurrence = null; + LocalDateTime recurrenceStartDate = new LocalDateTime(); if (SmsCampaignTriggerType.fromInt(triggerType.intValue()).isSchedule()) { @@ -184,7 +197,7 @@ public class SmsCampaign extends AbstractPersistableCustom { } return new SmsCampaign(campaignName, campaignType.intValue(), triggerType.intValue(), report, - providerId, paramValue, message, submittedOnDate, submittedBy, recurrence, recurrenceStartDate); + providerId, paramValue, message, submittedOnDate, submittedBy, recurrence, recurrenceStartDate, isNotification); } public Map update(JsonCommand command) { @@ -232,6 +245,11 @@ public class SmsCampaign extends AbstractPersistableCustom { final Long newValue = command.longValueOfParameterNamed(SmsCampaignValidator.providerId); actualChanges.put(SmsCampaignValidator.providerId, newValue); } + if (command.isChangeInBooleanParameterNamed(SmsCampaignValidator.isNotificationParamName, this.isNotification)) { + final Boolean newValue = command.booleanObjectValueOfParameterNamed(SmsCampaignValidator.isNotificationParamName); + this.isNotification = newValue; + actualChanges.put(SmsCampaignValidator.isNotificationParamName, newValue); + } if (SmsCampaignTriggerType.fromInt(this.triggerType).isSchedule()) { final String dateFormatAsInput = command.dateFormat(); @@ -558,4 +576,14 @@ public class SmsCampaign extends AbstractPersistableCustom { } return recurrenceBuilder.toString(); } + + public boolean isNotification() { + return this.isNotification; + } + + public void setNotification(boolean isNotification) { + this.isNotification = isNotification; + } + + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaignRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaignRepository.java index bc3bb7d08..cb1cf1c9f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaignRepository.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/domain/SmsCampaignRepository.java @@ -32,6 +32,8 @@ public interface SmsCampaignRepository extends JpaRepository, Collection findByCampaignTypeAndTriggerTypeAndStatus(final Integer campaignType, final Integer triggerType, final Integer status); + + Collection findByTriggerTypeAndStatus(final Integer triggerType, final Integer status); Collection findByTriggerType(final Integer triggerType) ; diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/serialization/SmsCampaignValidator.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/serialization/SmsCampaignValidator.java index dcc9a77c3..d9eb56fd0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/serialization/SmsCampaignValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/serialization/SmsCampaignValidator.java @@ -28,12 +28,16 @@ import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTriggerType; +import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaign; import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationRepositoryWrapper; import org.apache.fineract.portfolio.calendar.domain.CalendarFrequencyType; +import org.apache.fineract.portfolio.client.domain.Client; import org.joda.time.LocalDate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; @@ -67,19 +71,21 @@ public class SmsCampaignValidator { public static final String frequencyParamName = "frequency"; public static final String intervalParamName = "interval"; public static final String repeatsOnDayParamName = "repeatsOnDay"; + public static final String isNotificationParamName = "isNotification"; private final FromJsonHelper fromApiJsonHelper; + private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository; protected static final Set supportedParams = new HashSet<>(Arrays.asList(campaignName, campaignType, localeParamName, dateFormatParamName, runReportId, paramValue, message, recurrenceStartDate, activationDateParamName, submittedOnDateParamName, closureDateParamName, recurrenceParamName, providerId, triggerType, frequencyParamName, intervalParamName, - repeatsOnDayParamName, triggerEntityType, triggerActionType, dateTimeFormat)); + repeatsOnDayParamName, triggerEntityType, triggerActionType, dateTimeFormat, isNotificationParamName)); protected static final Set supportedParamsForUpdate = new HashSet<>(Arrays.asList(campaignName, campaignType, localeParamName, dateFormatParamName, runReportId, paramValue, message, recurrenceStartDate, activationDateParamName, recurrenceParamName, - providerId, triggerType, triggerEntityType, triggerActionType, dateTimeFormat)); + providerId, triggerType, triggerEntityType, triggerActionType, dateTimeFormat, isNotificationParamName)); protected static final Set ACTIVATION_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(localeParamName, dateFormatParamName, @@ -92,8 +98,9 @@ public class SmsCampaignValidator { protected static final Set PREVIEW_REQUEST_DATA_PARAMETERS = new HashSet<>(Arrays.asList(paramValue, message)); @Autowired - public SmsCampaignValidator(FromJsonHelper fromApiJsonHelper) { + public SmsCampaignValidator(FromJsonHelper fromApiJsonHelper, final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository) { this.fromApiJsonHelper = fromApiJsonHelper; + this.deviceRegistrationRepository = deviceRegistrationRepository; } public void validateCreate(String json) { @@ -159,6 +166,12 @@ public class SmsCampaignValidator { element); baseDataValidator.reset().parameter(SmsCampaignValidator.submittedOnDateParamName).value(submittedOnDate).notNull(); } + + if (this.fromApiJsonHelper.parameterExists(SmsCampaignValidator.isNotificationParamName, element)) { + final Boolean isNotification = this.fromApiJsonHelper.extractBooleanNamed(SmsCampaignValidator.isNotificationParamName, + element); + baseDataValidator.reset().parameter(SmsCampaignValidator.submittedOnDateParamName).trueOrFalseRequired(isNotification); + } throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -214,7 +227,11 @@ public class SmsCampaignValidator { } } } - + if (this.fromApiJsonHelper.parameterExists(SmsCampaignValidator.isNotificationParamName, element)) { + final Boolean isNotification = this.fromApiJsonHelper.extractBooleanNamed(SmsCampaignValidator.isNotificationParamName, + element); + baseDataValidator.reset().parameter(SmsCampaignValidator.submittedOnDateParamName).trueOrFalseRequired(isNotification); + } throwExceptionIfValidationWarningsExist(dataValidationErrors); } @@ -303,4 +320,17 @@ public class SmsCampaignValidator { private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { throw new PlatformApiDataValidationException(dataValidationErrors); } } + + public boolean isValidNotificationOrSms(Client client, SmsCampaign smsCampaign, Object mobileNo) { + if (smsCampaign.isNotification()) { + if (client != null) { + DeviceRegistration deviceRegistration = this.deviceRegistrationRepository + .findDeviceRegistrationByClientId(client.getId()); + return (deviceRegistration != null); + } + return false; + } + return (mobileNo != null); + } + } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java index 3b0e0aef4..7bf79ad49 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDomainServiceImpl.java @@ -35,6 +35,7 @@ import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTri import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaign; import org.apache.fineract.infrastructure.campaigns.sms.domain.SmsCampaignRepository; import org.apache.fineract.infrastructure.campaigns.sms.exception.SmsRuntimeException; +import org.apache.fineract.infrastructure.campaigns.sms.serialization.SmsCampaignValidator; import org.apache.fineract.infrastructure.sms.domain.SmsMessage; import org.apache.fineract.infrastructure.sms.domain.SmsMessageRepository; import org.apache.fineract.infrastructure.sms.scheduler.SmsMessageScheduledJobService; @@ -77,13 +78,15 @@ public class SmsCampaignDomainServiceImpl implements SmsCampaignDomainService { private final GroupRepository groupRepository; private final SmsMessageScheduledJobService smsMessageScheduledJobService; + private final SmsCampaignValidator smsCampaignValidator; @Autowired public SmsCampaignDomainServiceImpl(final SmsCampaignRepository smsCampaignRepository, final SmsMessageRepository smsMessageRepository, final BusinessEventNotifierService businessEventNotifierService, final OfficeRepository officeRepository, final SmsCampaignWritePlatformService smsCampaignWritePlatformCommandHandler, final GroupRepository groupRepository, - final SmsMessageScheduledJobService smsMessageScheduledJobService){ + final SmsMessageScheduledJobService smsMessageScheduledJobService, + final SmsCampaignValidator smsCampaignValidator){ this.smsCampaignRepository = smsCampaignRepository; this.smsMessageRepository = smsMessageRepository; this.businessEventNotifierService = businessEventNotifierService; @@ -91,6 +94,7 @@ public class SmsCampaignDomainServiceImpl implements SmsCampaignDomainService { this.smsCampaignWritePlatformCommandHandler = smsCampaignWritePlatformCommandHandler; this.groupRepository = groupRepository; this.smsMessageScheduledJobService = smsMessageScheduledJobService ; + this.smsCampaignValidator = smsCampaignValidator; } @PostConstruct @@ -217,10 +221,13 @@ public class SmsCampaignDomainServiceImpl implements SmsCampaignDomainService { String message = this.smsCampaignWritePlatformCommandHandler.compileSmsTemplate( smsCampaign.getMessage(), smsCampaign.getCampaignName(), smsParams); Object mobileNo = smsParams.get("mobileNo"); - if (mobileNo != null) { + if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) { + String mobileNumber = null; + if(mobileNo != null){ + mobileNumber = mobileNo.toString(); + } SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, message, - mobileNo.toString(), smsCampaign); - this.smsMessageRepository.save(smsMessage); + mobileNumber, smsCampaign, smsCampaign.isNotification()); Collection messages = new ArrayList<>(); messages.add(smsMessage); Map> smsDataMap = new HashMap<>(); @@ -273,9 +280,13 @@ public class SmsCampaignDomainServiceImpl implements SmsCampaignDomainService { String message = this.smsCampaignWritePlatformCommandHandler .compileSmsTemplate(smsCampaign.getMessage(), smsCampaign.getCampaignName(), smsParams); Object mobileNo = smsParams.get("mobileNo"); - if (mobileNo != null) { + if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) { + String mobileNumber = null; + if(mobileNo != null){ + mobileNumber = mobileNo.toString(); + } SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, message, - mobileNo.toString(), smsCampaign); + mobileNumber, smsCampaign, smsCampaign.isNotification()); this.smsMessageRepository.save(smsMessage); Collection messages = new ArrayList<>(); messages.add(smsMessage); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDropdownReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDropdownReadPlatformServiceImpl.java index d974d7ab3..69536f89e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDropdownReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignDropdownReadPlatformServiceImpl.java @@ -30,7 +30,6 @@ import org.apache.fineract.infrastructure.campaigns.helper.SmsConfigUtils; import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignEnumerations; import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTriggerType; import org.apache.fineract.infrastructure.campaigns.sms.data.SmsProviderData; -import org.apache.fineract.infrastructure.campaigns.sms.exception.ConnectionFailureException; import org.apache.fineract.infrastructure.core.data.EnumOptionData; import org.apache.fineract.portfolio.calendar.domain.CalendarWeekDaysType; import org.apache.fineract.portfolio.calendar.service.CalendarEnumerations; @@ -81,10 +80,8 @@ public class SmsCampaignDropdownReadPlatformServiceImpl implements SmsCampaignDr new ParameterizedTypeReference>() {}); smsProviderOptions = responseOne.getBody(); if (!responseOne.getStatusCode().equals(HttpStatus.OK)) { - throw new ConnectionFailureException(hostName); } } catch (Exception e) { - throw new ConnectionFailureException(hostName); } return smsProviderOptions; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignReadPlatformServiceImpl.java index 94215b0a4..5e15272db 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignReadPlatformServiceImpl.java @@ -219,7 +219,8 @@ public class SmsCampaignReadPlatformServiceImpl implements SmsCampaignReadPlatfo sql.append("acu.username as activatedByUsername, "); sql.append("sc.approvedon_date as activatedOnDate, "); sql.append("sr.report_name as reportName, "); - sql.append("provider_id as providerId "); + sql.append("provider_id as providerId, "); + sql.append("sc.is_notification as isNotification "); sql.append("from sms_campaign sc "); sql.append("left join m_appuser sbu on sbu.id = sc.submittedon_userid "); sql.append("left join m_appuser acu on acu.id = sc.approvedon_userid "); @@ -265,9 +266,9 @@ public class SmsCampaignReadPlatformServiceImpl implements SmsCampaignReadPlatfo activatedByUsername, closedOnDate, closedByUsername); final String reportName = rs.getString("reportName"); final Long providerId = rs.getLong("providerId"); - + final Boolean isNotification = rs.getBoolean("isNotification"); return SmsCampaignData.instance(id, campaignName, campaignTypeEnum, triggerTypeEnum, runReportId, reportName, paramValue, status, message, nextTriggerDate, lastTriggerDate, - smsCampaignTimeLine, recurrenceStartDate, recurrence, providerId); + smsCampaignTimeLine, recurrenceStartDate, recurrence, providerId, isNotification); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignWritePlatformServiceJpaImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignWritePlatformServiceJpaImpl.java index 0084ed6f9..976a6d75b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignWritePlatformServiceJpaImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/campaigns/sms/service/SmsCampaignWritePlatformServiceJpaImpl.java @@ -30,7 +30,6 @@ import java.util.Locale; import java.util.Map; import java.util.Set; -import org.apache.fineract.infrastructure.campaigns.constants.CampaignType; import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignStatus; import org.apache.fineract.infrastructure.campaigns.sms.constants.SmsCampaignTriggerType; import org.apache.fineract.infrastructure.campaigns.sms.data.CampaignPreviewData; @@ -56,6 +55,8 @@ import org.apache.fineract.infrastructure.dataqueries.domain.ReportRepository; import org.apache.fineract.infrastructure.dataqueries.exception.ReportNotFoundException; import org.apache.fineract.infrastructure.dataqueries.service.GenericDataService; import org.apache.fineract.infrastructure.dataqueries.service.ReadReportingService; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationRepositoryWrapper; import org.apache.fineract.infrastructure.jobs.annotation.CronTarget; import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.infrastructure.jobs.service.JobName; @@ -109,6 +110,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP private final ReadReportingService readReportingService; private final GenericDataService genericDataService; private final FromJsonHelper fromJsonHelper; + private final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository; private final SmsMessageScheduledJobService smsMessageScheduledJobService; @@ -118,7 +120,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP final SmsMessageRepository smsMessageRepository, final ClientRepositoryWrapper clientRepositoryWrapper, final ReadReportingService readReportingService, final GenericDataService genericDataService, final FromJsonHelper fromJsonHelper, final GroupRepository groupRepository, - final SmsMessageScheduledJobService smsMessageScheduledJobService) { + final SmsMessageScheduledJobService smsMessageScheduledJobService, final DeviceRegistrationRepositoryWrapper deviceRegistrationRepository) { this.context = context; this.smsCampaignRepository = smsCampaignRepository; this.smsCampaignValidator = smsCampaignValidator; @@ -130,6 +132,7 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP this.fromJsonHelper = fromJsonHelper; this.groupRepository = groupRepository; this.smsMessageScheduledJobService = smsMessageScheduledJobService ; + this.deviceRegistrationRepository = deviceRegistrationRepository; } @Transactional @@ -228,10 +231,14 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP Object mobileNo = entry.get("mobileNo"); Client client = this.clientRepositoryWrapper.findOneWithNotFoundDetection(clientId.longValue()); - if (mobileNo != null) { + if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) { // String countryCode = this.smsReadPlatformService.retrieveCountryCode(client.getOffice().getId()).getCountryCode(); - SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, mobileNo.toString(), - smsCampaign); + String mobileNumber = null; + if(mobileNo != null){ + mobileNumber = mobileNo.toString(); + } + SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, mobileNumber, + smsCampaign, smsCampaign.isNotification()); this.smsMessageRepository.save(smsMessage); } } @@ -242,6 +249,8 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP } + + @Override public void insertDirectCampaignIntoSmsOutboundTable(final Loan loan, final SmsCampaign smsCampaign) { try { @@ -278,9 +287,13 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP String textMessage = this.compileSmsTemplate(smsCampaign.getMessage(), smsCampaign.getCampaignName(), entry); Object mobileNo = entry.get("mobileNo"); - if (mobileNo != null) { - SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, mobileNo.toString(), - smsCampaign); + if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) { + String mobileNumber = null; + if(mobileNo != null){ + mobileNumber = mobileNo.toString(); + } + SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, mobileNumber, + smsCampaign, smsCampaign.isNotification()); smsMessage.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue()); this.smsMessageRepository.save(smsMessage); Collection messages = new ArrayList<>() ; @@ -320,9 +333,13 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP smsCampaign.getCampaignName(), entry); Object mobileNo = entry.get("mobileNo"); - if (mobileNo != null) { + if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) { + String mobileNumber = null; + if(mobileNo != null){ + mobileNumber = mobileNo.toString(); + } SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, - mobileNo.toString(), smsCampaign); + mobileNumber, smsCampaign, smsCampaign.isNotification()); smsMessage.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue()); this.smsMessageRepository.save(smsMessage); Collection messages = new ArrayList<>(); @@ -360,9 +377,13 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP smsCampaign.getCampaignName(), entry); Object mobileNo = entry.get("mobileNo"); - if (mobileNo != null) { + if (this.smsCampaignValidator.isValidNotificationOrSms(client, smsCampaign, mobileNo)) { + String mobileNumber = null; + if(mobileNo != null){ + mobileNumber = mobileNo.toString(); + } SmsMessage smsMessage = SmsMessage.pendingSms(null, null, client, null, textMessage, - mobileNo.toString(), smsCampaign); + mobileNumber, smsCampaign, smsCampaign.isNotification()); smsMessage.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue()); this.smsMessageRepository.save(smsMessage); Collection messages = new ArrayList<>(); @@ -634,8 +655,8 @@ public class SmsCampaignWritePlatformServiceJpaImpl implements SmsCampaignWriteP @Override @CronTarget(jobName = JobName.UPDATE_SMS_OUTBOUND_WITH_CAMPAIGN_MESSAGE) public void storeTemplateMessageIntoSmsOutBoundTable() throws JobExecutionException { - final Collection smsCampaignDataCollection = this.smsCampaignRepository.findByCampaignTypeAndTriggerTypeAndStatus( - CampaignType.SMS.getValue(), SmsCampaignTriggerType.SCHEDULE.getValue(), SmsCampaignStatus.ACTIVE.getValue()); + final Collection smsCampaignDataCollection = this.smsCampaignRepository.findByTriggerTypeAndStatus( + SmsCampaignTriggerType.SCHEDULE.getValue(), SmsCampaignStatus.ACTIVE.getValue()); if (smsCampaignDataCollection != null) { for (SmsCampaign smsCampaign : smsCampaignDataCollection) { LocalDateTime tenantDateNow = tenantDateTime(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java index 9b5aeb2d5..3168ef489 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/serialization/ExternalServicesPropertiesCommandFromApiJsonDeserializer.java @@ -24,6 +24,7 @@ import java.util.Set; import org.apache.commons.lang.StringUtils; import org.apache.fineract.infrastructure.configuration.exception.ExternalServiceConfigurationNotFoundException; +import org.apache.fineract.infrastructure.configuration.service.ExternalServicesConstants.NOTIFICATION_JSON_INPUT_PARAMS; import org.apache.fineract.infrastructure.configuration.service.ExternalServicesConstants.S3_JSON_INPUT_PARAMS; import org.apache.fineract.infrastructure.configuration.service.ExternalServicesConstants.SMS_JSON_INPUT_PARAMS; import org.apache.fineract.infrastructure.configuration.service.ExternalServicesConstants.SMTP_JSON_INPUT_PARAMS; @@ -40,6 +41,7 @@ public class ExternalServicesPropertiesCommandFromApiJsonDeserializer { private final Set S3SupportedParameters = S3_JSON_INPUT_PARAMS.getAllValues(); private final Set SMTPSupportedParameters = SMTP_JSON_INPUT_PARAMS.getAllValues(); private final Set SMSSupportedParameters = SMS_JSON_INPUT_PARAMS.getAllValues(); + private final Set NotificationSupportedParameters = NOTIFICATION_JSON_INPUT_PARAMS.getAllValues(); private final FromJsonHelper fromApiJsonHelper; @Autowired @@ -64,6 +66,10 @@ public class ExternalServicesPropertiesCommandFromApiJsonDeserializer { this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.SMSSupportedParameters); break; + case "NOTIFICATION": + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, this.NotificationSupportedParameters); + break; + default: throw new ExternalServiceConfigurationNotFoundException(externalServiceName); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java index 44a56ca7e..f123c1923 100755 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesConstants.java @@ -41,6 +41,11 @@ public class ExternalServicesConstants { public static final String SMS_END_POINT = "end_point"; public static final String SMS_TENANT_APP_KEY = "tenant_app_key"; + public static final String NOTIFICATION_SERVICE_NAME = "NOTIFICATION"; + public static final String NOTIFICATION_SERVER_KEY = "server_key"; + public static final String NOTIFICATION_GCM_END_POINT = "gcm_end_point"; + public static final String NOTIFICATION_FCM_END_POINT = "fcm_end_point"; + public static enum EXTERNALSERVICEPROPERTIES_JSON_INPUT_PARAMS { EXTERNAL_SERVICE_ID("external_service_id"), NAME("name"), VALUE("value"); @@ -164,5 +169,36 @@ public class ExternalServicesConstants { return this.value; } } + + public static enum NOTIFICATION_JSON_INPUT_PARAMS { + SERVER_KEY("server_key"), GCM_END_POINT("gcm_end_point"), FCM_END_POINT("fcm_end_point"); + + private final String value; + + private NOTIFICATION_JSON_INPUT_PARAMS(final String value) { + this.value = value; + } + + private static final Set values = new HashSet<>(); + + static { + for (final NOTIFICATION_JSON_INPUT_PARAMS type : NOTIFICATION_JSON_INPUT_PARAMS.values()) { + values.add(type.value); + } + } + + public static Set getAllValues() { + return values; + } + + @Override + public String toString() { + return name().toString().replaceAll("_", " "); + } + + public String getValue() { + return this.value; + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformService.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformService.java index 2d4683570..25f4b8e76 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformService.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformService.java @@ -24,6 +24,7 @@ import org.apache.fineract.infrastructure.campaigns.sms.data.MessageGatewayConfi import org.apache.fineract.infrastructure.configuration.data.ExternalServicesPropertiesData; import org.apache.fineract.infrastructure.configuration.data.S3CredentialsData; import org.apache.fineract.infrastructure.configuration.data.SMTPCredentialsData; +import org.apache.fineract.infrastructure.gcm.domain.NotificationConfigurationData; public interface ExternalServicesPropertiesReadPlatformService { @@ -34,5 +35,7 @@ public interface ExternalServicesPropertiesReadPlatformService { MessageGatewayConfigurationData getSMSGateway(); Collection retrieveOne(String serviceName); + + NotificationConfigurationData getNotificationConfiguration(); } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java index ccd401231..bf6270850 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesPropertiesReadPlatformServiceImpl.java @@ -28,6 +28,7 @@ import org.apache.fineract.infrastructure.configuration.data.S3CredentialsData; import org.apache.fineract.infrastructure.configuration.data.SMTPCredentialsData; import org.apache.fineract.infrastructure.configuration.exception.ExternalServiceConfigurationNotFoundException; import org.apache.fineract.infrastructure.core.service.RoutingDataSource; +import org.apache.fineract.infrastructure.gcm.domain.NotificationConfigurationData; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -176,6 +177,10 @@ public class ExternalServicesPropertiesReadPlatformServiceImpl implements Extern serviceNameToUse = ExternalServicesConstants.SMS_SERVICE_NAME; break; + case "NOTIFICATION": + serviceNameToUse = ExternalServicesConstants.NOTIFICATION_SERVICE_NAME; + break; + default: throw new ExternalServiceConfigurationNotFoundException(serviceName); } @@ -185,5 +190,35 @@ public class ExternalServicesPropertiesReadPlatformServiceImpl implements Extern return this.jdbcTemplate.query(sql, mapper, new Object[] {}); } + + private static final class NotificationDataExtractor implements ResultSetExtractor { + + @Override + public NotificationConfigurationData extractData(final ResultSet rs) throws SQLException, DataAccessException { + String serverKey = null; + String gcmEndPoint = null; + String fcmEndPoint = null; + while (rs.next()) { + if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.NOTIFICATION_SERVER_KEY )) { + serverKey = rs.getString("value"); + } else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.NOTIFICATION_GCM_END_POINT )) { + gcmEndPoint = rs.getString("value"); + } else if (rs.getString("name").equalsIgnoreCase(ExternalServicesConstants.NOTIFICATION_FCM_END_POINT )) { + fcmEndPoint = rs.getString("value"); + } + } + return new NotificationConfigurationData(null, serverKey, gcmEndPoint, fcmEndPoint); + } + } + + + @Override + public NotificationConfigurationData getNotificationConfiguration() { + final ResultSetExtractor resultSetExtractor = new NotificationDataExtractor(); + final String sql = "SELECT esp.name, esp.value FROM c_external_service_properties esp inner join c_external_service es on esp.external_service_id = es.id where es.name = '" + + ExternalServicesConstants.NOTIFICATION_SERVICE_NAME + "'"; + final NotificationConfigurationData notificationConfigurationData = this.jdbcTemplate.query(sql, resultSetExtractor, new Object[] {}); + return notificationConfigurationData; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java index 44f41647b..9bab493ea 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/configuration/service/ExternalServicesReadPlatformServiceImpl.java @@ -58,6 +58,10 @@ public class ExternalServicesReadPlatformServiceImpl implements ExternalServices serviceNameToUse = ExternalServicesConstants.SMS_SERVICE_NAME; break; + case "NOTIFICATION": + serviceNameToUse = ExternalServicesConstants.NOTIFICATION_SERVICE_NAME; + break; + default: throw new ExternalServiceConfigurationNotFoundException(serviceName); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/GcmConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/GcmConstants.java new file mode 100644 index 000000000..fa3b9bb65 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/GcmConstants.java @@ -0,0 +1,242 @@ +/** + * 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; + +/** + * Constants used on GCM service communication. + */ +public final class GcmConstants { + + + /** + * Title for notification + */ + public static final String title = "Hello !"; + + /** + * icon for notification. + */ + public static final String defaultIcon = "default"; + + + /** + * Parameter for to field. + */ + public static final String PARAM_TO = "to"; + + /** + * Prefix of the topic. + */ + public static final String TOPIC_PREFIX = "/topics/"; + + /** + * HTTP parameter for registration id. + */ + //public static final String PARAM_REGISTRATION_ID = "registration_id"; + + /** + * HTTP parameter for collapse key. + */ + public static final String PARAM_COLLAPSE_KEY = "collapse_key"; + + /** + * HTTP parameter for delaying the message delivery if the device is idle. + */ + public static final String PARAM_DELAY_WHILE_IDLE = "delay_while_idle"; + + /** + * HTTP parameter for telling gcm to validate the message without actually + * sending it. + */ + public static final String PARAM_DRY_RUN = "dry_run"; + + /** + * HTTP parameter for package name that can be used to restrict message + * delivery by matching against the package name used to generate the + * registration id. + */ + public static final String PARAM_RESTRICTED_PACKAGE_NAME = "restricted_package_name"; + + /** + * Parameter used to set the message time-to-live. + */ + public static final String PARAM_TIME_TO_LIVE = "time_to_live"; + + /** + * Parameter used to set the message priority. + */ + public static final String PARAM_PRIORITY = "priority"; + + /** + * Parameter used to set the content available (iOS only) + */ + public static final String PARAM_CONTENT_AVAILABLE = "content_available"; + + /** + * Value used to set message priority to normal. + */ + public static final String MESSAGE_PRIORITY_NORMAL = "normal"; + + /** + * Value used to set message priority to high. + */ + public static final String MESSAGE_PRIORITY_HIGH = "high"; + + /** + * A particular message could not be sent because the GCM servers were not + * available. Used only on JSON requests, as in plain text requests + * unavailability is indicated by a 503 response. + */ + public static final String ERROR_UNAVAILABLE = "Unavailable"; + + /** + * A particular message could not be sent because the GCM servers + * encountered an error. Used only on JSON requests, as in plain text + * requests internal errors are indicated by a 500 response. + */ + public static final String ERROR_INTERNAL_SERVER_ERROR = "InternalServerError"; + + /** + * Token returned by GCM when the requested registration id has a canonical + * value. + */ + public static final String TOKEN_CANONICAL_REG_ID = "registration_id"; + + /** + * JSON-only field representing the registration ids. + */ + public static final String JSON_REGISTRATION_IDS = "registration_ids"; + + /** + * JSON-only field representing the to recipient. + */ + public static final String JSON_TO = "to"; + + /** + * JSON-only field representing the payload data. + */ + public static final String JSON_PAYLOAD = "data"; + + /** + * JSON-only field representing the notification payload. + */ + public static final String JSON_NOTIFICATION = "notification"; + + /** + * JSON-only field representing the notification title. + */ + public static final String JSON_NOTIFICATION_TITLE = "title"; + + /** + * JSON-only field representing the notification body. + */ + public static final String JSON_NOTIFICATION_BODY = "body"; + + /** + * JSON-only field representing the notification icon. + */ + public static final String JSON_NOTIFICATION_ICON = "icon"; + + /** + * JSON-only field representing the notification sound. + */ + public static final String JSON_NOTIFICATION_SOUND = "sound"; + + /** + * JSON-only field representing the notification badge. + */ + public static final String JSON_NOTIFICATION_BADGE = "badge"; + + /** + * JSON-only field representing the notification tag. + */ + public static final String JSON_NOTIFICATION_TAG = "tag"; + + /** + * JSON-only field representing the notification color. + */ + public static final String JSON_NOTIFICATION_COLOR = "color"; + + /** + * JSON-only field representing the notification click action. + */ + public static final String JSON_NOTIFICATION_CLICK_ACTION = "click_action"; + + /** + * JSON-only field representing the notification body localization key. + */ + public static final String JSON_NOTIFICATION_BODY_LOC_KEY = "body_loc_key"; + + /** + * JSON-only field representing the notification body localization values. + */ + public static final String JSON_NOTIFICATION_BODY_LOC_ARGS = "body_loc_args"; + + /** + * JSON-only field representing the notification title localization key. + */ + public static final String JSON_NOTIFICATION_TITLE_LOC_KEY = "title_loc_key"; + + /** + * JSON-only field representing the notification title localization values. + */ + public static final String JSON_NOTIFICATION_TITLE_LOC_ARGS = "title_loc_args"; + + /** + * JSON-only field representing the number of successful messages. + */ + public static final String JSON_SUCCESS = "success"; + + /** + * JSON-only field representing the number of failed messages. + */ + public static final String JSON_FAILURE = "failure"; + + /** + * JSON-only field representing the number of messages with a canonical + * registration id. + */ + public static final String JSON_CANONICAL_IDS = "canonical_ids"; + + /** + * JSON-only field representing the id of the multicast request. + */ + public static final String JSON_MULTICAST_ID = "multicast_id"; + + /** + * JSON-only field representing the result of each individual request. + */ + public static final String JSON_RESULTS = "results"; + + /** + * JSON-only field representing the error field of an individual request. + */ + public static final String JSON_ERROR = "error"; + + /** + * JSON-only field sent by GCM when a message was successfully sent. + */ + public static final String JSON_MESSAGE_ID = "message_id"; + + private GcmConstants() { + throw new UnsupportedOperationException(); + } + + public static final Integer TIME_TO_LIVE = 30; +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiConstants.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiConstants.java new file mode 100644 index 000000000..0b675cb47 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiConstants.java @@ -0,0 +1,25 @@ +/** + * 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.api; + +public class DeviceRegistrationApiConstants { + public static final String clientIdParamName = "clientId"; + public static final String registrationIdParamName = "registrationId"; + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiResource.java new file mode 100644 index 000000000..1ba0fa737 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/api/DeviceRegistrationApiResource.java @@ -0,0 +1,160 @@ +/** + * 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.api; + +import java.util.Collection; +import java.util.HashMap; + +import javax.ws.rs.Consumes; +import javax.ws.rs.DELETE; +import javax.ws.rs.GET; +import javax.ws.rs.POST; +import javax.ws.rs.PUT; +import javax.ws.rs.Path; +import javax.ws.rs.PathParam; +import javax.ws.rs.Produces; +import javax.ws.rs.core.Context; +import javax.ws.rs.core.MediaType; +import javax.ws.rs.core.UriInfo; + +import org.apache.fineract.infrastructure.core.serialization.DefaultToApiJsonSerializer; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistration; +import org.apache.fineract.infrastructure.gcm.domain.DeviceRegistrationData; +import org.apache.fineract.infrastructure.gcm.service.DeviceRegistrationReadPlatformService; +import org.apache.fineract.infrastructure.gcm.service.DeviceRegistrationWritePlatformService; +import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Scope; +import org.springframework.stereotype.Component; + +import com.google.gson.Gson; +import com.google.gson.JsonObject; + +@Path("/device/registration") +@Component +@Scope("singleton") +public class DeviceRegistrationApiResource { + + private final PlatformSecurityContext context; + private final DeviceRegistrationWritePlatformService deviceRegistrationWritePlatformService; + private final DefaultToApiJsonSerializer toApiJsonSerializer; + private final DeviceRegistrationReadPlatformService deviceRegistrationReadPlatformService; + + @Autowired + public DeviceRegistrationApiResource(PlatformSecurityContext context, + final DefaultToApiJsonSerializer toApiJsonSerializer, + final DeviceRegistrationReadPlatformService deviceRegistrationReadPlatformService, + final DeviceRegistrationWritePlatformService deviceRegistrationWritePlatformService) { + this.context = context; + this.toApiJsonSerializer = toApiJsonSerializer; + this.deviceRegistrationReadPlatformService = deviceRegistrationReadPlatformService; + this.deviceRegistrationWritePlatformService = deviceRegistrationWritePlatformService; + } + + @POST + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String registerDevice(final String apiRequestBodyAsJson) { + this.context.authenticatedUser(); + Gson gson = new Gson(); + JsonObject json = new Gson().fromJson(apiRequestBodyAsJson, JsonObject.class); + Long clientId = json.get(DeviceRegistrationApiConstants.clientIdParamName).getAsLong(); + String registrationId = json.get(DeviceRegistrationApiConstants.registrationIdParamName).getAsString(); + DeviceRegistration deviceRegistration = this.deviceRegistrationWritePlatformService.registerDevice(clientId, registrationId); + String response = gson.toJson(deviceRegistration); + return response; + } + + @GET + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveAllDeviceRegistrations(@Context final UriInfo uriInfo) { + + this.context.authenticatedUser(); + + Collection deviceRegistrationDataList = this.deviceRegistrationReadPlatformService + .retrieveAllDeviceRegiistrations(); + + return this.toApiJsonSerializer.serialize(deviceRegistrationDataList); + } + + @GET + @Path("client/{clientId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveDeviceRegistrationByClientId(@PathParam("clientId") final Long clientId, @Context final UriInfo uriInfo) { + + this.context.authenticatedUser(); + + DeviceRegistrationData deviceRegistrationData = this.deviceRegistrationReadPlatformService + .retrieveDeviceRegiistrationByClientId(clientId); + + return this.toApiJsonSerializer.serialize(deviceRegistrationData); + } + + @GET + @Path("{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String retrieveDeviceRegiistration(@PathParam("id") final Long id, @Context final UriInfo uriInfo) { + + this.context.authenticatedUser(); + + DeviceRegistrationData deviceRegistrationData = this.deviceRegistrationReadPlatformService.retrieveDeviceRegiistration(id); + + return this.toApiJsonSerializer.serialize(deviceRegistrationData); + } + + @PUT + @Path("{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String updateDeviceRegistration(@PathParam("id") final Long id, final String apiRequestBodyAsJson) { + + this.context.authenticatedUser(); + + Gson gson = new Gson(); + JsonObject json = new Gson().fromJson(apiRequestBodyAsJson, JsonObject.class); + Long clientId = json.get(DeviceRegistrationApiConstants.clientIdParamName).getAsLong(); + String registrationId = json.get(DeviceRegistrationApiConstants.registrationIdParamName).getAsString(); + DeviceRegistration deviceRegistration = this.deviceRegistrationWritePlatformService.updateDeviceRegistration(id, clientId, + registrationId); + String response = gson.toJson(deviceRegistration); + return response; + } + + @DELETE + @Path("{id}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + public String delete(@PathParam("id") final Long id) { + + this.context.authenticatedUser(); + this.deviceRegistrationWritePlatformService.deleteDeviceRegistration(id); + return responseMap(id); + + } + + public String responseMap(Long id){ + HashMap responseMap = new HashMap<>(); + responseMap.put("resource", id); + return new Gson().toJson(responseMap); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistration.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistration.java new file mode 100644 index 000000000..c4ba7b330 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistration.java @@ -0,0 +1,85 @@ +/** + * 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.util.Date; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.JoinColumn; +import javax.persistence.OneToOne; +import javax.persistence.Table; +import javax.persistence.Temporal; +import javax.persistence.TemporalType; + +import org.apache.fineract.infrastructure.core.domain.AbstractPersistableCustom; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.client.domain.Client; + +@Entity +@Table(name = "client_device_registration") +public class DeviceRegistration extends AbstractPersistableCustom { + + @OneToOne + @JoinColumn(name = "client_id", nullable = false, unique = true) + private Client client; + + @Column(name = "registration_id", nullable = false, unique = true) + private String registrationId; + + @Column(name = "updatedon_date", nullable = false) + @Temporal(TemporalType.TIMESTAMP) + private Date updatedOnDate; + + private DeviceRegistration(final Client client, final String registrationId) { + this.client = client; + this.registrationId = registrationId; + this.updatedOnDate = DateUtils.getLocalDateTimeOfTenant().toDate(); + } + + public static DeviceRegistration instance(final Client client, + final String registrationId) { + return new DeviceRegistration(client, registrationId); + } + + public Client getClient() { + return this.client; + } + + public void setClient(Client client) { + this.client = client; + } + + public String getRegistrationId() { + return this.registrationId; + } + + public void setRegistrationId(String registrationId) { + this.registrationId = registrationId; + } + + public Date getUpdatedOnDate() { + return this.updatedOnDate; + } + + public void setUpdatedOnDate(Date updatedOnDate) { + this.updatedOnDate = updatedOnDate; + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationData.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationData.java new file mode 100644 index 000000000..3397a9bd3 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationData.java @@ -0,0 +1,47 @@ +/** + * 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.util.Date; + +import org.apache.fineract.portfolio.client.data.ClientData; + +public class DeviceRegistrationData { + + public Long id; + public ClientData clientData; + public String registrationId; + public Date updatedOnDate; + + private DeviceRegistrationData(final Long id, final ClientData clientData, + final String registrationId, final Date updatedOnDate) { + this.id = id; + this.clientData = clientData; + this.registrationId = registrationId; + this.updatedOnDate = updatedOnDate; + } + + public static DeviceRegistrationData instance(final Long id, + final ClientData clientData, final String registrationId, + final Date updatedOnDate) { + return new DeviceRegistrationData(id, clientData, registrationId, + updatedOnDate); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepository.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepository.java new file mode 100644 index 000000000..003311762 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepository.java @@ -0,0 +1,36 @@ +/** + * 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 org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +public interface DeviceRegistrationRepository extends + JpaRepository, + JpaSpecificationExecutor { + + public static final String FIND_DEVICE_REGISTRATION_BY_CLIENT = "select dr from DeviceRegistration dr where dr.client.id =:clientId "; + + @Query(FIND_DEVICE_REGISTRATION_BY_CLIENT) + DeviceRegistration findDeviceRegistrationByClientId( + @Param("clientId") Long clientId); + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepositoryWrapper.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepositoryWrapper.java new file mode 100644 index 000000000..a36b476ee --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/gcm/domain/DeviceRegistrationRepositoryWrapper.java @@ -0,0 +1,61 @@ +/** + * 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 org.apache.fineract.infrastructure.gcm.exception.DeviceRegistrationNotFoundException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.stereotype.Service; + +@Service +public class DeviceRegistrationRepositoryWrapper { + + private final DeviceRegistrationRepository repository; + + @Autowired + public DeviceRegistrationRepositoryWrapper( + DeviceRegistrationRepository repository) { + this.repository = repository; + } + + public DeviceRegistration findOneWithNotFoundDetection( + final Long deviceRegistrationId) { + final DeviceRegistration deviceRegistration = this.repository + .findOne(deviceRegistrationId); + if (deviceRegistration == null) { + throw new DeviceRegistrationNotFoundException(deviceRegistrationId); + } + return deviceRegistration; + } + + public void save(final DeviceRegistration deviceRegistration) { + this.repository.save(deviceRegistration); + } + + public void delete(final DeviceRegistration deviceRegistration) { + this.repository.delete(deviceRegistration); + } + + public void saveAndFlush(final DeviceRegistration deviceRegistration) { + this.repository.saveAndFlush(deviceRegistration); + } + + public DeviceRegistration findDeviceRegistrationByClientId(Long clientId) { + return this.repository.findDeviceRegistrationByClientId(clientId); + } +} 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 000000000..34234dcb8 --- /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. + * + *

+ * Instances of this class are immutable and should be created using a + * {@link Builder}. Examples: + * + * Simplest message: + * + *

+ * 
+ * Message message = new Message.Builder().build();
+ * 
+ * + * + * + * Message with optional attributes: + * + *
+ * 
+ * Message message = new Message.Builder()
+ *    .collapseKey(collapseKey)
+ *    .timeToLive(3)
+ *    .delayWhileIdle(true)
+ *    .dryRun(true)
+ *    .restrictedPackageName(restrictedPackageName)
+ *    .build();
+ * 
+ * + * + * + * Message with optional attributes and payload data: + * + *
+ * 
+ * Message message = new Message.Builder()
+ *    .priority("normal")
+ *    .collapseKey(collapseKey)
+ *    .timeToLive(3)
+ *    .delayWhileIdle(true)
+ *    .dryRun(true)
+ *    .restrictedPackageName(restrictedPackageName)
+ *    .addData("key1", "value1")
+ *    .addData("key2", "value2")
+ *    .build();
+ * 
+ * + * + */ +public final class Message implements Serializable { + + private final String collapseKey; + private final Boolean delayWhileIdle; + private final Integer timeToLive; + private final Map 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 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 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 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(); + } + +} 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 000000000..c58de9fe0 --- /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 results; + private final List retryMulticastIds; + + public static final class Builder { + + private final List 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 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 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 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 getResults() { + return results; + } + + /** + * Gets additional ids if more than one multicast message was sent. + */ + public List 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(); + } + +} 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 000000000..589e7725d --- /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. + * + *

+ * Instances of this class are immutable and should be created using a + * {@link Builder}. Examples: + * + * Simplest notification: + * + *

+ * 
+ * Notification notification = new Notification.Builder("myicon").build();
+ * 
+ * + * + * + * Notification with optional attributes: + * + *
+ * 
+ * Notification notification = new Notification.Builder("myicon")
+ *    .title("Hello world!")
+ *    .body("Here is a more detailed description")
+ *    .build();
+ * 
+ * + * + */ +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 bodyLocArgs; + private final String titleLocKey; + private final List 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 bodyLocArgs; + private String titleLocKey; + private List 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 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 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 getBodyLocArgs() { + return bodyLocArgs; + } + + /** + * Gets the title localization key. + */ + public String getTitleLocKey() { + return titleLocKey; + } + + /** + * Gets the title localization values list, which is immutable. + */ + public List 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(); + } + +} 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 000000000..2d4acc93f --- /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; + } + + + +} 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 000000000..76aafa8ef --- /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. + * + *

+ * 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. + * + *

+ * 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. + * + *

+ * In a nutshell, the workflow to handle a result is: + * + *

+ *   - 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.
+ * 
+ */ +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 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 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 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 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; + } + + + +} 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 000000000..cc9970e4d --- /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. + *

+ * 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. + *

+ * 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. + * + *

+ * Note: 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 jsonRequest = new HashMap<>(); + messageToMap(message, jsonRequest); + jsonRequest.put(JSON_TO, to); + Map 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 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. + * + *

+ * Note: 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 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 results = new HashMap<>(); + List unsentRegIds = new ArrayList<>(regIds); + boolean tryAgain; + List 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 updateStatus(List unsentRegIds, + Map allResults, MulticastResult multicastResult) { + List 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 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 registrationIds) throws IOException { + if (nonNull(registrationIds).isEmpty()) { + throw new IllegalArgumentException( + "registrationIds cannot be empty"); + } + Map jsonRequest = new HashMap<>(); + messageToMap(message, jsonRequest); + jsonRequest.put(JSON_REGISTRATION_IDS, registrationIds); + Map 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> results = (List>) jsonResponse + .get(JSON_RESULTS); + if (results != null) { + for (Map 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 makeGcmHttpRequest(Map 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 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 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 payload = message.getData(); + if (!payload.isEmpty()) { + mapRequest.put(JSON_PAYLOAD, payload); + } + if (message.getNotification() != null) { + Notification notification = message.getNotification(); + Map 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 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. + * + *

+ * Note: 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 newKeyValues(String key, + String value) { + Map 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. + *

+ * If the stream ends in a newline character, it will be stripped. + *

+ * 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 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(); + } + } +} 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 000000000..cc730a0ec --- /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); + } + +} 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 000000000..2194b43d8 --- /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. + *

+ * 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; + } + +} 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 000000000..1b4ec42c8 --- /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 retrieveAllDeviceRegiistrations(); + + DeviceRegistrationData retrieveDeviceRegiistration(Long id); + + DeviceRegistrationData retrieveDeviceRegiistrationByClientId(Long clientId); +} 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 000000000..8c64fe3b3 --- /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 { + + 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 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"); + } + } + +} 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 000000000..2391fdd58 --- /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); +} 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 000000000..52653eaa2 --- /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); + } + +} 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 000000000..67fb07237 --- /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 smsMessages) { + Map> notificationByEachClient = getNotificationListByClient(smsMessages); + for (Map.Entry> entry : notificationByEachClient + .entrySet()) { + this.sendNotifiaction(entry.getKey(), entry.getValue()); + } + } + + public Map> getNotificationListByClient( + List smsMessages) { + Map> 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 msgList = new ArrayList<>( + Arrays.asList(smsMessage)); + notificationByEachClient.put(clientId, msgList); + } + + } + } + return notificationByEachClient; + } + + public void sendNotifiaction(Long clientId, List 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); + + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessage.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessage.java index e589f00b5..c4966040f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessage.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessage.java @@ -66,7 +66,7 @@ public class SmsMessage extends AbstractPersistableCustom { @Column(name = "status_enum", nullable = false) private Integer statusType; - @Column(name = "mobile_no", nullable = false, length = 50) + @Column(name = "mobile_no", nullable = true, length = 50) private String mobileNo; @Column(name = "message", nullable = false) @@ -84,22 +84,25 @@ public class SmsMessage extends AbstractPersistableCustom { @Column(name = "delivered_on_date", nullable = true) @Temporal(TemporalType.TIMESTAMP) - private Date deliveredOnDate; + private Date deliveredOnDate; + + @Column(name = "is_notification", nullable = true) + private boolean isNotification; public static SmsMessage pendingSms(final String externalId, final Group group, final Client client, final Staff staff, - final String message, final String mobileNo, final SmsCampaign smsCampaign) { - return new SmsMessage(externalId, group, client, staff, SmsMessageStatusType.PENDING, message, mobileNo, smsCampaign); + final String message, final String mobileNo, final SmsCampaign smsCampaign, final boolean isNotification) { + return new SmsMessage(externalId, group, client, staff, SmsMessageStatusType.PENDING, message, mobileNo, smsCampaign, isNotification); } public static SmsMessage sentSms(final String externalId, final Group group, final Client client, final Staff staff, - final String message, final String mobileNo, final SmsCampaign smsCampaign) { - return new SmsMessage(externalId, group, client, staff, SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT, message, mobileNo, smsCampaign); + final String message, final String mobileNo, final SmsCampaign smsCampaign, final boolean isNotification) { + return new SmsMessage(externalId, group, client, staff, SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT, message, mobileNo, smsCampaign, isNotification); } public static SmsMessage instance(String externalId, final Group group, final Client client, final Staff staff, - final SmsMessageStatusType statusType, final String message, final String mobileNo, final SmsCampaign smsCampaign) { + final SmsMessageStatusType statusType, final String message, final String mobileNo, final SmsCampaign smsCampaign, final boolean isNotification) { - return new SmsMessage(externalId, group, client, staff, statusType, message, mobileNo, smsCampaign); + return new SmsMessage(externalId, group, client, staff, statusType, message, mobileNo, smsCampaign, isNotification); } protected SmsMessage() { @@ -107,7 +110,7 @@ public class SmsMessage extends AbstractPersistableCustom { } private SmsMessage(String externalId, final Group group, final Client client, final Staff staff, final SmsMessageStatusType statusType, - final String message, final String mobileNo, final SmsCampaign smsCampaign) { + final String message, final String mobileNo, final SmsCampaign smsCampaign, final boolean isNotification) { this.externalId = externalId; this.group = group; this.client = client; @@ -117,6 +120,7 @@ public class SmsMessage extends AbstractPersistableCustom { this.message = message; this.smsCampaign = smsCampaign; this.submittedOnDate = LocalDate.now().toDate(); + this.isNotification = isNotification; } public Map update(final JsonCommand command) { @@ -183,4 +187,14 @@ public class SmsMessage extends AbstractPersistableCustom { public void setDeliveredOnDate(final Date deliveredOnDate) { this.deliveredOnDate = deliveredOnDate; } + + public boolean isNotification() { + return this.isNotification; + } + + public void setNotification(boolean isNotification) { + this.isNotification = isNotification; + } + + } \ No newline at end of file diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessageAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessageAssembler.java index c879ffb6c..32591338b 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessageAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/domain/SmsMessageAssembler.java @@ -71,10 +71,12 @@ public class SmsMessageAssembler { } SmsCampaign smsCampaign = null; + boolean isNotification = false; if (this.fromApiJsonHelper.parameterExists(SmsApiConstants.campaignIdParamName, element)) { final Long campaignId = this.fromApiJsonHelper.extractLongNamed(SmsApiConstants.campaignIdParamName, element); smsCampaign = this.smsCampaignRepository.findOne(campaignId); if (smsCampaign == null) { throw new SmsCampaignNotFound(campaignId); } + isNotification = smsCampaign.isNotification(); } Client client = null; @@ -93,7 +95,7 @@ public class SmsMessageAssembler { final String message = this.fromApiJsonHelper.extractStringNamed(SmsApiConstants.messageParamName, element); - return SmsMessage.pendingSms(externalId, group, client, staff, message, mobileNo, smsCampaign); + return SmsMessage.pendingSms(externalId, group, client, staff, message, mobileNo, smsCampaign, isNotification); } public SmsMessage assembleFromResourceId(final Long resourceId) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/scheduler/SmsMessageScheduledJobServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/scheduler/SmsMessageScheduledJobServiceImpl.java index 373af1d08..e2e998a4f 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/scheduler/SmsMessageScheduledJobServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/sms/scheduler/SmsMessageScheduledJobServiceImpl.java @@ -37,6 +37,7 @@ import org.apache.fineract.infrastructure.campaigns.sms.exception.ConnectionFail import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; import org.apache.fineract.infrastructure.core.service.Page; import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.infrastructure.gcm.service.NotificationSenderService; import org.apache.fineract.infrastructure.jobs.annotation.CronTarget; import org.apache.fineract.infrastructure.jobs.service.JobName; import org.apache.fineract.infrastructure.sms.data.SmsMessageApiQueueResourceData; @@ -76,6 +77,7 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob private ExecutorService genericExecutorService ; private ExecutorService triggeredExecutorService ; private final SmsConfigUtils smsConfigUtils ; + private final NotificationSenderService notificationSenderService; /** @@ -83,10 +85,11 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob **/ @Autowired public SmsMessageScheduledJobServiceImpl(SmsMessageRepository smsMessageRepository, SmsReadPlatformService smsReadPlatformService, - final SmsConfigUtils smsConfigUtils) { + final SmsConfigUtils smsConfigUtils, final NotificationSenderService notificationSenderService) { this.smsMessageRepository = smsMessageRepository; this.smsReadPlatformService = smsReadPlatformService; this.smsConfigUtils = smsConfigUtils ; + this.notificationSenderService = notificationSenderService; } @PostConstruct @@ -110,6 +113,7 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob org.springframework.data.domain.Page pendingMessages = this.smsMessageRepository.findByStatusType( SmsMessageStatusType.PENDING.getValue(), pageRequest); List toSaveMessages = new ArrayList<>() ; + List toSendNotificationMessages = new ArrayList<>() ; try { if (pendingMessages.getContent().size() > 0) { @@ -118,18 +122,26 @@ public class SmsMessageScheduledJobServiceImpl implements SmsMessageScheduledJob Collection apiQueueResourceDatas = new ArrayList<>(); while (pendingMessageIterator.hasNext()) { SmsMessage smsData = pendingMessageIterator.next(); - - SmsMessageApiQueueResourceData apiQueueResourceData = SmsMessageApiQueueResourceData.instance(smsData.getId(), - tenantIdentifier, null, null, smsData.getMobileNo(), smsData.getMessage(), smsData.getSmsCampaign() - .getProviderId()); - apiQueueResourceDatas.add(apiQueueResourceData); - smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue()); - toSaveMessages.add(smsData) ; + if(smsData.isNotification()){ + smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue()); + toSendNotificationMessages.add(smsData); + }else{ + SmsMessageApiQueueResourceData apiQueueResourceData = SmsMessageApiQueueResourceData.instance(smsData.getId(), + tenantIdentifier, null, null, smsData.getMobileNo(), smsData.getMessage(), smsData.getSmsCampaign() + .getProviderId()); + apiQueueResourceDatas.add(apiQueueResourceData); + smsData.setStatusType(SmsMessageStatusType.WAITING_FOR_DELIVERY_REPORT.getValue()); + toSaveMessages.add(smsData) ; + } + } + if(toSaveMessages.size()>0){ + this.smsMessageRepository.save(toSaveMessages); + this.smsMessageRepository.flush(); + this.genericExecutorService.execute(new SmsTask(ThreadLocalContextUtil.getTenant(), apiQueueResourceDatas)); + } + if(!toSendNotificationMessages.isEmpty()){ + this.notificationSenderService.sendNotification(toSendNotificationMessages); } - this.smsMessageRepository.save(toSaveMessages); - this.smsMessageRepository.flush(); - this.genericExecutorService.execute(new SmsTask(ThreadLocalContextUtil.getTenant(), apiQueueResourceDatas)); - // new MyThread(ThreadLocalContextUtil.getTenant(), apiQueueResourceDatas).start(); } } catch (Exception e) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java index 3827307fc..0a8602ebb 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/client/data/ClientData.java @@ -289,6 +289,12 @@ final public class ClientData implements Comparable { clientNonPerson, clientLegalFormOptions,familyMemberOptions, legalForm,null,null, null, isStaff); } + + public static ClientData instance(final Long id, final String displayName){ + final Long officeId = null; + final String officeName = null; + return lookup(id, displayName, officeId, officeName); + } public static ClientData instance(final String accountNo, final EnumOptionData status, final CodeValueData subStatus, final Long officeId, final String officeName, final Long transferToOfficeId, final String transferToOfficeName, final Long id, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java index 9952dcf58..8b0b6a48c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/self/registration/service/SelfServiceRegistrationWritePlatformServiceImpl.java @@ -203,8 +203,9 @@ public class SelfServiceRegistrationWritePlatformServiceImpl implements SelfServ Group group = null; Staff staff = null; SmsCampaign smsCampaign = null; + boolean isNotification = false; SmsMessage smsMessage = SmsMessage.instance(externalId, group, selfServiceRegistration.getClient(), staff, - SmsMessageStatusType.PENDING, message, selfServiceRegistration.getMobileNumber(), smsCampaign); + SmsMessageStatusType.PENDING, message, selfServiceRegistration.getMobileNumber(), smsCampaign, isNotification); this.smsMessageRepository.save(smsMessage); this.smsMessageScheduledJobService.sendTriggeredMessage(new ArrayList<>(Arrays.asList(smsMessage)), providerId); } diff --git a/fineract-provider/src/main/resources/sql/migrations/core_db/V336__sms_campaign_notification.sql b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__sms_campaign_notification.sql new file mode 100644 index 000000000..37ab49629 --- /dev/null +++ b/fineract-provider/src/main/resources/sql/migrations/core_db/V336__sms_campaign_notification.sql @@ -0,0 +1,48 @@ +-- +-- 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. +-- + +CREATE TABLE `client_device_registration` ( + `id` BIGINT(20) NOT NULL AUTO_INCREMENT, + `client_id` BIGINT(20) NOT NULL, + `updatedon_date` DATETIME NOT NULL, + `registration_id` VARCHAR(255) NOT NULL, + PRIMARY KEY (`id`), + UNIQUE INDEX `registration_key_client_device_registration` (`registration_id`), + UNIQUE INDEX `client_key_client_device_registration` (`client_id`), + INDEX `FK1_client_device_registration_m_client` (`client_id`), + CONSTRAINT `FK1_client_device_registration_m_client` FOREIGN KEY (`client_id`) REFERENCES `m_client` (`id`) +); + +INSERT INTO `c_external_service` (`name`) VALUES ('NOTIFICATION'); + +INSERT INTO `c_external_service_properties` (`name`, `value`, `external_service_id`) VALUES ('server_key', +'AAAAToBmqQQ:APA91bEodkE12CwFl8VHqanUbeJYg1E05TiheVz59CZZYrBnCq3uM40UYhHfdP-JfeTQ0L0zoLqS8orjvW_ze0_VF8DSuyyqkrDibflhtUainsI0lwgVz5u1YP3q3c3erqjlySEuRShS', +(select id from `c_external_service` where name ='NOTIFICATION') +),('gcm_end_point','https://gcm-http.googleapis.com/gcm/send',(select id from `c_external_service` where name ='NOTIFICATION') +),('fcm_end_point','https://fcm.googleapis.com/fcm/send',(select id from `c_external_service` where name ='NOTIFICATION') +); + +ALTER TABLE sms_campaign +MODIFY COLUMN provider_id BIGINT(20) NULL DEFAULT NULL, +ADD COLUMN is_notification TINYINT(1) NULL DEFAULT '0'; + +ALTER TABLE sms_messages_outbound +MODIFY COLUMN mobile_no VARCHAR(50) NULL DEFAULT NULL, +ADD COLUMN is_notification TINYINT(1) NOT NULL DEFAULT '0' +