/**
* JBoss, Home of Professional Open Source
* Copyright Red Hat, Inc., and individual contributors.
*
* 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 org.jboss.aerogear.unifiedpush.service.impl;
import org.jboss.aerogear.unifiedpush.api.AndroidVariant;
import org.jboss.aerogear.unifiedpush.api.Category;
import org.jboss.aerogear.unifiedpush.api.Installation;
import org.jboss.aerogear.unifiedpush.api.Variant;
import org.jboss.aerogear.unifiedpush.api.VariantType;
import org.jboss.aerogear.unifiedpush.dao.CategoryDao;
import org.jboss.aerogear.unifiedpush.dao.InstallationDao;
import org.jboss.aerogear.unifiedpush.dao.ResultsStream;
import org.jboss.aerogear.unifiedpush.service.ClientInstallationService;
import org.jboss.aerogear.unifiedpush.service.annotations.LoggedIn;
import org.jboss.aerogear.unifiedpush.service.util.FCMTopicManager;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import javax.ejb.Asynchronous;
import javax.ejb.Stateless;
import javax.enterprise.inject.Instance;
import javax.inject.Inject;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
/**
* (Default) implementation of the {@code ClientInstallationService} interface.
* Delegates work to an injected DAO object.
*/
@Stateless
public class ClientInstallationServiceImpl implements ClientInstallationService {
private final Logger logger = LoggerFactory.getLogger(ClientInstallationServiceImpl.class);
@Inject
private InstallationDao installationDao;
@Inject
private CategoryDao categoryDao;
@Inject
@LoggedIn
private Instance<String> developer;
@Override
@Asynchronous
public void addInstallation(Variant variant, Installation entity) {
// does it already exist ?
Installation installation = this.findInstallationForVariantByDeviceToken(variant.getVariantID(), entity.getDeviceToken());
// Needed for the Admin UI Only. Help for setting up Routes
entity.setPlatform(variant.getType().getTypeName());
// new device/client ?
if (installation == null) {
logger.trace("Performing new device/client registration");
// store the installation:
storeInstallationAndSetReferences(variant, entity);
} else {
// We only update the metadata, if the device is enabled:
if (installation.isEnabled()) {
logger.trace("Updating received metadata for an 'enabled' installation");
// fix variant property of installation object
installation.setVariant(variant);
// update the entity:
this.updateInstallation(installation, entity);
}
}
}
@Override
@Asynchronous
public void addInstallations(Variant variant, List<Installation> installations) {
// don't bother
if (installations == null || installations.isEmpty()) {
return;
}
Set<String> existingTokens = installationDao.findAllDeviceTokenForVariantID(variant.getVariantID());
// clear out:
installationDao.flushAndClear();
for (int i = 0; i < installations.size(); i++) {
Installation current = installations.get(i);
// let's avoid duplicated tokens/devices per variant
// For devices without a token, let's also not bother the DAO layer to throw BeanValidation exception
if (!existingTokens.contains(current.getDeviceToken()) && hasTokenValue(current)) {
logger.trace("Importing device with token: {}", current.getDeviceToken());
storeInstallationAndSetReferences(variant, current);
// and add a reference to the existing tokens set, to ensure the JSON file contains no duplicates:
existingTokens.add(current.getDeviceToken());
// some tunings, ever 10k devices releasing resources
if (i % 10000 == 0) {
logger.trace("releasing some resources during import");
installationDao.flushAndClear();
}
} else {
// for now, we ignore them.... no update applied!
logger.trace("Device with token '{}' already exists. Ignoring it ", current.getDeviceToken());
}
}
// clear out:
installationDao.flushAndClear();
}
@Override
public void removeInstallations(
List<Installation> installations) {
// uh..., fancy method reference :)
installations.forEach(this::removeInstallation);
}
@Override
public void updateInstallation(
Installation installation) {
installationDao.update(installation);
}
@Override
public void updateInstallation(Installation installationToUpdate, Installation postedInstallation) {
// copy the "updateable" values:
mergeCategories(installationToUpdate, postedInstallation.getCategories());
installationToUpdate.setDeviceToken(postedInstallation.getDeviceToken());
installationToUpdate.setAlias(postedInstallation.getAlias());
installationToUpdate.setDeviceType(postedInstallation.getDeviceType());
installationToUpdate.setOperatingSystem(postedInstallation
.getOperatingSystem());
installationToUpdate.setOsVersion(postedInstallation.getOsVersion());
installationToUpdate.setEnabled(postedInstallation.isEnabled());
installationToUpdate.setPlatform(postedInstallation.getPlatform());
// update it:
updateInstallation(installationToUpdate);
// unsubscribe Android devices from topics that device should no longer be subscribed to
if (installationToUpdate.getVariant().getType() == VariantType.ANDROID) {
unsubscribeOldTopics(installationToUpdate);
}
}
@Override
public Installation findById(String primaryKey) {
return installationDao.find(primaryKey);
}
@Override
public void removeInstallation(Installation installation) {
installationDao.delete(installation);
}
@Override
@Asynchronous
public void removeInstallationsForVariantByDeviceTokens(String variantID, Set<String> deviceTokens) {
// collect inactive installations for the given variant:
List<Installation> inactiveInstallations = installationDao.findInstallationsForVariantByDeviceTokens(variantID, deviceTokens);
// get rid of them
this.removeInstallations(inactiveInstallations);
}
@Override
@Asynchronous
public void removeInstallationForVariantByDeviceToken(String variantID, String deviceToken) {
removeInstallation(findInstallationForVariantByDeviceToken(variantID, deviceToken));
}
@Override
public Installation findInstallationForVariantByDeviceToken(String variantID, String deviceToken) {
return installationDao.findInstallationForVariantByDeviceToken(variantID, deviceToken);
}
@Override
@Asynchronous
public void unsubscribeOldTopics(Installation installation) {
FCMTopicManager topicManager = new FCMTopicManager((AndroidVariant) installation.getVariant());
Set<String> oldCategories = topicManager.getSubscribedCategories(installation);
// Remove current categories from the set of old ones
oldCategories.removeAll(convertToNames(installation.getCategories()));
// Remove global variant topic because we don't want to unsubscribe it
oldCategories.remove(installation.getVariant().getVariantID());
for (String categoryName : oldCategories) {
topicManager.unsubscribe(installation, categoryName);
}
}
// =====================================================================
// ======== Various finder services for the Sender REST API ============
// =====================================================================
/**
* Finder for 'send', used for Android, iOS and SimplePush clients
*/
@Override
public ResultsStream.QueryBuilder<String> findAllDeviceTokenForVariantIDByCriteria(String variantID, List<String> categories, List<String> aliases, List<String> deviceTypes, int maxResults, String lastTokenFromPreviousBatch) {
return installationDao.findAllDeviceTokenForVariantIDByCriteria(variantID, categories, aliases, deviceTypes, maxResults, lastTokenFromPreviousBatch, false);
}
@Override
public ResultsStream.QueryBuilder<String> findAllOldGoogleCloudMessagingDeviceTokenForVariantIDByCriteria(String variantID, List<String> categories, List<String> aliases, List<String> deviceTypes, int maxResults, String lastTokenFromPreviousBatch) {
return installationDao.findAllDeviceTokenForVariantIDByCriteria(variantID, categories, aliases, deviceTypes, maxResults, lastTokenFromPreviousBatch, true);
}
/**
* A simple validation util that checks if a token is present
*/
private static boolean hasTokenValue(Installation installation) {
return installation.getDeviceToken() != null && !installation.getDeviceToken().isEmpty();
}
/**
* When an installation is created or updated, the categories are passed without IDs.
* This method solve this issue by checking for existing categories and updating them (otherwise it would
* persist a new object).
* @param entity to merge the categories for
* @param categoriesToMerge are the categories to merge with the existing one
*/
private void mergeCategories(Installation entity, Set<Category> categoriesToMerge) {
if (entity.getCategories() != null) {
final List<String> categoryNames = convertToNames(categoriesToMerge);
final List<Category> existingCategoriesFromDB = categoryDao.findByNames(categoryNames);
// Replace json dematerialised categories with their persistent counter parts (see Category.equals),
// by remove existing/persistent categories from the new collection, and adding them back in (with their PK).
categoriesToMerge.removeAll(existingCategoriesFromDB);
categoriesToMerge.addAll(existingCategoriesFromDB);
// and apply the passed in ones.
entity.setCategories(categoriesToMerge);
}
}
private static List<String> convertToNames(Set<Category> categories) {
return categories.stream().map(Category::getName).collect(Collectors.toList());
}
/*
* Helper to set references and perform the actual storage
*/
private void storeInstallationAndSetReferences(Variant variant, Installation entity) {
// ensure lower case for iOS
if (variant.getType() == VariantType.IOS) {
entity.setDeviceToken(entity.getDeviceToken().toLowerCase());
}
// set reference
entity.setVariant(variant);
// update attached categories
mergeCategories(entity, entity.getCategories());
// store Installation entity
installationDao.create(entity);
}
}