@DELETE @Path("{token}") public Response unregisterInstallations( @PathParam("token") String token, @Context HttpServletRequest request) { // find the matching variation: final Variant variant = loadVariantWhenAuthorized(request); if (variant == null) { return appendAllowOriginHeader( Response.status(Status.UNAUTHORIZED) .header("WWW-Authenticate", "Basic realm=\"AeroGear UnifiedPush Server\"") .entity("Unauthorized Request"), request); } // look up all installations (with same token) for the given variant: Installation installation = clientInstallationService.findInstallationForVariantByDeviceToken( variant.getVariantID(), token); if (installation == null) { return appendAllowOriginHeader(Response.status(Status.NOT_FOUND), request); } else { logger.log(Level.INFO, "Deleting metadata Installation"); // remove clientInstallationService.removeInstallation(installation); } return appendAllowOriginHeader(Response.noContent(), request); }
@POST @Consumes(MediaType.APPLICATION_JSON) public Response registerInstallation(Installation entity, @Context HttpServletRequest request) { // find the matching variation: final Variant variant = loadVariantWhenAuthorized(request); if (variant == null) { return appendAllowOriginHeader( Response.status(Status.UNAUTHORIZED) .header("WWW-Authenticate", "Basic realm=\"AeroGear UnifiedPush Server\"") .entity("Unauthorized Request"), request); } // Poor validation: We require the Token! And the 'simplePushEndpoint' for SimplePush clients! if (entity.getDeviceToken() == null || (variant.getType() == VariantType.SIMPLE_PUSH && entity.getSimplePushEndpoint() == null)) { return appendAllowOriginHeader(Response.status(Status.BAD_REQUEST), request); } // look up all installations (with same token) for the given variant: Installation installation = clientInstallationService.findInstallationForVariantByDeviceToken( variant.getVariantID(), entity.getDeviceToken()); // Needed for the Admin UI Only. Help for setting up Routes entity.setPlatform(variant.getType().getTypeName()); // The 'mobile application' on the device/client was launched. // If the installation is already in the DB, let's update the metadata, // otherwise we register a new installation: logger.log(Level.FINEST, "Mobile Application on device was launched"); // new device/client ? if (installation == null) { logger.log(Level.FINEST, "Performing new device/client registration"); // store the installation: clientInstallationService.addInstallation(variant.getType(), entity); // add installation to the matching variant genericVariantService.addInstallation(variant, entity); } else { // We only update the metadata, if the device is enabled: if (installation.isEnabled()) { logger.log(Level.FINEST, "Updating received metadata for an 'enabled' installation"); // update the entity: clientInstallationService.updateInstallation(installation, entity); } } return appendAllowOriginHeader(Response.ok(entity), request); }
/** * 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); } } }