io.getlime.push.service.PushMessageSenderService.java Source code

Java tutorial

Introduction

Here is the source code for io.getlime.push.service.PushMessageSenderService.java

Source

/*
 * Copyright 2016 Lime - HighTech Solutions 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.turo.pushy.apns.ApnsClient;
import com.turo.pushy.apns.ApnsClientBuilder;
import com.turo.pushy.apns.DeliveryPriority;
import com.turo.pushy.apns.PushNotificationResponse;
import com.turo.pushy.apns.auth.ApnsSigningKey;
import com.turo.pushy.apns.proxy.HttpProxyHandlerFactory;
import com.turo.pushy.apns.util.ApnsPayloadBuilder;
import com.turo.pushy.apns.util.SimpleApnsPushNotification;
import com.turo.pushy.apns.util.TokenUtil;
import io.getlime.push.configuration.PushServiceConfiguration;
import io.getlime.push.errorhandling.exceptions.PushServerException;
import io.getlime.push.model.entity.PushMessage;
import io.getlime.push.model.entity.PushMessageAttributes;
import io.getlime.push.model.entity.PushMessageBody;
import io.getlime.push.model.entity.PushMessageSendResult;
import io.getlime.push.model.validator.PushMessageValidator;
import io.getlime.push.repository.AppCredentialsRepository;
import io.getlime.push.repository.PushDeviceRepository;
import io.getlime.push.repository.dao.PushMessageDAO;
import io.getlime.push.repository.model.AppCredentialsEntity;
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.fcm.FcmNotification;
import io.getlime.push.service.fcm.model.FcmSendRequest;
import io.getlime.push.service.fcm.model.FcmSendResponse;
import io.getlime.push.service.fcm.model.base.FcmResult;
import io.netty.util.concurrent.Future;
import io.netty.util.concurrent.GenericFutureListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.util.concurrent.ListenableFuture;
import org.springframework.util.concurrent.ListenableFutureCallback;

import javax.net.ssl.SSLException;
import javax.transaction.Transactional;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Phaser;
import java.util.logging.Level;
import java.util.logging.Logger;

/**
 * Class responsible for sending push notifications to devices based on platform.
 *
 * @author Petr Dvorak, petr@lime-company.eu
 */
@Service
public class PushMessageSenderService {

    private AppCredentialsRepository appCredentialsRepository;
    private PushDeviceRepository pushDeviceRepository;
    private PushMessageDAO pushMessageDAO;
    private PushServiceConfiguration pushServiceConfiguration;
    private AppCredentialStorageMap appRelatedPushClientMap = new AppCredentialStorageMap();

    @Autowired
    public PushMessageSenderService(AppCredentialsRepository appCredentialsRepository,
            PushDeviceRepository pushDeviceRepository, PushMessageDAO pushMessageDAO,
            PushServiceConfiguration pushServiceConfiguration) {
        this.appCredentialsRepository = appCredentialsRepository;
        this.pushDeviceRepository = pushDeviceRepository;
        this.pushMessageDAO = pushMessageDAO;
        this.pushServiceConfiguration = pushServiceConfiguration;
    }

    /**
     * Send push notifications to given application.
     *
     * @param appId App ID used for addressing push messages. Required so that appropriate APNs/FCM credentials can be obtained.
     * @param pushMessageList List with push message objects.
     * @return Result of this batch sending.
     */
    @Transactional
    public PushMessageSendResult sendPushMessage(final Long appId, List<PushMessage> pushMessageList)
            throws PushServerException {
        // Prepare clients
        AppRelatedPushClient pushClient = prepareClients(appId);

        // Prepare synchronization primitive for parallel push message sending
        final Phaser phaser = new Phaser(1);

        // Prepare result object
        final PushMessageSendResult sendResult = new PushMessageSendResult();

        // Send push message batch
        for (PushMessage pushMessage : pushMessageList) {

            // Validate push message before sending
            validatePushMessage(pushMessage);

            // Fetch connected devices
            List<PushDeviceRegistrationEntity> devices = getPushDevices(appId, pushMessage.getUserId(),
                    pushMessage.getActivationId());

            // Iterate over all devices for given user
            for (final PushDeviceRegistrationEntity device : devices) {
                final PushMessageEntity pushMessageObject = pushMessageDAO.storePushMessageObject(
                        pushMessage.getBody(), pushMessage.getAttributes(), pushMessage.getUserId(),
                        pushMessage.getActivationId(), device.getId());

                // Check if given push is not personal, or if it is, that device is in active state.
                // This avoids sending personal notifications to devices that are blocked or removed.
                boolean isMessagePersonal = pushMessage.getAttributes() != null
                        && pushMessage.getAttributes().getPersonal();
                boolean isDeviceActive = device.getActive();
                if (!isMessagePersonal || isDeviceActive) {

                    // Register phaser for synchronization
                    phaser.register();

                    // Decide if the device is iOS or Android and send message accordingly
                    String platform = device.getPlatform();
                    if (platform.equals(PushDeviceRegistrationEntity.Platform.iOS)) {
                        sendMessageToIos(pushClient.getApnsClient(), pushMessage.getBody(),
                                pushMessage.getAttributes(), device.getPushToken(),
                                pushClient.getAppCredentials().getIosBundle(), new PushSendingCallback() {
                                    @Override
                                    public void didFinishSendingMessage(Result result,
                                            Map<String, Object> contextData) {
                                        switch (result) {
                                        case OK: {
                                            sendResult.getIos().setSent(sendResult.getIos().getSent() + 1);
                                            pushMessageObject.setStatus(PushMessageEntity.Status.SENT);
                                            pushMessageDAO.save(pushMessageObject);
                                            break;
                                        }
                                        case PENDING: {
                                            sendResult.getIos().setPending(sendResult.getIos().getPending() + 1);
                                            pushMessageObject.setStatus(PushMessageEntity.Status.PENDING);
                                            pushMessageDAO.save(pushMessageObject);
                                            break;
                                        }
                                        case FAILED: {
                                            sendResult.getIos().setFailed(sendResult.getIos().getFailed() + 1);
                                            pushMessageObject.setStatus(PushMessageEntity.Status.FAILED);
                                            pushMessageDAO.save(pushMessageObject);
                                            break;
                                        }
                                        case FAILED_DELETE: {
                                            sendResult.getIos().setFailed(sendResult.getIos().getFailed() + 1);
                                            pushMessageObject.setStatus(PushMessageEntity.Status.FAILED);
                                            pushMessageDAO.save(pushMessageObject);
                                            pushDeviceRepository.delete(device);
                                            break;
                                        }
                                        }
                                        sendResult.getIos().setTotal(sendResult.getIos().getTotal() + 1);
                                        phaser.arriveAndDeregister();
                                    }
                                });
                    } else if (platform.equals(PushDeviceRegistrationEntity.Platform.Android)) {
                        final String token = device.getPushToken();
                        sendMessageToAndroid(pushClient.getFcmClient(), pushMessage.getBody(),
                                pushMessage.getAttributes(), token, new PushSendingCallback() {
                                    @Override
                                    public void didFinishSendingMessage(Result sendingResult,
                                            Map<String, Object> contextData) {
                                        switch (sendingResult) {
                                        case OK: {
                                            sendResult.getAndroid().setSent(sendResult.getAndroid().getSent() + 1);
                                            pushMessageObject.setStatus(PushMessageEntity.Status.SENT);
                                            pushMessageDAO.save(pushMessageObject);
                                            updateFcmTokenIfNeeded(appId, token, contextData);
                                            break;
                                        }
                                        case PENDING: {
                                            sendResult.getAndroid()
                                                    .setPending(sendResult.getAndroid().getPending() + 1);
                                            pushMessageObject.setStatus(PushMessageEntity.Status.PENDING);
                                            pushMessageDAO.save(pushMessageObject);
                                            break;
                                        }
                                        case FAILED: {
                                            sendResult.getAndroid()
                                                    .setFailed(sendResult.getAndroid().getFailed() + 1);
                                            pushMessageObject.setStatus(PushMessageEntity.Status.FAILED);
                                            pushMessageDAO.save(pushMessageObject);
                                            break;
                                        }
                                        case FAILED_DELETE: {
                                            sendResult.getAndroid()
                                                    .setFailed(sendResult.getAndroid().getFailed() + 1);
                                            pushMessageObject.setStatus(PushMessageEntity.Status.FAILED);
                                            pushMessageDAO.save(pushMessageObject);
                                            pushDeviceRepository.delete(device);
                                            break;
                                        }
                                        }
                                        sendResult.getAndroid().setTotal(sendResult.getAndroid().getTotal() + 1);
                                        phaser.arriveAndDeregister();
                                    }
                                });
                    }
                }
            }
        }
        phaser.arriveAndAwaitAdvance();
        return sendResult;
    }

    /**
     * Send push message content with related message attributes to provided device (platform and token) using
     * credentials for given application. Return the result in the callback.
     *
     * @param appId App ID.
     * @param platform Mobile platform (iOS, Android).
     * @param token Push message token.
     * @param pushMessageBody Push message body.
     * @throws PushServerException In case any issue happens while sending the push message. Detailed information about
     *                             the error can be found in exception message.
     */
    @Transactional
    public void sendCampaignMessage(Long appId, String platform, String token, PushMessageBody pushMessageBody,
            String userId, Long deviceId, String activationId) throws PushServerException {
        sendCampaignMessage(appId, platform, token, pushMessageBody, null, userId, deviceId, activationId);
    }

    /**
     * Send push message content with related message attributes to provided device (platform and token) using
     * credentials for given application. Return the result in the callback.
     *
     * @param appId App ID.
     * @param platform Mobile platform (iOS, Android).
     * @param token Push message token.
     * @param pushMessageBody Push message body.
     * @param attributes Push message attributes.
     * @throws PushServerException In case any issue happens while sending the push message. Detailed information about
     * the error can be found in exception message.
     */
    @Transactional
    public void sendCampaignMessage(final Long appId, String platform, final String token,
            PushMessageBody pushMessageBody, PushMessageAttributes attributes, String userId, Long deviceId,
            String activationId) throws PushServerException {

        final AppRelatedPushClient pushClient = prepareClients(appId);

        final PushMessageEntity pushMessageObject = pushMessageDAO.storePushMessageObject(pushMessageBody,
                attributes, userId, activationId, deviceId);

        if (platform.equals(PushDeviceRegistrationEntity.Platform.iOS)) {
            sendMessageToIos(pushClient.getApnsClient(), pushMessageBody, attributes, token,
                    pushClient.getAppCredentials().getIosBundle(), new PushSendingCallback() {
                        @Override
                        public void didFinishSendingMessage(Result result, Map<String, Object> contextData) {
                            switch (result) {
                            case OK: {
                                pushMessageObject.setStatus(PushMessageEntity.Status.SENT);
                                pushMessageDAO.save(pushMessageObject);
                                break;
                            }
                            case PENDING: {
                                pushMessageObject.setStatus(PushMessageEntity.Status.PENDING);
                                pushMessageDAO.save(pushMessageObject);
                                break;
                            }
                            case FAILED: {
                                pushMessageObject.setStatus(PushMessageEntity.Status.FAILED);
                                pushMessageDAO.save(pushMessageObject);
                                break;
                            }
                            case FAILED_DELETE: {
                                pushMessageObject.setStatus(PushMessageEntity.Status.FAILED);
                                pushMessageDAO.save(pushMessageObject);
                                pushDeviceRepository
                                        .delete(pushDeviceRepository.findFirstByAppIdAndPushToken(appId, token));
                                break;
                            }
                            }
                        }
                    });
        } else if (platform.equals(PushDeviceRegistrationEntity.Platform.Android)) {
            sendMessageToAndroid(pushClient.getFcmClient(), pushMessageBody, attributes, token,
                    new PushSendingCallback() {
                        @Override
                        public void didFinishSendingMessage(Result result, Map<String, Object> contextData) {
                            switch (result) {
                            case OK: {
                                pushMessageObject.setStatus(PushMessageEntity.Status.SENT);
                                pushMessageDAO.save(pushMessageObject);
                                updateFcmTokenIfNeeded(appId, token, contextData);
                                break;
                            }
                            case PENDING: {
                                pushMessageObject.setStatus(PushMessageEntity.Status.PENDING);
                                pushMessageDAO.save(pushMessageObject);
                                break;
                            }
                            case FAILED: {
                                pushMessageObject.setStatus(PushMessageEntity.Status.FAILED);
                                pushMessageDAO.save(pushMessageObject);
                                break;
                            }
                            case FAILED_DELETE: {
                                pushMessageObject.setStatus(PushMessageEntity.Status.FAILED);
                                pushMessageDAO.save(pushMessageObject);
                                pushDeviceRepository
                                        .delete(pushDeviceRepository.findFirstByAppIdAndPushToken(appId, token));
                                break;
                            }
                            }
                        }
                    });
        }
    }

    // Send message to iOS platform
    private void sendMessageToIos(final ApnsClient apnsClient, final PushMessageBody pushMessageBody,
            final PushMessageAttributes attributes, final String pushToken, final String iosTopic,
            final PushSendingCallback callback) throws PushServerException {

        final String token = TokenUtil.sanitizeTokenString(pushToken);
        final String payload = buildApnsPayload(pushMessageBody,
                attributes == null ? false : attributes.getSilent()); // In case there are no attributes, the message is not silent
        Date validUntil = pushMessageBody.getValidUntil();
        final SimpleApnsPushNotification pushNotification = new SimpleApnsPushNotification(token, iosTopic, payload,
                validUntil, DeliveryPriority.IMMEDIATE, pushMessageBody.getCollapseKey());
        final Future<PushNotificationResponse<SimpleApnsPushNotification>> sendNotificationFuture = apnsClient
                .sendNotification(pushNotification);

        sendNotificationFuture.addListener(
                new GenericFutureListener<Future<PushNotificationResponse<SimpleApnsPushNotification>>>() {

                    @Override
                    public void operationComplete(
                            Future<PushNotificationResponse<SimpleApnsPushNotification>> future) throws Exception {
                        try {
                            final PushNotificationResponse<SimpleApnsPushNotification> pushNotificationResponse = future
                                    .get();
                            if (pushNotificationResponse != null) {
                                if (!pushNotificationResponse.isAccepted()) {
                                    Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE,
                                            "Notification rejected by the APNs gateway: "
                                                    + pushNotificationResponse.getRejectionReason());
                                    if (pushNotificationResponse.getRejectionReason().equals("BadDeviceToken")) {
                                        Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE,
                                                "\t... due to bad device token value.");
                                        callback.didFinishSendingMessage(PushSendingCallback.Result.FAILED_DELETE,
                                                null);
                                    } else if (pushNotificationResponse.getTokenInvalidationTimestamp() != null) {
                                        Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE,
                                                "\t... and the token is invalid as of "
                                                        + pushNotificationResponse.getTokenInvalidationTimestamp());
                                        callback.didFinishSendingMessage(PushSendingCallback.Result.FAILED_DELETE,
                                                null);
                                    }
                                } else {
                                    callback.didFinishSendingMessage(PushSendingCallback.Result.OK, null);
                                }
                            } else {
                                Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE,
                                        "Notification rejected by the APNs gateway: unknown error, will retry");
                                callback.didFinishSendingMessage(PushSendingCallback.Result.PENDING, null);
                            }
                        } catch (ExecutionException | InterruptedException e) {
                            callback.didFinishSendingMessage(PushSendingCallback.Result.FAILED, null);
                        }
                    }
                });
    }

    // Send message to Android platform
    private void sendMessageToAndroid(final FcmClient fcmClient, final PushMessageBody pushMessageBody,
            final PushMessageAttributes attributes, final String pushToken, final PushSendingCallback callback)
            throws PushServerException {

        FcmSendRequest request = new FcmSendRequest();
        request.setTo(pushToken);
        request.setData(pushMessageBody.getExtras());
        request.setCollapseKey(pushMessageBody.getCollapseKey());
        if (attributes == null || !attributes.getSilent()) { // if there are no attributes, assume the message is not silent
            FcmNotification notification = new FcmNotification();
            notification.setTitle(pushMessageBody.getTitle());
            notification.setBody(pushMessageBody.getBody());
            notification.setIcon(pushMessageBody.getIcon());
            notification.setSound(pushMessageBody.getSound());
            notification.setTag(pushMessageBody.getCategory());
            request.setNotification(notification);
        }
        final ListenableFuture<ResponseEntity<FcmSendResponse>> future = fcmClient.exchange(request);

        future.addCallback(new ListenableFutureCallback<ResponseEntity<FcmSendResponse>>() {

            @Override
            public void onFailure(Throwable throwable) {
                Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE,
                        "Notification rejected by the FCM gateway: " + throwable.getLocalizedMessage());
                callback.didFinishSendingMessage(PushSendingCallback.Result.FAILED, null);
            }

            @Override
            public void onSuccess(ResponseEntity<FcmSendResponse> response) {
                for (FcmResult fcmResult : response.getBody().getFcmResults()) {
                    if (fcmResult.getMessageId() != null) {
                        // no issues, straight sending
                        if (fcmResult.getRegistrationId() == null) {
                            Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.INFO,
                                    "Notification sent");
                            callback.didFinishSendingMessage(PushSendingCallback.Result.OK, null);
                        } else {
                            // no issues, straight sending + update token (pass it via context)
                            Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.INFO,
                                    "Notification sent and token has been updated");
                            Map<String, Object> contextData = new HashMap<>();
                            contextData.put(FcmResult.KEY_UPDATE_TOKEN, fcmResult.getRegistrationId());
                            callback.didFinishSendingMessage(PushSendingCallback.Result.OK, contextData);
                        }
                    } else {
                        if (fcmResult.getFcmError() != null) {
                            switch (fcmResult.getFcmError().toLowerCase()) { // make sure to account for case issues
                            // token doesn't exist, remove device registration
                            case "notregistered":
                                Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE,
                                        "Notification rejected by the FCM gateway, invalid token, will be deleted: ");
                                callback.didFinishSendingMessage(PushSendingCallback.Result.FAILED_DELETE, null);
                                break;
                            // retry to send later
                            case "unavailable":
                                Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE,
                                        "Notification rejected by the FCM gateway, will retry to send: ");
                                callback.didFinishSendingMessage(PushSendingCallback.Result.PENDING, null);
                                break;
                            // non-recoverable error, remove device registration
                            default:
                                Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE,
                                        "Notification rejected by the FCM gateway, non-recoverable error, will be deleted: ");
                                callback.didFinishSendingMessage(PushSendingCallback.Result.FAILED_DELETE, null);
                                break;
                            }
                        }
                    }
                }
            }
        });
    }

    // Lookup application credentials by appID and throw exception in case application is not found.
    private AppCredentialsEntity getAppCredentials(Long appId) {
        AppCredentialsEntity credentials = appCredentialsRepository.findFirstByAppId(appId);
        if (credentials == null) {
            throw new IllegalArgumentException("Application not found");
        }
        return credentials;
    }

    // Return list of devices related to given user or activation ID (if present). List of devices is related to particular application as well.
    private List<PushDeviceRegistrationEntity> getPushDevices(Long appId, String userId, String activationId)
            throws PushServerException {
        if (userId == null || userId.isEmpty()) {
            Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE, "No userId was specified");
            throw new PushServerException("No userId was specified");
        }
        List<PushDeviceRegistrationEntity> devices;
        if (activationId != null) { // in case the message should go to the specific device
            devices = pushDeviceRepository.findByUserIdAndAppIdAndActivationId(userId, appId, activationId);
        } else {
            devices = pushDeviceRepository.findByUserIdAndAppId(userId, appId);
        }
        return devices;
    }

    // Prepare and connect APNS client.
    private ApnsClient prepareApnsClient(byte[] apnsPrivateKey, String teamId, String keyId)
            throws PushServerException {
        final ApnsClientBuilder apnsClientBuilder = new ApnsClientBuilder();
        apnsClientBuilder.setProxyHandlerFactory(apnsClientProxy());
        if (pushServiceConfiguration.isApnsUseDevelopment()) {
            apnsClientBuilder.setApnsServer(ApnsClientBuilder.DEVELOPMENT_APNS_HOST);
        } else {
            apnsClientBuilder.setApnsServer(ApnsClientBuilder.PRODUCTION_APNS_HOST);
        }
        try {
            ApnsSigningKey key = ApnsSigningKey.loadFromInputStream(new ByteArrayInputStream(apnsPrivateKey),
                    teamId, keyId);
            apnsClientBuilder.setSigningKey(key);
        } catch (InvalidKeyException | NoSuchAlgorithmException | IOException e) {
            Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE, e.getMessage());
            throw new PushServerException("Invalid private key");
        }
        try {
            return apnsClientBuilder.build();
        } catch (SSLException e) {
            Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.SEVERE, e.getMessage());
            throw new PushServerException("SSL problem");
        }
    }

    // Prepare and connect FCM client
    private FcmClient prepareFcmClient(String serverKey) {
        FcmClient client = new FcmClient(serverKey);
        if (pushServiceConfiguration.isFcmProxyEnabled()) {
            String proxyUrl = pushServiceConfiguration.getFcmProxyUrl();
            int proxyPort = pushServiceConfiguration.getFcmProxyPort();
            String proxyUsername = pushServiceConfiguration.getFcmProxyUsername();
            String proxyPassword = pushServiceConfiguration.getFcmProxyPassword();
            if (proxyUsername != null && proxyUsername.isEmpty()) {
                proxyUsername = null;
            }
            if (proxyPassword != null && proxyPassword.isEmpty()) {
                proxyPassword = null;
            }
            client.setProxy(proxyUrl, proxyPort, proxyUsername, proxyPassword);
        }
        return client;
    }

    // Prepare and cached APNS and FCM clients for provided app
    private AppRelatedPushClient prepareClients(Long appId) throws PushServerException {
        synchronized (this) {
            AppRelatedPushClient pushClient = appRelatedPushClientMap.get(appId);
            if (pushClient == null) {
                final AppCredentialsEntity credentials = getAppCredentials(appId);
                ApnsClient apnsClient = prepareApnsClient(credentials.getIosPrivateKey(),
                        credentials.getIosTeamId(), credentials.getIosKeyId());
                FcmClient fcmClient = prepareFcmClient(credentials.getAndroidServerKey());
                pushClient = new AppRelatedPushClient();
                pushClient.setAppCredentials(credentials);
                pushClient.setApnsClient(apnsClient);
                pushClient.setFcmClient(fcmClient);
                appRelatedPushClientMap.put(appId, pushClient);
            }
            return pushClient;
        }
    }

    // Prepare proxy settings for APNs
    private HttpProxyHandlerFactory apnsClientProxy() {
        if (pushServiceConfiguration.isApnsProxyEnabled()) {
            String proxyUrl = pushServiceConfiguration.getApnsProxyUrl();
            int proxyPort = pushServiceConfiguration.getApnsProxyPort();
            String proxyUsername = pushServiceConfiguration.getApnsProxyUsername();
            String proxyPassword = pushServiceConfiguration.getApnsProxyPassword();
            if (proxyUsername != null && proxyUsername.isEmpty()) {
                proxyUsername = null;
            }
            if (proxyPassword != null && proxyPassword.isEmpty()) {
                proxyPassword = null;
            }
            return new HttpProxyHandlerFactory(new InetSocketAddress(proxyUrl, proxyPort), proxyUsername,
                    proxyPassword);
        }
        return null;
    }

    // Use validator to check there are no errors in push message
    private void validatePushMessage(PushMessage pushMessage) throws PushServerException {
        String error = PushMessageValidator.validatePushMessage(pushMessage);
        if (error != null) {
            Logger.getLogger(PushMessageSenderService.class.getName()).log(Level.WARNING, error);
            throw new PushServerException(error);
        }
    }

    // Update FCM token based on context data
    private void updateFcmTokenIfNeeded(Long appId, String token, Map<String, Object> contextData) {
        if (contextData != null) {
            String updatedToken = (String) contextData.get(FcmResult.KEY_UPDATE_TOKEN);
            if (updatedToken != null) {
                PushDeviceRegistrationEntity device = pushDeviceRepository.findFirstByAppIdAndPushToken(appId,
                        token);
                device.setPushToken(updatedToken);
                pushDeviceRepository.save(device);
            }
        }
    }

    /**
     * Method to build APNs message payload.
     *
     * @param push     Push message object with APNs data.
     * @param isSilent Indicates if the message is silent or not.
     * @return String with APNs JSON payload.
     */
    private String buildApnsPayload(PushMessageBody push, boolean isSilent) {
        final ApnsPayloadBuilder payloadBuilder = new ApnsPayloadBuilder();
        payloadBuilder.setAlertTitle(push.getTitle());
        payloadBuilder.setAlertBody(push.getBody());
        payloadBuilder.setBadgeNumber(push.getBadge());
        payloadBuilder.setCategoryName(push.getCategory());
        payloadBuilder.setSoundFileName(push.getSound());
        payloadBuilder.setContentAvailable(isSilent);
        //payloadBuilder.setThreadId(push.getBody().getCollapseKey());
        Map<String, Object> extras = push.getExtras();
        if (extras != null) {
            for (Map.Entry<String, Object> entry : extras.entrySet()) {
                payloadBuilder.addCustomProperty(entry.getKey(), entry.getValue());
            }
        }
        return payloadBuilder.buildWithDefaultMaximumLength();
    }
}