diff --git a/docs/Configuration-Properties.md b/docs/Configuration-Properties.md index e46015565..d9b74c163 100644 --- a/docs/Configuration-Properties.md +++ b/docs/Configuration-Properties.md @@ -25,15 +25,16 @@ The Push Server uses the following public configuration properties: ## PowerAuth Push Service Configuration -| Property | Default | Note | -|---|---|---| -| `powerauth.push.service.applicationName` | `powerauth-push` | Technical name of the instance | -| `powerauth.push.service.applicationDisplayName` | `PowerAuth Push Server` | Display name of the instance | -| `powerauth.push.service.applicationEnvironment` | `_empty_` | Environment identifier | -| `powerauth.push.service.message.storage.enabled` | `false` | Whether persistent storing of sent messages is enabled | -| `powerauth.push.service.registration.multipleActivations.enabled` | `false` | Whether push registration supports "associated activations" | -| `powerauth.push.service.registration.retry.backoff` | `100` | Duration in milliseconds before a retry attempt during device registration in case of an insert error | -| `owerauth.push.service.registration.retry.maxAttempts` | `2` | Max number of retry attempts during device registration in case of an insert error | +| Property | Default | Note | +|-------------------------------------------------------------------|-------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `powerauth.push.service.applicationName` | `powerauth-push` | Technical name of the instance | +| `powerauth.push.service.applicationDisplayName` | `PowerAuth Push Server` | Display name of the instance | +| `powerauth.push.service.applicationEnvironment` | `_empty_` | Environment identifier | +| `powerauth.push.service.message.storage.enabled` | `false` | Whether persistent storing of sent messages is enabled | +| `powerauth.push.service.registration.multipleActivations.enabled` | `false` | Whether push registration supports "associated activations" | +| `powerauth.push.service.registration.retry.backoff` | `100` | Duration in milliseconds before a retry attempt during device registration in case of an insert error | +| `powerauth.push.service.registration.retry.maxAttempts` | `2` | Max number of retry attempts during device registration in case of an insert error | +| `powerauth.push.service.clients.cache.refreshAfterWrite` | `5m` | APNS, FCM and HMS client configuration is cached. It is evicted if updated via administration on a single node. This is a smart fallback for the clustered environment. | ## PowerAuth Push Campaign Setup diff --git a/docs/PowerAuth-Push-Server-1.9.0.md b/docs/PowerAuth-Push-Server-1.9.0.md index 28ee3db86..44d81c2c0 100644 --- a/docs/PowerAuth-Push-Server-1.9.0.md +++ b/docs/PowerAuth-Push-Server-1.9.0.md @@ -2,4 +2,17 @@ This guide contains instructions for migration from PowerAuth Push Server version `1.8.x` to version `1.9.x`. -No migration steps nor database changes are required. +## Database Changes + +For convenience, you can use liquibase for your database migration. + +If you prefer to make manual DB schema changes, please use the following SQL scripts: + +- [PostgreSQL script](./sql/postgresql/migration_1.8.0_1.9.0.sql) +- [Oracle script](./sql/oracle/migration_1.8.0_1.9.0.sql) +- [MSSQL script](./sql/mssql/migration_1.8.0_1.9.0.sql) + + +### App Credentials Timestamp + +To improve caching, the columns `timestamp_created`, and `timestamp_last_updated` have been added into the table `push_app_credentials`. diff --git a/docs/Push-Server-Database.md b/docs/Push-Server-Database.md index d688a6d8f..5d7e14941 100644 --- a/docs/Push-Server-Database.md +++ b/docs/Push-Server-Database.md @@ -92,7 +92,9 @@ CREATE TABLE push_app_credentials ( ios_bundle VARCHAR(255), ios_environment VARCHAR(32), android_private_key BYTEA, - android_project_id VARCHAR(255) + android_project_id VARCHAR(255), + timestamp_created TIMESTAMP(6) DEFAULT NOW() NOT NULL, + timestamp_last_updated TIMESTAMP(6) ); CREATE UNIQUE INDEX push_app_cred_app ON push_app_credentials (app_id); @@ -100,17 +102,19 @@ CREATE UNIQUE INDEX push_app_cred_app ON push_app_credentials (app_id); #### Columns -| Name | Type | Info | Note | -|-----------------------|--------------|-----------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| -| `id` | INTEGER | primary key, index, autoincrement | Unique credential record ID. | -| `app_id` | VARCHAR(255) | index | Associated application ID. | -| `ios_key_id` | VARCHAR(255) | - | Key ID used for identifying a private key in APNs service. | -| `ios_private_key` | BYTEA | - | Binary representation of P8 file with private key used for Apple's APNs service. | -| `ios_team_id` | VARCHAR(255) | - | Team ID used for sending push notifications. | -| `ios_bundle` | VARCHAR(255) | - | Application bundle ID, used as a APNs "topic". | -| `ios_environment` | VARCHAR(32) | - | Per-application APNs environment setting. `NULL` or unknown value inherits from global server configuration, values `development` or `production` override the settings. | -| `android_private_key` | BYTEA | - | Firebase service account private key used when obtaining access tokens for FCM HTTP v1 API. | -| `android_project_id` | VARCHAR(255) | - | Firebase project ID, used when sending push messages using FCM. | +| Name | Type | Info | Note | +|--------------------------|--------------|--------------------------------------|--------------------------------------------------------------------------------------------------------------------------------------------------------------------------| +| `id` | INTEGER | primary key, index, autoincrement | Unique credential record ID. | +| `app_id` | VARCHAR(255) | index | Associated application ID. | +| `ios_key_id` | VARCHAR(255) | - | Key ID used for identifying a private key in APNs service. | +| `ios_private_key` | BYTEA | - | Binary representation of P8 file with private key used for Apple's APNs service. | +| `ios_team_id` | VARCHAR(255) | - | Team ID used for sending push notifications. | +| `ios_bundle` | VARCHAR(255) | - | Application bundle ID, used as a APNs "topic". | +| `ios_environment` | VARCHAR(32) | - | Per-application APNs environment setting. `NULL` or unknown value inherits from global server configuration, values `development` or `production` override the settings. | +| `android_private_key` | BYTEA | - | Firebase service account private key used when obtaining access tokens for FCM HTTP v1 API. | +| `android_project_id` | VARCHAR(255) | - | Firebase project ID, used when sending push messages using FCM. | +| `timestamp_created` | TIMESTAMP | `NOT NULL DEFAULT CURRENT_TIMESTAMP` | Timestamp when the record was created. | +| `timestamp_last_updated` | TIMESTAMP | | Timestamp when the record was last updated. | #### Keys diff --git a/docs/db/changelog/changesets/powerauth-push-server/1.9.x/20241011-app-credentials-timestamp.xml b/docs/db/changelog/changesets/powerauth-push-server/1.9.x/20241011-app-credentials-timestamp.xml new file mode 100644 index 000000000..74f67317c --- /dev/null +++ b/docs/db/changelog/changesets/powerauth-push-server/1.9.x/20241011-app-credentials-timestamp.xml @@ -0,0 +1,22 @@ + + + + + + + + + + + Add columns timestamp_last_updated and timestamp_created to push_app_credentials table + + + + + + + + + \ No newline at end of file diff --git a/docs/db/changelog/changesets/powerauth-push-server/1.9.x/db.changelog-version.xml b/docs/db/changelog/changesets/powerauth-push-server/1.9.x/db.changelog-version.xml new file mode 100644 index 000000000..9d39dcb65 --- /dev/null +++ b/docs/db/changelog/changesets/powerauth-push-server/1.9.x/db.changelog-version.xml @@ -0,0 +1,8 @@ + + + + + + \ No newline at end of file diff --git a/docs/db/changelog/changesets/powerauth-push-server/db.changelog-module.xml b/docs/db/changelog/changesets/powerauth-push-server/db.changelog-module.xml index 9b81cae22..6b91e95c0 100644 --- a/docs/db/changelog/changesets/powerauth-push-server/db.changelog-module.xml +++ b/docs/db/changelog/changesets/powerauth-push-server/db.changelog-module.xml @@ -3,6 +3,11 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.9.xsd"> + + + + + @@ -11,5 +16,6 @@ + diff --git a/docs/sql/mssql/migration_1.8.0_1.9.0.sql b/docs/sql/mssql/migration_1.8.0_1.9.0.sql new file mode 100644 index 000000000..aff4076bd --- /dev/null +++ b/docs/sql/mssql/migration_1.8.0_1.9.0.sql @@ -0,0 +1,7 @@ +-- Changeset powerauth-push-server/1.9.x/20241011-app-credentials-timestamp.xml::1::Lubos Racansky +-- Add columns timestamp_last_updated and timestamp_created to push_app_credentials table +ALTER TABLE push_app_credentials ADD timestamp_created datetime2 CONSTRAINT DF_push_app_credentials_timestamp_created DEFAULT GETDATE() NOT NULL; +GO + +ALTER TABLE push_app_credentials ADD timestamp_last_updated datetime2; +GO diff --git a/docs/sql/oracle/migration_1.8.0_1.9.0.sql b/docs/sql/oracle/migration_1.8.0_1.9.0.sql new file mode 100644 index 000000000..afe02b682 --- /dev/null +++ b/docs/sql/oracle/migration_1.8.0_1.9.0.sql @@ -0,0 +1,5 @@ +-- Changeset powerauth-push-server/1.9.x/20241011-app-credentials-timestamp.xml::1::Lubos Racansky +-- Add columns timestamp_last_updated and timestamp_created to push_app_credentials table +ALTER TABLE push_app_credentials ADD timestamp_created TIMESTAMP DEFAULT sysdate NOT NULL; + +ALTER TABLE push_app_credentials ADD timestamp_last_updated TIMESTAMP; diff --git a/docs/sql/postgresql/migration_1.8.0_1.9.0.sql b/docs/sql/postgresql/migration_1.8.0_1.9.0.sql new file mode 100644 index 000000000..f6081e2de --- /dev/null +++ b/docs/sql/postgresql/migration_1.8.0_1.9.0.sql @@ -0,0 +1,5 @@ +-- Changeset powerauth-push-server/1.9.x/20241011-app-credentials-timestamp.xml::1::Lubos Racansky +-- Add columns timestamp_last_updated and timestamp_created to push_app_credentials table +ALTER TABLE push_app_credentials ADD timestamp_created TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL; + +ALTER TABLE push_app_credentials ADD timestamp_last_updated TIMESTAMP WITHOUT TIME ZONE; diff --git a/powerauth-push-server/pom.xml b/powerauth-push-server/pom.xml index 145d6cbe8..bc33b3980 100644 --- a/powerauth-push-server/pom.xml +++ b/powerauth-push-server/pom.xml @@ -158,6 +158,11 @@ jackson-datatype-jsr310 + + com.github.ben-manes.caffeine + caffeine + + io.netty diff --git a/powerauth-push-server/src/main/java/io/getlime/push/configuration/CacheConfiguration.java b/powerauth-push-server/src/main/java/io/getlime/push/configuration/CacheConfiguration.java new file mode 100644 index 000000000..532f3a9b5 --- /dev/null +++ b/powerauth-push-server/src/main/java/io/getlime/push/configuration/CacheConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed 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 io.getlime.push.configuration; + +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.LoadingCache; +import io.getlime.push.service.AppRelatedPushClient; +import io.getlime.push.service.AppRelatedPushClientCacheLoader; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import java.time.Duration; + + +/** + * Cache configuration. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Configuration +@Slf4j +public class CacheConfiguration { + + /** + * Configure cache for {@link AppRelatedPushClient}. + * + * @return cache for AppRelatedPushClient + */ + @Bean + public LoadingCache appRelatedPushClientCache( + @Value("${powerauth.push.service.clients.cache.refreshAfterWrite}") final Duration refreshAfterWrite, + final AppRelatedPushClientCacheLoader cacheLoader) { + + logger.info("Initializing AppRelatedPushClient cache with refreshAfterWrite={}", refreshAfterWrite); + return Caffeine.newBuilder() + .refreshAfterWrite(refreshAfterWrite) + .build(cacheLoader); + } + +} diff --git a/powerauth-push-server/src/main/java/io/getlime/push/configuration/StorageMapConfiguration.java b/powerauth-push-server/src/main/java/io/getlime/push/configuration/StorageMapConfiguration.java deleted file mode 100644 index 662e7585d..000000000 --- a/powerauth-push-server/src/main/java/io/getlime/push/configuration/StorageMapConfiguration.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2016 Wultra s.r.o. - * - * Licensed 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 io.getlime.push.configuration; - -import io.getlime.push.service.batch.storage.AppCredentialStorageMap; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; - - -/** - * Configuration class for in memory maps - * - * @author Martin Tupy, martin.tupy.work@gmail.com - */ -@Configuration -public class StorageMapConfiguration { - - /** - * Bean definition for app credentials map - * @return Bean with app credential storage map. - */ - @Bean - public AppCredentialStorageMap appCredentialStorageMap() { - return new AppCredentialStorageMap(); - } -} diff --git a/powerauth-push-server/src/main/java/io/getlime/push/controller/rest/AdministrationController.java b/powerauth-push-server/src/main/java/io/getlime/push/controller/rest/AdministrationController.java index 8b0825d9b..5183e325b 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/controller/rest/AdministrationController.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/controller/rest/AdministrationController.java @@ -15,8 +15,6 @@ */ package io.getlime.push.controller.rest; -import com.wultra.security.powerauth.client.PowerAuthClient; -import com.wultra.security.powerauth.client.model.entity.Application; import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; import io.getlime.core.rest.model.base.request.ObjectRequest; import io.getlime.core.rest.model.base.response.ObjectResponse; @@ -29,48 +27,27 @@ import io.getlime.push.model.response.GetApplicationDetailResponse; import io.getlime.push.model.response.GetApplicationListResponse; import io.getlime.push.model.validator.*; -import io.getlime.push.repository.AppCredentialsRepository; import io.getlime.push.repository.model.AppCredentialsEntity; -import io.getlime.push.service.batch.storage.AppCredentialStorageMap; -import io.getlime.push.service.http.HttpCustomizationService; +import io.getlime.push.service.AdministrationService; import jakarta.validation.Valid; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.web.bind.annotation.*; -import java.util.*; +import java.util.List; /** * Controller for administering the push server. * * @author Roman Strobl, roman.strobl@wultra.com */ +@Slf4j +@AllArgsConstructor @RestController @RequestMapping(value = "admin/app") public class AdministrationController { - private static final Logger logger = LoggerFactory.getLogger(AdministrationController.class); - - private final PowerAuthClient powerAuthClient; - private final AppCredentialsRepository appCredentialsRepository; - private final AppCredentialStorageMap appCredentialStorageMap; - private final HttpCustomizationService httpCustomizationService; - - /** - * Constructor with injected fields. - * @param powerAuthClient PowerAuth service client. - * @param appCredentialsRepository Application credentials repository. - * @param appCredentialStorageMap Application credentials storage map. - * @param httpCustomizationService HTTP customization service. - */ - @Autowired - public AdministrationController(PowerAuthClient powerAuthClient, AppCredentialsRepository appCredentialsRepository, AppCredentialStorageMap appCredentialStorageMap, HttpCustomizationService httpCustomizationService) { - this.powerAuthClient = powerAuthClient; - this.appCredentialsRepository = appCredentialsRepository; - this.appCredentialStorageMap = appCredentialStorageMap; - this.httpCustomizationService = httpCustomizationService; - } + private final AdministrationService administrationService; /** * List applications configured in Push Server. @@ -79,18 +56,9 @@ public AdministrationController(PowerAuthClient powerAuthClient, AppCredentialsR @GetMapping(value = "list") public ObjectResponse listApplications() { logger.debug("Received listApplications request"); + final List applications = administrationService.findAllApplications(); final GetApplicationListResponse response = new GetApplicationListResponse(); - final Iterable appCredentials = appCredentialsRepository.findAll(); - final List appList = new ArrayList<>(); - for (AppCredentialsEntity appCredentialsEntity : appCredentials) { - final PushServerApplication app = new PushServerApplication(); - app.setAppId(appCredentialsEntity.getAppId()); - app.setIos(appCredentialsEntity.getIosPrivateKey() != null); - app.setAndroid(appCredentialsEntity.getAndroidPrivateKey() != null); - app.setHuawei(isHuawei(appCredentialsEntity)); - appList.add(app); - } - response.setApplicationList(appList); + response.setApplicationList(applications); logger.debug("The listApplications request succeeded"); return new ObjectResponse<>(response); } @@ -104,27 +72,9 @@ public ObjectResponse listApplications() { public ObjectResponse listUnconfiguredApplications() throws PushServerException { try { logger.debug("Received listUnconfiguredApplications request"); + final List applications = administrationService.findUnconfiguredApplications(); final GetApplicationListResponse response = new GetApplicationListResponse(); - - // Get all applications in PA Server - final List applicationList = getApplicationList().getApplications(); - - // Get all applications that are already set up - final Iterable appCredentials = appCredentialsRepository.findAll(); - - // Compute intersection by app ID - final Set identifiers = new HashSet<>(); - for (AppCredentialsEntity appCred: appCredentials) { - identifiers.add(appCred.getAppId()); - } - for (Application app : applicationList) { - if (!identifiers.contains(app.getApplicationId())) { - final PushServerApplication applicationToAdd = new PushServerApplication(); - applicationToAdd.setAppId(app.getApplicationId()); - // add apps in intersection - response.getApplicationList().add(applicationToAdd); - } - } + response.setApplicationList(applications); logger.debug("The listUnconfiguredApplications request succeeded"); return new ObjectResponse<>(response); } catch (PowerAuthClientException ex) { @@ -148,7 +98,7 @@ public ObjectResponse getApplicationDetail(@Reques throw new PushServerException(errorMessage); } final GetApplicationDetailResponse response = new GetApplicationDetailResponse(); - final AppCredentialsEntity appCredentialsEntity = findAppCredentialsEntityById(requestObject.getAppId()); + final AppCredentialsEntity appCredentialsEntity = administrationService.findAppCredentials(requestObject.getAppId()); final PushServerApplication app = new PushServerApplication(); app.setAppId(appCredentialsEntity.getAppId()); app.setIos(appCredentialsEntity.getIosPrivateKey() != null); @@ -188,18 +138,8 @@ public ObjectResponse createApplication(@RequestBody throw new PushServerException("Request object must not be empty"); } logger.info("Received createApplication request, application ID: {}", requestObject.getAppId()); - String errorMessage = CreateApplicationRequestValidator.validate(requestObject); - final Optional appCredentialsEntityOptional = appCredentialsRepository.findFirstByAppId(requestObject.getAppId()); - if (appCredentialsEntityOptional.isPresent()) { - errorMessage = "Application already exists"; - } - if (errorMessage != null) { - throw new PushServerException(errorMessage); - } - final AppCredentialsEntity appCredentialsEntity = new AppCredentialsEntity(); - appCredentialsEntity.setAppId(requestObject.getAppId()); - final AppCredentialsEntity newAppCredentialsEntity = appCredentialsRepository.save(appCredentialsEntity); - final CreateApplicationResponse response = new CreateApplicationResponse(newAppCredentialsEntity.getAppId()); + final AppCredentialsEntity appCredentials = administrationService.createAppCredentials(requestObject); + final CreateApplicationResponse response = new CreateApplicationResponse(appCredentials.getAppId()); logger.info("The createApplication request succeeded, application ID: {}", requestObject.getAppId()); return new ObjectResponse<>(response); } @@ -221,25 +161,10 @@ public Response updateIos(@RequestBody ObjectRequest request) if (errorMessage != null) { throw new PushServerException(errorMessage); } - final AppCredentialsEntity appCredentialsEntity = findAppCredentialsEntityById(requestObject.getAppId()); - final byte[] privateKeyBytes = Base64.getDecoder().decode(requestObject.getPrivateKeyBase64()); - appCredentialsEntity.setIosPrivateKey(privateKeyBytes); - appCredentialsEntity.setIosTeamId(requestObject.getTeamId()); - appCredentialsEntity.setIosKeyId(requestObject.getKeyId()); - appCredentialsEntity.setIosBundle(requestObject.getBundle()); - appCredentialsEntity.setIosEnvironment(convert(requestObject.getEnvironment())); - appCredentialsRepository.save(appCredentialsEntity); - appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The updateIos request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + administrationService.updateIosAppCredentials(requestObject); return new Response(); } - private static String convert(final ApnsEnvironment environment) { - return Optional.ofNullable(environment) - .map(ApnsEnvironment::getKey) - .orElse(null); - } - /** * Remove iOS configuration. * @param request Remove iOS configuration request. @@ -257,15 +182,7 @@ public Response removeIos(@RequestBody ObjectRequest request) if (errorMessage != null) { throw new PushServerException(errorMessage); } - final AppCredentialsEntity appCredentialsEntity = findAppCredentialsEntityById(requestObject.getAppId()); - appCredentialsEntity.setIosPrivateKey(null); - appCredentialsEntity.setIosTeamId(null); - appCredentialsEntity.setIosKeyId(null); - appCredentialsEntity.setIosBundle(null); - appCredentialsEntity.setIosEnvironment(null); - appCredentialsRepository.save(appCredentialsEntity); - appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The removeIos request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + administrationService.removeIosAppCredentials(requestObject.getAppId()); return new Response(); } @@ -286,13 +203,7 @@ public Response updateAndroid(@RequestBody ObjectRequest r if (errorMessage != null) { throw new PushServerException(errorMessage); } - final AppCredentialsEntity appCredentialsEntity = findAppCredentialsEntityById(requestObject.getAppId()); - final byte[] privateKeyBytes = Base64.getDecoder().decode(requestObject.getPrivateKeyBase64()); - appCredentialsEntity.setAndroidPrivateKey(privateKeyBytes); - appCredentialsEntity.setAndroidProjectId(requestObject.getProjectId()); - appCredentialsRepository.save(appCredentialsEntity); - appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The updateAndroid request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + administrationService.updateAndroidAppCredentials(requestObject); return new Response(); } @@ -313,12 +224,7 @@ public Response removeAndroid(@RequestBody ObjectRequest r if (errorMessage != null) { throw new PushServerException(errorMessage); } - final AppCredentialsEntity appCredentialsEntity = findAppCredentialsEntityById(requestObject.getAppId()); - appCredentialsEntity.setAndroidPrivateKey(null); - appCredentialsEntity.setAndroidProjectId(null); - appCredentialsRepository.save(appCredentialsEntity); - appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The removeAndroid request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + administrationService.removeAndroidAppCredentials(requestObject.getAppId()); return new Response(); } @@ -333,14 +239,7 @@ public Response removeAndroid(@RequestBody ObjectRequest r public Response updateHuawei(@Valid @RequestBody ObjectRequest request) throws PushServerException { final UpdateHuaweiRequest requestObject = request.getRequestObject(); logger.info("Received update Huawei request, application ID: {}", requestObject.getAppId()); - - final AppCredentialsEntity appCredentialsEntity = findAppCredentialsEntityById(requestObject.getAppId()); - appCredentialsEntity.setHmsProjectId(requestObject.getProjectId()); - appCredentialsEntity.setHmsClientId(requestObject.getClientId()); - appCredentialsEntity.setHmsClientSecret(requestObject.getClientSecret()); - appCredentialsRepository.save(appCredentialsEntity); - appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The update Huawei request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + administrationService.updateHuaweiAppCredentials(requestObject); return new Response(); } @@ -355,32 +254,7 @@ public Response updateHuawei(@Valid @RequestBody ObjectRequest request) throws PushServerException { final RemoveHuaweiRequest requestObject = request.getRequestObject(); logger.info("Received remove Huawei request, application ID: {}", requestObject.getAppId()); - - final AppCredentialsEntity appCredentialsEntity = findAppCredentialsEntityById(requestObject.getAppId()); - appCredentialsEntity.setHmsProjectId(null); - appCredentialsEntity.setHmsClientSecret(null); - appCredentialsEntity.setHmsClientId(null); - appCredentialsRepository.save(appCredentialsEntity); - appCredentialStorageMap.cleanByKey(appCredentialsEntity.getAppId()); - logger.info("The remove Huawei request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + administrationService.removeHuaweiAppCredentials(requestObject.getAppId()); return new Response(); } - - /** - * Find app credentials entity by ID. - * @param powerAuthAppId App credentials ID. - * @return App credentials entity. - * @throws PushServerException Thrown when application credentials entity does not exists. - */ - private AppCredentialsEntity findAppCredentialsEntityById(String powerAuthAppId) throws PushServerException { - return appCredentialsRepository.findFirstByAppId(powerAuthAppId).orElseThrow(() -> - new PushServerException("Application credentials with entered ID does not exist")); - } - - private com.wultra.security.powerauth.client.model.response.GetApplicationListResponse getApplicationList() throws PowerAuthClientException { - return powerAuthClient.getApplicationList( - httpCustomizationService.getQueryParams(), - httpCustomizationService.getHttpHeaders() - ); - } } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/repository/model/AppCredentialsEntity.java b/powerauth-push-server/src/main/java/io/getlime/push/repository/model/AppCredentialsEntity.java index c51cda195..cc0a21b44 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/repository/model/AppCredentialsEntity.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/repository/model/AppCredentialsEntity.java @@ -22,6 +22,7 @@ import java.io.Serial; import java.io.Serializable; +import java.time.LocalDateTime; /** * Class representing application tokens used to authenticate against APNs, FCM, or HMS services. @@ -112,4 +113,10 @@ public class AppCredentialsEntity implements Serializable { @Column(name = "hms_client_secret") private String hmsClientSecret; + @Column(name = "timestamp_created", nullable = false, columnDefinition = "TIMESTAMP DEFAULT CURRENT_TIMESTAMP") + private LocalDateTime timestampCreated = LocalDateTime.now(); + + @Column(name = "timestamp_last_updated") + private LocalDateTime timestampLastUpdated; + } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/AdministrationService.java b/powerauth-push-server/src/main/java/io/getlime/push/service/AdministrationService.java new file mode 100644 index 000000000..404938429 --- /dev/null +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/AdministrationService.java @@ -0,0 +1,225 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed 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 io.getlime.push.service; + +import com.github.benmanes.caffeine.cache.LoadingCache; +import com.wultra.security.powerauth.client.PowerAuthClient; +import com.wultra.security.powerauth.client.model.entity.Application; +import com.wultra.security.powerauth.client.model.error.PowerAuthClientException; +import io.getlime.push.errorhandling.exceptions.PushServerException; +import io.getlime.push.model.entity.PushServerApplication; +import io.getlime.push.model.enumeration.ApnsEnvironment; +import io.getlime.push.model.request.CreateApplicationRequest; +import io.getlime.push.model.request.UpdateAndroidRequest; +import io.getlime.push.model.request.UpdateHuaweiRequest; +import io.getlime.push.model.request.UpdateIosRequest; +import io.getlime.push.model.validator.CreateApplicationRequestValidator; +import io.getlime.push.repository.AppCredentialsRepository; +import io.getlime.push.repository.model.AppCredentialsEntity; +import io.getlime.push.service.http.HttpCustomizationService; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; +import org.springframework.transaction.support.TransactionSynchronization; +import org.springframework.transaction.support.TransactionSynchronizationManager; + +import java.time.LocalDateTime; +import java.util.*; +import java.util.stream.StreamSupport; + +/** + * Administration service. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@Service +@Transactional +@AllArgsConstructor +@Slf4j +public class AdministrationService { + + private final PowerAuthClient powerAuthClient; + private final AppCredentialsRepository appCredentialsRepository; + private final LoadingCache appRelatedPushClientCache; + private final HttpCustomizationService httpCustomizationService; + + @Transactional(readOnly = true) + public List findAllApplications() { + return StreamSupport.stream(appCredentialsRepository.findAll().spliterator(),false) + .map(appCredentialsEntity -> { + final PushServerApplication app = new PushServerApplication(); + app.setAppId(appCredentialsEntity.getAppId()); + app.setIos(appCredentialsEntity.getIosPrivateKey() != null); + app.setAndroid(appCredentialsEntity.getAndroidPrivateKey() != null); + app.setHuawei(isHuawei(appCredentialsEntity)); + return app; + }) + .toList(); + } + + @Transactional(readOnly = true) + public List findUnconfiguredApplications() throws PowerAuthClientException { + // Get all applications in PA Server + final List applicationList = getApplicationList().getApplications(); + + // Get all applications that are already set up + final Iterable appCredentials = appCredentialsRepository.findAll(); + + // Compute intersection by app ID + final Set identifiers = new HashSet<>(); + for (AppCredentialsEntity appCred: appCredentials) { + identifiers.add(appCred.getAppId()); + } + + final List result = new ArrayList<>(); + for (Application app : applicationList) { + if (!identifiers.contains(app.getApplicationId())) { + final PushServerApplication applicationToAdd = new PushServerApplication(); + applicationToAdd.setAppId(app.getApplicationId()); + result.add(applicationToAdd); + } + } + + return result; + } + + @Transactional(readOnly = true) + public AppCredentialsEntity findAppCredentials(final String appId) throws PushServerException { + return findAppCredentialsByAppId(appId); + } + + public AppCredentialsEntity createAppCredentials(final CreateApplicationRequest request) throws PushServerException { + String errorMessage = CreateApplicationRequestValidator.validate(request); + final Optional appCredentialsEntityOptional = appCredentialsRepository.findFirstByAppId(request.getAppId()); + if (appCredentialsEntityOptional.isPresent()) { + errorMessage = "Application already exists"; + } + if (errorMessage != null) { + throw new PushServerException(errorMessage); + } + final AppCredentialsEntity appCredentialsEntity = new AppCredentialsEntity(); + appCredentialsEntity.setAppId(request.getAppId()); + return appCredentialsRepository.save(appCredentialsEntity); + } + + public void updateIosAppCredentials(final UpdateIosRequest request) throws PushServerException { + final AppCredentialsEntity appCredentialsEntity = findAppCredentialsByAppId(request.getAppId()); + final byte[] privateKeyBytes = Base64.getDecoder().decode(request.getPrivateKeyBase64()); + appCredentialsEntity.setIosPrivateKey(privateKeyBytes); + appCredentialsEntity.setIosTeamId(request.getTeamId()); + appCredentialsEntity.setIosKeyId(request.getKeyId()); + appCredentialsEntity.setIosBundle(request.getBundle()); + appCredentialsEntity.setIosEnvironment(convert(request.getEnvironment())); + appCredentialsEntity.setTimestampLastUpdated(LocalDateTime.now()); + appCredentialsRepository.save(appCredentialsEntity); + refreshCacheAfterCommit(appCredentialsEntity.getAppId()); + logger.info("The updateIos request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + } + + public void removeIosAppCredentials(final String appId) throws PushServerException { + final AppCredentialsEntity appCredentialsEntity = findAppCredentialsByAppId(appId); + appCredentialsEntity.setIosPrivateKey(null); + appCredentialsEntity.setIosTeamId(null); + appCredentialsEntity.setIosKeyId(null); + appCredentialsEntity.setIosBundle(null); + appCredentialsEntity.setIosEnvironment(null); + appCredentialsEntity.setTimestampLastUpdated(LocalDateTime.now()); + appCredentialsRepository.save(appCredentialsEntity); + refreshCacheAfterCommit(appCredentialsEntity.getAppId()); + logger.info("The removeIos request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + } + + public void updateAndroidAppCredentials(final UpdateAndroidRequest request) throws PushServerException { + final AppCredentialsEntity appCredentialsEntity = findAppCredentialsByAppId(request.getAppId()); + final byte[] privateKeyBytes = Base64.getDecoder().decode(request.getPrivateKeyBase64()); + appCredentialsEntity.setAndroidPrivateKey(privateKeyBytes); + appCredentialsEntity.setAndroidProjectId(request.getProjectId()); + appCredentialsEntity.setTimestampLastUpdated(LocalDateTime.now()); + appCredentialsRepository.save(appCredentialsEntity); + refreshCacheAfterCommit(appCredentialsEntity.getAppId()); + logger.info("The updateAndroid request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + } + + public void removeAndroidAppCredentials(final String appId) throws PushServerException { + final AppCredentialsEntity appCredentialsEntity = findAppCredentialsByAppId(appId); + appCredentialsEntity.setAndroidPrivateKey(null); + appCredentialsEntity.setAndroidProjectId(null); + appCredentialsEntity.setTimestampLastUpdated(LocalDateTime.now()); + appCredentialsRepository.save(appCredentialsEntity); + refreshCacheAfterCommit(appCredentialsEntity.getAppId()); + logger.info("The removeAndroid request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + } + + public void updateHuaweiAppCredentials(final UpdateHuaweiRequest request) throws PushServerException { + final AppCredentialsEntity appCredentialsEntity = findAppCredentialsByAppId(request.getAppId()); + appCredentialsEntity.setHmsProjectId(request.getProjectId()); + appCredentialsEntity.setHmsClientId(request.getClientId()); + appCredentialsEntity.setHmsClientSecret(request.getClientSecret()); + appCredentialsEntity.setTimestampLastUpdated(LocalDateTime.now()); + appCredentialsRepository.save(appCredentialsEntity); + refreshCacheAfterCommit(appCredentialsEntity.getAppId()); + logger.info("The update Huawei request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + } + + public void removeHuaweiAppCredentials(final String appId) throws PushServerException { + final AppCredentialsEntity appCredentialsEntity = findAppCredentialsByAppId(appId); + appCredentialsEntity.setHmsProjectId(null); + appCredentialsEntity.setHmsClientSecret(null); + appCredentialsEntity.setHmsClientId(null); + appCredentialsEntity.setTimestampLastUpdated(LocalDateTime.now()); + appCredentialsRepository.save(appCredentialsEntity); + refreshCacheAfterCommit(appId); + logger.info("The remove Huawei request succeeded, application credentials entity ID: {}", appCredentialsEntity.getId()); + } + + private void refreshCacheAfterCommit(final String appId) { + TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() { + @Override + public void afterCommit() { + appRelatedPushClientCache.refresh(appId); + } + }); + } + + /** + * Find app credentials entity by ID. + * @param powerAuthAppId App credentials ID. + * @return App credentials entity. + * @throws PushServerException Thrown when application credentials entity does not exists. + */ + private AppCredentialsEntity findAppCredentialsByAppId(String powerAuthAppId) throws PushServerException { + return appCredentialsRepository.findFirstByAppId(powerAuthAppId).orElseThrow(() -> + new PushServerException("Application credentials with entered ID: %s does not exist".formatted(powerAuthAppId))); + } + + private com.wultra.security.powerauth.client.model.response.GetApplicationListResponse getApplicationList() throws PowerAuthClientException { + return powerAuthClient.getApplicationList( + httpCustomizationService.getQueryParams(), + httpCustomizationService.getHttpHeaders() + ); + } + + private static String convert(final ApnsEnvironment environment) { + return Optional.ofNullable(environment) + .map(ApnsEnvironment::getKey) + .orElse(null); + } + + private static boolean isHuawei(final AppCredentialsEntity appCredentialsEntity) { + return appCredentialsEntity.getHmsClientSecret() != null && appCredentialsEntity.getHmsClientId() != null; + } +} diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/AppRelatedPushClientCacheLoader.java b/powerauth-push-server/src/main/java/io/getlime/push/service/AppRelatedPushClientCacheLoader.java new file mode 100644 index 000000000..37a66440c --- /dev/null +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/AppRelatedPushClientCacheLoader.java @@ -0,0 +1,105 @@ +/* + * Copyright 2024 Wultra s.r.o. + * + * Licensed 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 io.getlime.push.service; + +import com.eatthepath.pushy.apns.ApnsClient; +import com.github.benmanes.caffeine.cache.CacheLoader; +import io.getlime.push.errorhandling.exceptions.PushServerException; +import io.getlime.push.repository.AppCredentialsRepository; +import io.getlime.push.repository.model.AppCredentialsEntity; +import io.getlime.push.service.fcm.FcmClient; +import io.getlime.push.service.hms.HmsClient; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.util.Objects; + +/** + * Specialization of {@link CacheLoader} for {@link AppRelatedPushClient}. + * + * @author Lubos Racansky, lubos.racansky@wultra.com + */ +@AllArgsConstructor +@Slf4j +@Component +public class AppRelatedPushClientCacheLoader implements CacheLoader { + + private final AppCredentialsRepository appCredentialsRepository; + + private final PushSendingWorker pushSendingWorker; + + /** + * Smartly reload {@link AppRelatedPushClient}. + * Fetch {@link AppCredentialsEntity#getTimestampLastUpdated()} and reload only if the value differs from the value store in the cache. + *

+ * {@inheritDoc} + */ + @Override + public AppRelatedPushClient reload(final String appId, final AppRelatedPushClient oldAppRelatedPushClient) throws Exception { + final AppCredentialsEntity credentials = appCredentialsRepository.findFirstByAppId(appId).orElse(null); + if (credentials == null) { + logger.warn("AppCredentials does not exist anymore for app: {}", appId); + return null; + } + + final LocalDateTime lastUpdatedInDb = credentials.getTimestampLastUpdated(); + final LocalDateTime lastUpdatedInCache = oldAppRelatedPushClient.getAppCredentials().getTimestampLastUpdated(); + if (Objects.equals(lastUpdatedInCache, lastUpdatedInDb)) { + logger.debug("LastUpdated is same for app: {}", appId); + return oldAppRelatedPushClient; + } + + logger.debug("LastUpdated differs for app: {}", appId); + return createPushClient(credentials); + } + + @Override + public AppRelatedPushClient load(final String appId) throws Exception { + final AppCredentialsEntity credentials = appCredentialsRepository.findFirstByAppId(appId).orElse(null); + if (credentials == null) { + logger.warn("AppCredentials does not exist for app: {}", appId); + return null; + } + + return createPushClient(credentials); + } + + private AppRelatedPushClient createPushClient(final AppCredentialsEntity credentials) throws PushServerException { + logger.info("Creating APNS, FCM, and HMS clients for app: {}", credentials.getAppId()); + + final AppRelatedPushClient pushClient = new AppRelatedPushClient(); + pushClient.setAppCredentials(credentials); + + if (credentials.getIosPrivateKey() != null) { + final ApnsClient apnsClient = pushSendingWorker.prepareApnsClient(credentials); + pushClient.setApnsClient(apnsClient); + } + + if (credentials.getAndroidPrivateKey() != null) { + final FcmClient fcmClient = pushSendingWorker.prepareFcmClient(credentials.getAndroidProjectId(), credentials.getAndroidPrivateKey()); + pushClient.setFcmClient(fcmClient); + } + + if (credentials.getHmsClientId() != null) { + final HmsClient hmsClient = pushSendingWorker.prepareHmsClient(credentials); + pushClient.setHmsClient(hmsClient); + } + + return pushClient; + } +} diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/PushMessageSenderService.java b/powerauth-push-server/src/main/java/io/getlime/push/service/PushMessageSenderService.java index 77b8e21bc..7bda96d9e 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/service/PushMessageSenderService.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/PushMessageSenderService.java @@ -16,7 +16,7 @@ package io.getlime.push.service; -import com.eatthepath.pushy.apns.ApnsClient; +import com.github.benmanes.caffeine.cache.LoadingCache; import io.getlime.push.configuration.PushServiceConfiguration; import io.getlime.push.errorhandling.exceptions.PushServerException; import io.getlime.push.model.entity.*; @@ -30,12 +30,8 @@ import io.getlime.push.repository.model.Platform; import io.getlime.push.repository.model.PushDeviceRegistrationEntity; import io.getlime.push.repository.model.PushMessageEntity; -import io.getlime.push.service.batch.storage.AppCredentialStorageMap; -import io.getlime.push.service.fcm.FcmClient; -import io.getlime.push.service.hms.HmsClient; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.beans.factory.annotation.Autowired; +import lombok.AllArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Service; import java.util.List; @@ -46,42 +42,18 @@ * * @author Petr Dvorak, petr@wultra.com */ +@Slf4j +@AllArgsConstructor @Service public class PushMessageSenderService { - private static final Logger logger = LoggerFactory.getLogger(PushMessageSenderService.class); - private final PushSendingWorker pushSendingWorker; private final AppCredentialsRepository appCredentialsRepository; private final PushDeviceRepository pushDeviceRepository; private final PushMessageDAO pushMessageDAO; - private final AppCredentialStorageMap appRelatedPushClientMap; + private final LoadingCache appRelatedPushClientCache; private final PushServiceConfiguration configuration; - /** - * Constructor with autowired dependencies. - * @param appCredentialsRepository App credentials repository. - * @param pushDeviceRepository Push service repository. - * @param pushMessageDAO Push message DAO. - * @param pushSendingWorker Push sending worker. - * @param appRelatedPushClientMap Map with cached push clients in a map. - * @param configuration Push service configuration. - */ - @Autowired - public PushMessageSenderService(AppCredentialsRepository appCredentialsRepository, - PushDeviceRepository pushDeviceRepository, - PushMessageDAO pushMessageDAO, - PushSendingWorker pushSendingWorker, - AppCredentialStorageMap appRelatedPushClientMap, - PushServiceConfiguration configuration) { - this.appCredentialsRepository = appCredentialsRepository; - this.pushDeviceRepository = pushDeviceRepository; - this.pushMessageDAO = pushMessageDAO; - this.pushSendingWorker = pushSendingWorker; - this.appRelatedPushClientMap = appRelatedPushClientMap; - this.configuration = configuration; - } - /** * Send push notifications to given application. * @@ -92,7 +64,6 @@ public PushMessageSenderService(AppCredentialsRepository appCredentialsRepositor * @throws PushServerException In case push message sending fails. */ public BasePushMessageSendResult sendPushMessage(final String appId, final Mode mode, List pushMessageList) throws PushServerException { - // Prepare clients final AppRelatedPushClient pushClient = prepareClients(appId); // Prepare synchronization primitive for parallel push message sending @@ -225,7 +196,6 @@ public void sendCampaignMessage(String appId, Platform platform, String token, P * the error can be found in exception message. */ public void sendCampaignMessage(final String appId, Platform platform, final String token, PushMessageBody pushMessageBody, PushMessageAttributes attributes, Priority priority, String userId, Long deviceId, String activationId) throws PushServerException { - final AppRelatedPushClient pushClient = prepareClients(appId); final PushMessageEntity pushMessageObject = pushMessageDAO.storePushMessageObject(pushMessageBody, attributes, userId, activationId, deviceId); @@ -281,31 +251,12 @@ private List getPushDevices(Long appCredentialsId, return devices; } - // Prepare and cache APNS, FCM, and HMS clients for provided app private AppRelatedPushClient prepareClients(String appId) throws PushServerException { - synchronized (this) { - AppRelatedPushClient pushClient = appRelatedPushClientMap.get(appId); - if (pushClient == null) { - final AppCredentialsEntity credentials = getAppCredentials(appId); - pushClient = new AppRelatedPushClient(); - if (credentials.getIosPrivateKey() != null) { - final ApnsClient apnsClient = pushSendingWorker.prepareApnsClient(credentials); - pushClient.setApnsClient(apnsClient); - } - if (credentials.getAndroidPrivateKey() != null) { - final FcmClient fcmClient = pushSendingWorker.prepareFcmClient(credentials.getAndroidProjectId(), credentials.getAndroidPrivateKey()); - pushClient.setFcmClient(fcmClient); - } - if (credentials.getHmsClientId() != null) { - final HmsClient hmsClient = pushSendingWorker.prepareHmsClient(credentials); - pushClient.setHmsClient(hmsClient); - } - pushClient.setAppCredentials(credentials); - appRelatedPushClientMap.put(appId, pushClient); - logger.info("Creating APNS, FCM, and HMS clients for app {}", appId); - } - return pushClient; + final AppRelatedPushClient pushClient = appRelatedPushClientCache.get(appId); + if (pushClient == null) { + throw new PushServerException("AppCredentials not found: " + appId); } + return pushClient; } diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/batch/UserDeviceItemWriter.java b/powerauth-push-server/src/main/java/io/getlime/push/service/batch/UserDeviceItemWriter.java index f890cc063..b747f6f0a 100644 --- a/powerauth-push-server/src/main/java/io/getlime/push/service/batch/UserDeviceItemWriter.java +++ b/powerauth-push-server/src/main/java/io/getlime/push/service/batch/UserDeviceItemWriter.java @@ -24,13 +24,15 @@ import io.getlime.push.repository.model.aggregate.UserDevice; import io.getlime.push.repository.serialization.JsonSerialization; import io.getlime.push.service.PushMessageSenderService; -import io.getlime.push.service.batch.storage.CampaignMessageStorageMap; import org.springframework.batch.core.configuration.annotation.StepScope; import org.springframework.batch.item.Chunk; import org.springframework.batch.item.ItemWriter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.ConcurrentMap; + /** * Item writer that send notification to directed device and save message to database. * @@ -45,7 +47,7 @@ public class UserDeviceItemWriter implements ItemWriter { private final JsonSerialization jsonSerialization; // Non-autowired fields - private final CampaignMessageStorageMap campaignStorageMap = new CampaignMessageStorageMap(); + private final ConcurrentMap campaignMessageStorage = new ConcurrentHashMap<>(); /** * Constructor with autowired dependencies. @@ -76,13 +78,12 @@ public void write(Chunk list) throws Exception { final Long deviceId = device.getDeviceId(); final String activationId = device.getActivationId(); - // Load and cache campaign information - PushCampaignEntity campaign = campaignStorageMap.get(campaignId); + final PushCampaignEntity campaign = campaignMessageStorage.computeIfAbsent(campaignId, k -> + pushCampaignRepository.findById(k).orElse(null)); if (campaign == null) { - campaign = pushCampaignRepository.findById(campaignId).orElseThrow(() -> - new PushServerException("Campaign with entered ID does not exist")); - campaignStorageMap.put(campaignId, campaign); + throw new PushServerException("Campaign with entered ID: %s does not exist".formatted(campaign)); } + final PushMessageBody messageBody = jsonSerialization.deserializePushMessageBody(campaign.getMessage()); // Send the push message using push sender service diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/batch/storage/AppCredentialStorageMap.java b/powerauth-push-server/src/main/java/io/getlime/push/service/batch/storage/AppCredentialStorageMap.java deleted file mode 100644 index a6f3c6e2c..000000000 --- a/powerauth-push-server/src/main/java/io/getlime/push/service/batch/storage/AppCredentialStorageMap.java +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright 2016 Wultra s.r.o. - * - * Licensed 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 io.getlime.push.service.batch.storage; - -import io.getlime.push.service.AppRelatedPushClient; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - -/** - * Simple in-memory storage cache for app credentials and push service clients. - * Uses {@link ConcurrentHashMap} as an underlying storage. - * - * @author Petr Dvorak, petr@wultra.com - */ -public class AppCredentialStorageMap implements ItemStorageMap { - - private final ConcurrentMap map = new ConcurrentHashMap<>(); - - @Override - public AppRelatedPushClient get(String key) { - return map.get(key); - } - - @Override - public void put(String key, AppRelatedPushClient value) { - map.put(key, value); - } - - @Override - public boolean contains(String key) { - return map.containsKey(key); - } - - @Override - public void cleanAll() { - map.clear(); - } - - @Override - public void cleanByKey(String key) { - map.remove(key); - } -} diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/batch/storage/CampaignMessageStorageMap.java b/powerauth-push-server/src/main/java/io/getlime/push/service/batch/storage/CampaignMessageStorageMap.java deleted file mode 100644 index a7cadacd3..000000000 --- a/powerauth-push-server/src/main/java/io/getlime/push/service/batch/storage/CampaignMessageStorageMap.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Copyright 2016 Wultra s.r.o. - * - * Licensed 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 io.getlime.push.service.batch.storage; - -import io.getlime.push.repository.model.PushCampaignEntity; - -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ConcurrentMap; - - -/** - * Simple to use class for storing campaigns. Simple in-memory storage cache for app credentials. Uses {@link ConcurrentHashMap} - * as an underlying storage. - * - * @author Petr Dvorak, petr@wultra.com - */ -public class CampaignMessageStorageMap implements ItemStorageMap { - - private final ConcurrentMap mapStorage = new ConcurrentHashMap<>(); - - @Override - public PushCampaignEntity get(Long key) { - return mapStorage.get(key); - } - - @Override - public void put(Long key, PushCampaignEntity value) { - mapStorage.put(key, value); - } - - @Override - public boolean contains(Long key) { - return mapStorage.containsKey(key); - } - - @Override - public void cleanAll() { - mapStorage.clear(); - } - - @Override - public void cleanByKey(Long key) { - mapStorage.remove(key); - } -} diff --git a/powerauth-push-server/src/main/java/io/getlime/push/service/batch/storage/ItemStorageMap.java b/powerauth-push-server/src/main/java/io/getlime/push/service/batch/storage/ItemStorageMap.java deleted file mode 100644 index da971f166..000000000 --- a/powerauth-push-server/src/main/java/io/getlime/push/service/batch/storage/ItemStorageMap.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016 Wultra s.r.o. - * - * Licensed 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 io.getlime.push.service.batch.storage; - -/** - * Interface for generic item storage - * @param Key class for item storage. - * @param Value class for item storage. - */ -public interface ItemStorageMap { - - /** - * Get the value from the map. - * @param key Key. - * @return Value from the map for corresponding key. - */ - V get(K key); - - /** - * Put a value for provided key. - * @param key Key. - * @param value Value. - */ - void put(K key, V value); - - /** - * Check if storage contains value for given key. - * @param key Key. - * @return True if there is a value for given key, false otherwise. - */ - boolean contains(K key); - - /** - * Clean all values from the storage. - */ - void cleanAll(); - - /** - * Clean value for provided key. - * @param key Key. - */ - void cleanByKey(K key); - -} diff --git a/powerauth-push-server/src/main/resources/application-dev.properties b/powerauth-push-server/src/main/resources/application-dev.properties index d627f39d5..c3be0fe1d 100644 --- a/powerauth-push-server/src/main/resources/application-dev.properties +++ b/powerauth-push-server/src/main/resources/application-dev.properties @@ -3,3 +3,8 @@ spring.liquibase.enabled=true spring.liquibase.change-log=classpath:db/changelog/db.changelog-module.xml powerauth.push.service.registration.multipleActivations.enabled=true + +powerauth.push.service.clients.cache.refreshAfterWrite=1m + +logging.level.com.wultra=DEBUG +logging.level.io.getlime=DEBUG diff --git a/powerauth-push-server/src/main/resources/application.properties b/powerauth-push-server/src/main/resources/application.properties index c18cd6176..e2ddb54a4 100644 --- a/powerauth-push-server/src/main/resources/application.properties +++ b/powerauth-push-server/src/main/resources/application.properties @@ -53,6 +53,8 @@ powerauth.push.service.registration.multipleActivations.enabled=false powerauth.push.service.registration.retry.backoff=100 powerauth.push.service.registration.retry.maxAttempts=2 +powerauth.push.service.clients.cache.refreshAfterWrite=5m + # APNs Configuration powerauth.push.service.apns.useDevelopment=true powerauth.push.service.apns.proxy.enabled=false