@Override
  public void sendPushMessage(
      Variant variant,
      Collection<String> clientIdentifiers,
      UnifiedPushMessage pushMessage,
      String pushMessageInformationId,
      NotificationSenderCallback senderCallback) {
    final AdmService admService = ADM.newService();

    final PayloadBuilder builder = ADM.newPayload();

    // flatten the "special keys"
    builder.dataField("alert", pushMessage.getMessage().getAlert());

    // if present, apply the time-to-live metadata:
    int ttl = pushMessage.getConfig().getTimeToLive();
    if (ttl != -1) {
      builder.expiresAfter(ttl);
    }

    // dirty hack for cordova,
    // TODO should be removed once we have our clients SDKs, tracked by AGPUSH-1269
    builder.dataField("message", "useless payload");

    // Handle consolidation key
    builder.consolidationKey(pushMessage.getMessage().getConsolidationKey());

    pushMessage
        .getMessage()
        .getUserData()
        .keySet()
        .forEach(key -> builder.dataField(key, pushMessage.getMessage().getUserData().get(key)));

    // add the aerogear-push-id
    builder.dataField(InternalUnifiedPushMessage.PUSH_MESSAGE_ID, pushMessageInformationId);

    final AdmVariant admVariant = (AdmVariant) variant;

    clientIdentifiers.forEach(
        token -> {
          try {
            admService.sendMessageToDevice(
                token, admVariant.getClientId(), admVariant.getClientSecret(), builder.build());
            senderCallback.onSuccess();
          } catch (Exception e) {
            logger.error("Error sending payload to ADM server", e);
            senderCallback.onError(e.getMessage());
          }
        });
    logger.info(
        String.format(
            "Sent push notification to Amazon's ADM Server for %d tokens",
            clientIdentifiers.size()));
  }
  /**
   * Sends APNs notifications ({@link UnifiedPushMessage}) to all devices, that are represented by
   * the {@link Collection} of tokens for the given {@link iOSVariant}.
   *
   * @param iOSVariant the logical construct, needed to lookup the certificate and the passphrase.
   * @param tokens collection of tokens, representing actual iOS devices
   * @param pushMessage the payload to be submitted
   */
  public void sendPushMessage(
      iOSVariant iOSVariant, Collection<String> tokens, UnifiedPushMessage pushMessage) {
    // no need to send empty list
    if (tokens.isEmpty()) {
      return;
    }

    PayloadBuilder builder =
        APNS.newPayload()
            // adding recognized key values
            .alertBody(pushMessage.getAlert()) // alert dialog, in iOS
            .badge(pushMessage.getBadge()) // little badge icon update;
            .sound(pushMessage.getSound()); // sound to be played by app

    // apply the 'content-available:1' value:
    if (pushMessage.isContentAvailable()) {
      // content-available:1 is (with iOS7) not only used
      // Newsstand, however 'notnoop' names it this way (legacy)...
      builder = builder.forNewsstand();
    }

    builder = builder.customFields(pushMessage.getData()); // adding other (submitted) fields

    final String apnsMessage = builder.build(); // build the JSON payload, for APNs

    ApnsService service = buildApnsService(iOSVariant);

    if (service != null) {
      try {
        logger.fine(String.format("Sending transformed APNs payload: '%s' ", apnsMessage));
        // send:
        service.start();

        Date expireDate = createFutureDateBasedOnTTL(pushMessage.getTimeToLive());
        service.push(tokens, apnsMessage, expireDate);

        // after sending, let's ask for the inactive tokens:
        final Set<String> inactiveTokens = service.getInactiveDevices().keySet();

        // transform the tokens to be all lower-case:
        final Set<String> transformedTokens = lowerCaseAllTokens(inactiveTokens);

        // trigger asynchronous deletion:
        clientInstallationService.removeInstallationsForVariantByDeviceTokens(
            iOSVariant.getVariantID(), transformedTokens);
      } catch (RuntimeException e) {
        logger.log(Level.SEVERE, "Error sending messages to APN server", e);
      } finally {

        // tear down and release resources:
        service.stop();
      }
    } else {
      logger.severe("No certificate was found. Could not send messages to APNs");
    }
  }
  /**
   * Receives request for processing a {@link UnifiedPushMessage} and loads tokens for devices that
   * match requested parameters from database.
   *
   * <p>Device tokens are loaded in a stream and split to batches of configured size (see {@link
   * SenderConfiguration#batchSize()}). Once the pre-configured number of batches (see {@link
   * SenderConfiguration#batchesToLoad()}) is reached, this method resends message to the same queue
   * it took the request from, so that the transaction it worked in is split and further processing
   * may continue in next transaction.
   *
   * <p>Additionally it fires {@link BatchLoadedEvent} as CDI event (that is translated to JMS
   * event) that helps {@link MetricsCollector} to track how many batches were loaded. When all
   * batches were loaded for the given variant, it fires {@link AllBatchesLoadedEvent}.
   *
   * @param msg holder object containing the payload and info about the effected variants
   */
  public void loadAndQueueTokenBatch(@Observes @Dequeue MessageHolderWithVariants msg)
      throws IllegalStateException {
    final UnifiedPushMessage message = msg.getUnifiedPushMessage();
    final VariantType variantType = msg.getVariantType();
    final Collection<Variant> variants = msg.getVariants();
    final String lastTokenFromPreviousBatch = msg.getLastTokenFromPreviousBatch();
    final SenderConfiguration configuration =
        senderConfiguration.select(new SenderTypeLiteral(variantType)).get();
    final PushMessageInformation pushMessageInformation = msg.getPushMessageInformation();
    int serialId = msg.getLastSerialId();

    logger.debug("Received message from queue: " + message.getMessage().getAlert());

    final Criteria criteria = message.getCriteria();
    final List<String> categories = criteria.getCategories();
    final List<String> aliases = criteria.getAliases();
    final List<String> deviceTypes = criteria.getDeviceTypes();

    logger.info(
        String.format(
            "Preparing message delivery and loading tokens for the %s 3rd-party Push Network (for %d variants)",
            variantType, variants.size()));

    for (Variant variant : variants) {

      try {

        ResultsStream<String> tokenStream;
        final Set<String> topics = new TreeSet<>();
        final boolean isAndroid = variantType.equals(VariantType.ANDROID);

        // the entire batch size
        int batchesToLoad = configuration.batchesToLoad();

        // Some checks for GCM, because of GCM-3 topics
        boolean gcmTopicRequest = (isAndroid && TokenLoaderUtils.isGCMTopicRequest(criteria));
        if (gcmTopicRequest) {

          // If we are able to do push for GCM topics...

          // 1)
          // find all topics, BUT only on the very first round of batches
          // otherwise after 10 (or what ever the max. is) another request would be sent to that
          // topic
          if (serialId == 0) {
            topics.addAll(TokenLoaderUtils.extractGCMTopics(criteria, variant.getVariantID()));

            // topics are handled as a first extra batch,
            // therefore we have to adjust the number by adding this extra batch
            batchesToLoad = batchesToLoad + 1;
          }

          // 2) always load the legacy tokens, for all number of batch iterations
          tokenStream =
              clientInstallationService
                  .findAllOldGoogleCloudMessagingDeviceTokenForVariantIDByCriteria(
                      variant.getVariantID(),
                      categories,
                      aliases,
                      deviceTypes,
                      configuration.tokensToLoad(),
                      lastTokenFromPreviousBatch)
                  .fetchSize(configuration.batchSize())
                  .executeQuery();
        } else {
          tokenStream =
              clientInstallationService
                  .findAllDeviceTokenForVariantIDByCriteria(
                      variant.getVariantID(),
                      categories,
                      aliases,
                      deviceTypes,
                      configuration.tokensToLoad(),
                      lastTokenFromPreviousBatch)
                  .fetchSize(configuration.batchSize())
                  .executeQuery();
        }

        String lastTokenInBatch = null;
        int tokensLoaded = 0;
        for (int batchNumber = 0; batchNumber < batchesToLoad; batchNumber++) {

          // increasing the serial ID,
          // to make sure it's properly read from all block
          ++serialId;

          final Set<String> tokens = new TreeSet<>();

          // On Android, the first batch is for GCM3 topics
          // legacy tokens are submitted in the batch #2 and later
          if (isAndroid && batchNumber == 0 && !topics.isEmpty()) {
            tokens.addAll(topics);
          } else {
            for (int i = 0; i < configuration.batchSize() && tokenStream.next(); i++) {
              lastTokenInBatch = tokenStream.get();
              tokens.add(lastTokenInBatch);
              tokensLoaded += 1;
            }
          }

          if (tokens.size() > 0) {
            if (tryToDispatchTokens(
                new MessageHolderWithTokens(
                    msg.getPushMessageInformation(), message, variant, tokens, serialId))) {
              logger.info(
                  String.format(
                      "Loaded batch #%s, containing %d tokens, for %s variant (%s)",
                      serialId,
                      tokens.size(),
                      variant.getType().getTypeName(),
                      variant.getVariantID()));
            } else {
              logger.debug(
                  String.format(
                      "Failing token loading transaction for batch token #%s for %s variant (%s), since queue is full, will retry...",
                      serialId, variant.getType().getTypeName(), variant.getVariantID()));
              context.setRollbackOnly();
              return;
            }

            // using combined key of variant and PMI (AGPUSH-1585):
            batchLoaded.fire(
                new BatchLoadedEvent(
                    variant.getVariantID() + ":" + msg.getPushMessageInformation().getId()));
            if (serialId == MessageHolderWithVariants.INITIAL_SERIAL_ID) {
              triggerVariantMetricCollection.fire(
                  new TriggerVariantMetricCollectionEvent(
                      msg.getPushMessageInformation(), variant));
            }
          } else {
            logger.debug(
                String.format(
                    "Ending batch processing: No more tokens for batch #%s available", serialId));
            break;
          }
        }

        // should we trigger next transaction?
        if (tokensLoaded >= configuration.tokensToLoad()) {
          logger.debug(
              String.format(
                  "Ending token loading transaction for %s variant (%s)",
                  variant.getType().getTypeName(), variant.getVariantID()));
          nextBatchEvent.fire(
              new MessageHolderWithVariants(
                  msg.getPushMessageInformation(),
                  message,
                  msg.getVariantType(),
                  variants,
                  serialId,
                  lastTokenInBatch));
        } else {
          logger.debug(
              String.format(
                  "All batches for %s variant were loaded (%s)",
                  variant.getType().getTypeName(), pushMessageInformation.getId()));

          // using combined key of variant and PMI (AGPUSH-1585):
          allBatchesLoaded.fire(
              new AllBatchesLoadedEvent(
                  variant.getVariantID() + ":" + msg.getPushMessageInformation().getId()));
          triggerVariantMetricCollection.fire(
              new TriggerVariantMetricCollectionEvent(pushMessageInformation, variant));

          if (tokensLoaded == 0 && lastTokenFromPreviousBatch == null) {
            // no tokens were loaded at all!
            if (gcmTopicRequest) {
              logger.debug("No legacy(non-InstanceID) tokens found. Just pure GCM topic requests");
            } else {
              logger.warn("Check your push query: Not a single token was loaded from the DB!");
            }

            VariantMetricInformation variantMetricInformation = new VariantMetricInformation();
            variantMetricInformation.setPushMessageInformation(msg.getPushMessageInformation());
            variantMetricInformation.setVariantID(variant.getVariantID());
            variantMetricInformation.setDeliveryStatus(Boolean.TRUE);
            dispatchVariantMetricEvent.fire(variantMetricInformation);
          }
        }
      } catch (ResultStreamException e) {
        logger.error("Failed to load batch of tokens", e);
      }
    }
  }