/*
* Copyright 2014-2016 CyberVision, Inc.
*
* 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.kaaproject.kaa.server.operations.service.notification;
import org.kaaproject.kaa.common.dto.EndpointGroupDto;
import org.kaaproject.kaa.common.dto.EndpointGroupStateDto;
import org.kaaproject.kaa.common.dto.EndpointNotificationDto;
import org.kaaproject.kaa.common.dto.EndpointProfileDto;
import org.kaaproject.kaa.common.dto.NotificationDto;
import org.kaaproject.kaa.common.dto.TopicDto;
import org.kaaproject.kaa.common.dto.TopicTypeDto;
import org.kaaproject.kaa.common.hash.EndpointObjectHash;
import org.kaaproject.kaa.common.hash.Sha1HashUtils;
import org.kaaproject.kaa.server.common.Base64Util;
import org.kaaproject.kaa.server.common.dao.EndpointService;
import org.kaaproject.kaa.server.common.dao.NotificationService;
import org.kaaproject.kaa.server.common.dao.TopicService;
import org.kaaproject.kaa.server.operations.pojo.GetNotificationRequest;
import org.kaaproject.kaa.server.operations.pojo.GetNotificationResponse;
import org.kaaproject.kaa.server.operations.service.cache.CacheService;
import org.kaaproject.kaa.server.operations.service.cache.TopicListCacheEntry;
import org.kaaproject.kaa.server.sync.SubscriptionCommand;
import org.kaaproject.kaa.server.sync.SubscriptionCommandType;
import org.kaaproject.kaa.server.sync.TopicState;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.StringJoiner;
import java.util.TimeZone;
/**
* The Class DefaultNotificationDeltaService.
*/
@Service
public class DefaultNotificationDeltaService implements NotificationDeltaService {
/**
* The Constant LOG.
*/
private static final Logger LOG = LoggerFactory.getLogger(DefaultNotificationDeltaService.class);
/**
* The cache service.
*/
@Autowired
CacheService cacheService;
/**
* The notification service.
*/
@Autowired
private NotificationService notificationService;
/**
* The topic service.
*/
@Autowired
private TopicService topicService;
/**
* The endpoint service.
*/
@Autowired
private EndpointService endpointService;
@Override
public TopicListCacheEntry getTopicListHash(String appToken,
String endpointId,
EndpointProfileDto profile) {
LOG.debug("[{}][{}] Calculating new topic list", appToken, endpointId);
List<TopicDto> topics = recalculateTopicList(profile.getGroupState());
Collections.sort(topics);
long[] ids = new long[topics.size()];
StringJoiner joiner = new StringJoiner("|");
for (int i = 0; i < topics.size(); i++) {
Long id = Long.valueOf(topics.get(i).getId());
ids[i] = id.longValue();
joiner.add(id.toString());
}
int simpleHash = Arrays.hashCode(ids);
EndpointObjectHash complexHash = EndpointObjectHash.fromBytes(
Sha1HashUtils.hashToBytes(joiner.toString()));
TopicListCacheEntry entry = new TopicListCacheEntry(simpleHash, complexHash, topics);
cacheService.putTopicList(complexHash, entry);
LOG.debug("[{}][{}] Calculated new topic list {}", appToken, endpointId, entry);
return entry;
}
/*
* (non-Javadoc)
*
* @see org.kaaproject.kaa.server.operations.service.notification.
* NotificationDeltaService
* #getNotificationDelta(org.kaaproject.kaa.server.operations
* .pojo.GetNotificationRequest,
* org.kaaproject.kaa.server.operations.service.delta.HistoryDelta)
*/
@Override
public GetNotificationResponse getNotificationDelta(GetNotificationRequest request) {
String endpointId = Base64Util.encode(request.getProfile());
GetNotificationResponse response = new GetNotificationResponse();
EndpointProfileDto profile = request.getProfile();
Set<String> subscriptionSet = buildSubscriptionSet(profile);
boolean subscriptionSetChanged = false;
if (request.getTopicHash() != profile.getSimpleTopicHash()) {
LOG.debug("[{}] Topic list changed. recalculating topic list", endpointId);
TopicListCacheEntry topicListCache = cacheService.getTopicListByHash(
EndpointObjectHash.fromBytes(profile.getTopicHash()));
List<TopicDto> topicList = topicListCache.getTopics();
LOG.debug("[{}] New topic list contains {} topics", endpointId, topicList.size());
List<String> allPossibleTopics = new ArrayList<>(topicList.size());
for (TopicDto topic : topicList) {
allPossibleTopics.add(topic.getId());
if (topic.getType() == TopicTypeDto.MANDATORY) {
if (subscriptionSet.add(topic.getId())) { // NOSONAR
subscriptionSetChanged = true;
LOG.debug("[{}] added subscription for mandatory topic id: {}, name: {}",
endpointId,
topic.getId(),
topic.getName());
}
}
}
// Remove all topics that are outdated after latest history changes;
if (subscriptionSet.retainAll(allPossibleTopics)) {
subscriptionSetChanged = true;
}
response.setTopicList(topicList);
}
if (request.getSubscriptionCommands() != null) {
for (SubscriptionCommand subscriptionCommand : request.getSubscriptionCommands()) {
if (subscriptionCommand.getCommand() == SubscriptionCommandType.ADD) {
if (subscriptionSet.add(subscriptionCommand.getTopicId())) {
LOG.debug("[{}] added subscription for topic id: {} based on client request",
endpointId, subscriptionCommand.getTopicId());
subscriptionSetChanged = true;
}
} else {
if (subscriptionSet.remove(subscriptionCommand.getTopicId())) {
LOG.debug("[{}] removed subscription for topic id: {} based on client request",
endpointId, subscriptionCommand.getTopicId());
subscriptionSetChanged = true;
}
}
}
}
Map<String, Integer> subscriptionStates = buildTopicStateMap(request, subscriptionSet);
long now = new GregorianCalendar(TimeZone.getTimeZone("UTC")).getTimeInMillis();
List<NotificationDto> notifications = new ArrayList<>();
for (String topicId : subscriptionSet) {
int seqNumber = subscriptionStates.get(topicId);
LOG.debug(
"[{}] fetch new subscriptions for topic id: {}, system schema version {}, "
+ "user schema version {}, starting seq number {}",
endpointId, topicId, profile.getSystemNfVersion(),
profile.getUserNfVersion(), seqNumber);
List<NotificationDto> topicNotifications =
notificationService.findNotificationsByTopicIdAndVersionAndStartSecNum(
topicId, seqNumber, profile.getSystemNfVersion(), profile.getUserNfVersion());
if (topicNotifications != null) {
int count = 0;
for (NotificationDto notification : topicNotifications) {
seqNumber = Math.max(seqNumber, notification.getSecNum());
Date date = notification.getExpiredAt();
if (date != null && date.getTime() > now) {
notifications.add(notification);
count++;
}
}
LOG.debug("[{}] detected {} new subscriptions for topic id: {} ",
endpointId, count, topicId);
subscriptionStates.put(topicId, seqNumber);
}
}
if (request.getAcceptedUnicastNotifications() != null) {
for (String acceptedUnicastId : request.getAcceptedUnicastNotifications()) {
notificationService.removeUnicastNotificationById(acceptedUnicastId);
LOG.debug("[{}] deleted accepted unicast notification {} ",
endpointId, acceptedUnicastId);
}
}
List<EndpointNotificationDto> unicastNotifications =
notificationService.findUnicastNotificationsByKeyHash(
request.getProfile().getEndpointKeyHash());
for (EndpointNotificationDto unicastNotification : unicastNotifications) {
LOG.debug("[{}] detected new unicast notification: {} ",
endpointId, unicastNotification.getId());
LOG.trace("[{}] detected new unicast notification: {} ", endpointId, unicastNotification);
NotificationDto notificationDto = unicastNotification.getNotificationDto();
if (notificationDto != null) {
Date date = notificationDto.getExpiredAt();
if (date != null && date.getTime() > now) {
LOG.trace("[{}] notification expiration time is {}({}) which is later then {}",
endpointId, date.getTime(), date, now);
notificationDto.setId(unicastNotification.getId());
notifications.add(notificationDto);
}
}
}
response.setNotifications(notifications);
response.setSubscriptionStates(subscriptionStates);
if (subscriptionSetChanged) {
LOG.debug("[{}] Updating profile with subscription set. Size {}",
endpointId, subscriptionSet.size());
response.setSubscriptionSetChanged(true);
response.setSubscriptionSet(subscriptionSet);
}
return response;
}
/**
* Convert unicast notification.
*
* @param unicastNotification the unicast notification
* @return the notification dto
*/
private NotificationDto convertUnicastNotification(EndpointNotificationDto unicastNotification) {
NotificationDto dto = unicastNotification.getNotificationDto();
dto.setId(unicastNotification.getId());
return dto;
}
/**
* Builds the topic state map.
*
* @param request the request
* @param subscriptionSet the subscription set
* @return the map
*/
private Map<String, Integer> buildTopicStateMap(GetNotificationRequest request,
Set<String> subscriptionSet) {
Map<String, Integer> topicStates = new HashMap<>();
if (request.getTopicStates() != null) {
for (TopicState topicState : request.getTopicStates()) {
topicStates.put(topicState.getTopicId(), topicState.getSeqNumber());
}
}
Map<String, Integer> subscriptionStates = new HashMap<>();
for (String subscription : subscriptionSet) {
Integer seqNumber = topicStates.get(subscription);
subscriptionStates.put(subscription, seqNumber != null ? seqNumber : 0);
}
return subscriptionStates;
}
/**
* Recalculate topic list.
*
* @param groups endpoint group state list
* @return the list
*/
private List<TopicDto> recalculateTopicList(List<EndpointGroupStateDto> groups) {
Set<TopicDto> topicSet = new HashSet<TopicDto>();
for (EndpointGroupStateDto egs : groups) {
EndpointGroupDto endpointGroup = cacheService.getEndpointGroupById(egs.getEndpointGroupId());
if (endpointGroup.getTopics() != null) {
for (String topicId : endpointGroup.getTopics()) {
TopicDto topic = cacheService.getTopicById(topicId);
topicSet.add(topic);
}
}
}
List<TopicDto> topicList = new ArrayList<>(topicSet);
Collections.sort(topicList, new Comparator<TopicDto>() {
@Override
public int compare(TopicDto o1, TopicDto o2) {
return o1.getId().compareTo(o2.getId());
}
});
return topicList;
}
/**
* Builds the subscription set.
*
* @param profile the profile
* @return the sets the
*/
private Set<String> buildSubscriptionSet(EndpointProfileDto profile) {
return profile.getSubscriptions() != null
? new HashSet<>(profile.getSubscriptions())
: new HashSet<>();
}
/*
* (non-Javadoc)
*
* @see org.kaaproject.kaa.server.operations.service.notification.
* NotificationDeltaService#findNotificationById(java.lang.String)
*/
@Override
public NotificationDto findNotificationById(String notificationId) {
return notificationService.findNotificationById(notificationId);
}
/*
* (non-Javadoc)
*
* @see org.kaaproject.kaa.server.operations.service.notification.
* NotificationDeltaService#findUnicastNotificationById(java.lang.String)
*/
@Override
public NotificationDto findUnicastNotificationById(String unicastNotificationId) {
EndpointNotificationDto notification = notificationService.findUnicastNotificationById(
unicastNotificationId);
if (notification == null) {
return null;
} else {
return convertUnicastNotification(notification);
}
}
}