/*
* 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.akka.actors.core.user;
import akka.actor.ActorContext;
import org.kaaproject.kaa.server.operations.service.akka.AkkaContext;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.route.RouteOperation;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointUserConfigurationUpdate;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.EndpointUserConfigurationUpdateMessage;
import org.kaaproject.kaa.server.operations.service.akka.messages.core.user.UserConfigurationUpdate;
import org.kaaproject.kaa.server.operations.service.event.EventService;
import org.kaaproject.kaa.server.operations.service.event.GlobalRouteInfo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
public class GlobalUserActorMessageProcessor {
private static final Logger LOG = LoggerFactory.getLogger(GlobalUserActor.class);
private final EventService eventService;
private final String userId;
private final String tenantId;
private final GlobalRouteTable<ConfigurationKey> map;
private final Map<ConfigurationKey, byte[]> ucfHashes;
/**
* Create new instance of <code>GlobalUserActorMessageProcessor</code>.
*
* @param context the context
* @param userId the user is
* @param tenantId the tenant id
*/
public GlobalUserActorMessageProcessor(AkkaContext context, String userId, String tenantId) {
this.eventService = context.getEventService();
this.userId = userId;
this.tenantId = tenantId;
this.map = new GlobalRouteTable<>();
this.ucfHashes = new HashMap<>();
}
/**
* Process message.
*
* @param context the actor context
* @param route the global route info
*/
public void process(ActorContext context, GlobalRouteInfo route) {
if (route.getRouteOperation() == RouteOperation.ADD) {
LOG.debug("[{}][{}] Adding route {} for cf version {}",
tenantId, userId, route, route.getCfVersion());
ConfigurationKey key = ConfigurationKey.fromRouteInfo(route);
map.add(key, route);
checkHashAndSendNotification(context, key, route);
} else if (route.getRouteOperation() == RouteOperation.DELETE) {
LOG.debug("[{}][{}] Remove route {} for cf version {}",
tenantId, userId, route, route.getCfVersion());
map.remove(route);
} else {
LOG.warn("[{}][{}] unsupported route operations {}",
tenantId, userId, route.getRouteOperation());
}
}
/**
* Process message.
*
* @param context the actor context
* @param update the user configuration update
*/
public void process(ActorContext context, UserConfigurationUpdate update) {
LOG.debug("Processing notification {}", update);
ConfigurationKey key = ConfigurationKey.fromUpdateMessage(update);
ucfHashes.put(key, update.getHash());
sendStateUpdatesToLocalServers(context, key, update);
sendStateUpdatesToRemoteServers(context, key, update);
}
private void sendStateUpdatesToLocalServers(ActorContext context,
ConfigurationKey key,
UserConfigurationUpdate update) {
for (GlobalRouteInfo route : map.getLocalRoutes(key)) {
checkHashAndSendNotification(context, update.getHash(), route);
}
}
private void sendStateUpdatesToRemoteServers(ActorContext context,
ConfigurationKey key,
UserConfigurationUpdate update) {
Map<String, Set<GlobalRouteInfo>> routes = map.getRemoteRoutes(key);
for (Entry<String, Set<GlobalRouteInfo>> entry : routes.entrySet()) {
LOG.debug("Sending notification to {} about configuration update", entry.getKey());
for (GlobalRouteInfo route : entry.getValue()) {
checkHashAndSendNotification(context, update.getHash(), route);
}
}
}
private void checkHashAndSendNotification(ActorContext context,
ConfigurationKey key,
GlobalRouteInfo route) {
byte[] currentUcfHash = ucfHashes.get(key);
if (currentUcfHash != null) {
checkHashAndSendNotification(context, currentUcfHash, route);
} else {
LOG.trace("No updates for key {} yet", key);
}
}
private void checkHashAndSendNotification(ActorContext context, byte[] newHash,
GlobalRouteInfo route) {
if (!Arrays.equals(newHash, route.getUcfHash())) {
LOG.trace("Sending notification to route {}", route);
if (route.isLocal()) {
context.parent()
.tell(
new EndpointUserConfigurationUpdateMessage(toUpdate(newHash, route)),
context.self());
} else {
eventService
.sendEndpointStateInfo(route.getAddress().getServerId(), toUpdate(newHash, route));
}
} else {
LOG.trace("Ignoring notification to route {} due to matching hashes", route);
}
}
private EndpointUserConfigurationUpdate toUpdate(byte[] newHash, GlobalRouteInfo route) {
return new EndpointUserConfigurationUpdate(
tenantId, userId, route.getAddress().getApplicationToken(),
route.getAddress().getEndpointKey(), newHash);
}
/**
* Process cluster update.
*
* @param context the actor context
*/
public void processClusterUpdate(ActorContext context) {
if (!eventService.isMainUserNode(userId)) {
LOG.trace("No longer a global user node for user {}", userId);
map.clear();
context.stop(context.self());
}
}
private static class ConfigurationKey {
private final int schemaVersion;
private final String appToken;
private ConfigurationKey(int schemaVersion, String appToken) {
super();
this.schemaVersion = schemaVersion;
this.appToken = appToken;
}
public static ConfigurationKey fromRouteInfo(GlobalRouteInfo route) {
return new ConfigurationKey(route.getCfVersion(), route.getAddress().getApplicationToken());
}
public static ConfigurationKey fromUpdateMessage(UserConfigurationUpdate msg) {
return new ConfigurationKey(msg.getSchemaVersion(), msg.getApplicationToken());
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + ((appToken == null) ? 0 : appToken.hashCode());
result = prime * result + schemaVersion;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
ConfigurationKey other = (ConfigurationKey) obj;
if (appToken == null) {
if (other.appToken != null) {
return false;
}
} else if (!appToken.equals(other.appToken)) {
return false;
}
if (schemaVersion != other.schemaVersion) {
return false;
}
return true;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("ConfigurationKey [schemaVersion=");
builder.append(schemaVersion);
builder.append(", appToken=");
builder.append(appToken);
builder.append("]");
return builder.toString();
}
}
private static class GlobalRouteTable<T> {
private final Map<T, Set<GlobalRouteInfo>> routes;
public GlobalRouteTable() {
routes = new HashMap<>();
}
private static Set<GlobalRouteInfo> notNull(Set<GlobalRouteInfo> result) {
if (result != null) {
return result;
} else {
return Collections.emptySet();
}
}
public boolean add(T key, GlobalRouteInfo route) {
Set<GlobalRouteInfo> keyRoutes = routes.get(key);
if (keyRoutes == null) {
keyRoutes = new HashSet<>();
routes.put(key, keyRoutes);
}
return keyRoutes.add(route);
}
public boolean remove(GlobalRouteInfo route) {
boolean found = false;
for (T key : routes.keySet()) {
found = found || routes.get(key).remove(route);
}
return found;
}
public Set<GlobalRouteInfo> getRoutes(T key) {
return notNull(routes.get(key));
}
public Set<GlobalRouteInfo> getLocalRoutes(T key) {
Set<GlobalRouteInfo> result = new HashSet<>();
for (GlobalRouteInfo route : getRoutes(key)) {
if (route.getAddress().getServerId() != null) {
continue;
}
result.add(route);
}
return notNull(result);
}
public Map<String, Set<GlobalRouteInfo>> getRemoteRoutes(T key) {
Map<String, Set<GlobalRouteInfo>> result = new HashMap<>();
for (GlobalRouteInfo route : getRoutes(key)) {
String serverId = route.getAddress().getServerId();
if (serverId == null) {
continue;
}
Set<GlobalRouteInfo> set = result.get(serverId);
if (set == null) {
set = new HashSet<>();
result.put(serverId, set);
}
set.add(route);
}
return result;
}
public void clear() {
routes.clear();
}
}
}