/*
* 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.client.notification;
import org.kaaproject.kaa.client.channel.NotificationTransport;
import org.kaaproject.kaa.client.context.ExecutorContext;
import org.kaaproject.kaa.client.persistence.KaaClientState;
import org.kaaproject.kaa.common.endpoint.gen.Notification;
import org.kaaproject.kaa.common.endpoint.gen.SubscriptionCommand;
import org.kaaproject.kaa.common.endpoint.gen.SubscriptionCommandType;
import org.kaaproject.kaa.common.endpoint.gen.SubscriptionType;
import org.kaaproject.kaa.common.endpoint.gen.Topic;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Default {@link NotificationManager} implementation.
*
* @author Yaroslav Zeygerman
*/
public class DefaultNotificationManager implements NotificationManager, NotificationProcessor {
private static final Logger LOG = LoggerFactory.getLogger(DefaultNotificationManager.class);
private final ExecutorContext executorContext;
private final NotificationDeserializer deserializer;
private final Set<NotificationListener> mandatoryListeners = new HashSet<>();
private final Map<Long, List<NotificationListener>> optionalListeners = new HashMap<>();
private final Set<NotificationTopicListListener> topicsListeners = new HashSet<>();
private final List<SubscriptionCommand> subscriptionInfo = new LinkedList<>();
private final KaaClientState state;
private Map<Long, Topic> topics = new HashMap<>();
private volatile NotificationTransport transport;
/**
* All-args constructor.
*/
public DefaultNotificationManager(KaaClientState state, ExecutorContext executorContext,
NotificationTransport transport) {
this.state = state;
this.transport = transport;
this.executorContext = executorContext;
this.deserializer = new NotificationDeserializer(executorContext);
Collection<Topic> topicList = state.getTopics();
if (topicList != null) {
for (Topic topic : topicList) {
topics.put(topic.getId(), topic);
}
}
}
@Override
public void addNotificationListener(NotificationListener listener) {
if (listener == null) {
LOG.warn("Failed to add notification listener: null");
throw new IllegalArgumentException("NUll notification listener");
}
synchronized (mandatoryListeners) {
if (!mandatoryListeners.contains(listener)) {
mandatoryListeners.add(listener);
}
}
}
@Override
public void addNotificationListener(Long topicId, NotificationListener listener)
throws UnavailableTopicException {
if (listener == null) {
LOG.warn("Failed to add listener: id={}, listener={}", topicId, null);
throw new IllegalArgumentException("Bad listener data");
}
findTopicById(topicId);
synchronized (optionalListeners) {
List<NotificationListener> listeners = optionalListeners.get(topicId);
if (listeners == null) {
listeners = new LinkedList<>();
optionalListeners.put(topicId, listeners);
}
listeners.add(listener);
}
}
@Override
public void removeNotificationListener(NotificationListener listener) {
if (listener == null) {
LOG.warn("Failed to remove notification listener: null");
throw new IllegalArgumentException("NUll notification listener");
}
synchronized (mandatoryListeners) {
mandatoryListeners.remove(listener);
}
}
@Override
public void removeNotificationListener(Long topicId, NotificationListener listener)
throws UnavailableTopicException {
if (topicId == null || listener == null) {
LOG.warn("Failed to remove listener: id={}, listener={}", topicId, listener);
throw new IllegalArgumentException("Bad listener data");
}
findTopicById(topicId);
synchronized (optionalListeners) {
List<NotificationListener> listeners = optionalListeners.get(topicId);
if (listeners != null) {
listeners.remove(listener);
}
}
}
@Override
public void addTopicListListener(NotificationTopicListListener listener) {
if (listener == null) {
LOG.warn("Failed to add topic list listener: null");
throw new IllegalArgumentException("NUll topic list listener");
}
synchronized (topicsListeners) {
topicsListeners.add(listener);
}
}
@Override
public void removeTopicListListener(NotificationTopicListListener listener) {
if (listener == null) {
LOG.warn("Failed to remove topic list listener: null");
throw new IllegalArgumentException("NUll topic list listener");
}
synchronized (topicsListeners) {
topicsListeners.remove(listener);
}
}
@Override
public List<Topic> getTopics() {
List<Topic> topicList = new LinkedList<Topic>();
synchronized (topics) {
for (Topic topic : topics.values()) {
topicList.add(topic);
}
}
return topicList;
}
@Override
public void subscribeToTopic(Long topicId, boolean forceSync) throws UnavailableTopicException {
Topic topic = findTopicById(topicId);
if (topic.getSubscriptionType() != SubscriptionType.OPTIONAL_SUBSCRIPTION) {
LOG.warn("Failed to subscribe: topic '{}' isn't optional", topicId);
throw new UnavailableTopicException(String.format("Topic '%s' isn't optional", topicId));
}
updateSubscriptionInfo(topicId, SubscriptionCommandType.ADD);
if (forceSync) {
doSync();
}
}
@Override
public void subscribeToTopics(List<Long> topicIds, boolean forceSync)
throws UnavailableTopicException {
List<SubscriptionCommand> subscriptionUpdate = new LinkedList<>();
for (Long id : topicIds) {
Topic topic = findTopicById(id);
if (topic.getSubscriptionType() != SubscriptionType.OPTIONAL_SUBSCRIPTION) {
LOG.warn("Failed to subscribe: topic '{}' isn't optional", id);
throw new UnavailableTopicException(String.format("Topic '%s' isn't optional", id));
}
subscriptionUpdate.add(new SubscriptionCommand(id, SubscriptionCommandType.ADD));
}
updateSubscriptionInfo(subscriptionUpdate);
if (forceSync) {
doSync();
}
}
@Override
public void unsubscribeFromTopic(Long topicId, boolean forceSync)
throws UnavailableTopicException {
Topic topic = findTopicById(topicId);
if (topic.getSubscriptionType() != SubscriptionType.OPTIONAL_SUBSCRIPTION) {
LOG.warn("Failed to unsubscribe: topic '{}' isn't optional", topicId);
throw new UnavailableTopicException(String.format("Topic '%s' isn't optional", topicId));
}
updateSubscriptionInfo(topicId, SubscriptionCommandType.REMOVE);
if (forceSync) {
doSync();
}
}
@Override
public void unsubscribeFromTopics(List<Long> topicIds, boolean forceSync)
throws UnavailableTopicException {
List<SubscriptionCommand> subscriptionUpdate = new LinkedList<>();
for (Long id : topicIds) {
Topic topic = findTopicById(id);
if (topic.getSubscriptionType() != SubscriptionType.OPTIONAL_SUBSCRIPTION) {
LOG.warn("Failed to unsubscribe: topic '{}' isn't optional", id);
throw new UnavailableTopicException(String.format("Topic '%s' isn't optional", id));
}
subscriptionUpdate.add(new SubscriptionCommand(id, SubscriptionCommandType.REMOVE));
}
updateSubscriptionInfo(subscriptionUpdate);
if (forceSync) {
doSync();
}
}
@Override
public void sync() {
doSync();
}
@Override
public void topicsListUpdated(final List<Topic> list) {
Map<Long, Topic> newTopics = new HashMap<>();
synchronized (topics) {
for (Topic topic : list) {
newTopics.put(topic.getId(), topic);
if (topics.remove(topic.getId()) == null) {
state.addTopic(topic);
}
}
synchronized (optionalListeners) {
for (Topic topic : topics.values()) {
optionalListeners.remove(topic.getId());
state.removeTopic(topic.getId());
}
}
topics = newTopics;
}
synchronized (topicsListeners) {
for (final NotificationTopicListListener listener : topicsListeners) {
executorContext.getCallbackExecutor().submit(new Runnable() {
@Override
public void run() {
listener.onListUpdated(list);
}
});
}
}
}
@Override
public void notificationReceived(List<Notification> notifications) throws IOException {
for (Notification notification : notifications) {
try {
Topic topic = findTopicById(notification.getTopicId());
boolean hasOwner = false;
synchronized (optionalListeners) {
List<NotificationListener> listeners = optionalListeners.get(topic.getId());
if (listeners != null && !listeners.isEmpty()) {
hasOwner = true;
notifyListeners(listeners, topic, notification);
}
}
if (!hasOwner) {
synchronized (mandatoryListeners) {
notifyListeners(mandatoryListeners, topic, notification);
}
}
} catch (UnavailableTopicException ex) {
LOG.warn("Received notification for an unknown topic (id={}), exception catched: {}",
notification.getTopicId(), ex);
}
}
}
private void notifyListeners(Collection<NotificationListener> listeners, final Topic topic,
final Notification notification) {
final Collection<NotificationListener> listenersCopy =
new ArrayList<NotificationListener>(listeners);
if (notification.getBody() != null) {
executorContext.getCallbackExecutor().submit(new Runnable() {
@Override
public void run() {
try {
deserializer.notify(
Collections.unmodifiableCollection(listenersCopy),
topic,
notification.getBody().array());
} catch (IOException ex) {
LOG.error("Failed to process notification for topic {}", topic.getId(), ex);
}
}
});
}
}
private void updateSubscriptionInfo(Long id, SubscriptionCommandType type) {
synchronized (subscriptionInfo) {
subscriptionInfo.add(new SubscriptionCommand(id, type));
}
}
private void updateSubscriptionInfo(List<SubscriptionCommand> subscriptionUpdate) {
synchronized (subscriptionInfo) {
subscriptionInfo.addAll(subscriptionUpdate);
}
}
private Topic findTopicById(Long id) throws UnavailableTopicException {
synchronized (topics) {
Topic topic = topics.get(id);
if (topic == null) {
LOG.warn("Failed to find topic: id {} is unknown", id);
throw new UnavailableTopicException(String.format("Topic id '%s' is unknown", id));
}
return topic;
}
}
private void doSync() {
synchronized (subscriptionInfo) {
transport.onSubscriptionChanged(subscriptionInfo);
subscriptionInfo.clear();
transport.sync();
}
}
}