/*
* Copyright 2009-2016 Weibo, 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 com.networknt.registry.support.command;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.ConcurrentHashMap;
import java.util.regex.Pattern;
import com.networknt.registry.URLImpl;
import com.networknt.status.Status;
import org.apache.commons.lang3.StringUtils;
import com.networknt.registry.URLParamType;
import com.networknt.exception.FrameworkException;
import com.networknt.registry.NotifyListener;
import com.networknt.registry.URL;
import com.networknt.utility.CollectionUtil;
import com.networknt.utility.ConcurrentHashSet;
import com.networknt.switcher.SwitcherUtil;
import com.networknt.utility.NetUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class CommandServiceManager implements CommandListener, ServiceListener {
private static final Logger logger = LoggerFactory.getLogger(CommandServiceManager.class);
private static final String REGISTRY_IS_NULL = "ERR10024";
private static final String WEIGHT_OUT_OF_RANGE = "ERR10025";
public static final String LIGHT_COMMAND_SWITCHER = "feature.light.command.enable";
private static Pattern IP_PATTERN = Pattern.compile("^!?[0-9.]*\\*?$");
static {
SwitcherUtil.initSwitcher(LIGHT_COMMAND_SWITCHER, true);
}
private URL refUrl;
private ConcurrentHashSet<NotifyListener> notifySet;
private CommandFailbackRegistry registry;
// service cache
private Map<String, List<URL>> groupServiceCache;
// command cache
private String commandStringCache = "";
private volatile RpcCommand commandCache;
public CommandServiceManager(URL refUrl) {
if(logger.isInfoEnabled()) logger.info("CommandServiceManager init url:" + refUrl.toFullStr());
this.refUrl = refUrl;
notifySet = new ConcurrentHashSet<NotifyListener>();
groupServiceCache = new ConcurrentHashMap<String, List<URL>>();
}
@Override
public void notifyService(URL serviceUrl, URL registryUrl, List<URL> urls) {
if (registry == null) {
throw new FrameworkException(new Status(REGISTRY_IS_NULL));
}
URL urlCopy = serviceUrl.createCopy();
String groupName = urlCopy.getParameter(URLParamType.group.getName(), URLParamType.group.getValue());
groupServiceCache.put(groupName, urls);
List<URL> finalResult = new ArrayList<URL>();
if (commandCache != null) {
Map<String, Integer> weights = new HashMap<String, Integer>();
finalResult = discoverServiceWithCommand(refUrl, weights, commandCache);
} else {
if(logger.isInfoEnabled()) logger.info("command cache is null. service:" + serviceUrl.toSimpleString());
// if no command cache, return group
finalResult.addAll(discoverOneGroup(refUrl));
}
for (NotifyListener notifyListener : notifySet) {
notifyListener.notify(registry.getUrl(), finalResult);
}
}
@Override
public void notifyCommand(URL serviceUrl, String commandString) {
if(logger.isInfoEnabled()) logger.info("CommandServiceManager notify command. service:" + serviceUrl.toSimpleString() + ", command:" + commandString);
if (!SwitcherUtil.isOpen(LIGHT_COMMAND_SWITCHER) || commandString == null) {
if(logger.isInfoEnabled()) logger.info("command reset empty since swither is close.");
commandString = "";
}
List<URL> finalResult = new ArrayList<URL>();
URL urlCopy = serviceUrl.createCopy();
if (!StringUtils.equals(commandString, commandStringCache)) {
commandStringCache = commandString;
commandCache = RpcCommandUtil.stringToCommand(commandStringCache);
Map<String, Integer> weights = new HashMap<String, Integer>();
if (commandCache != null) {
commandCache.sort();
finalResult = discoverServiceWithCommand(refUrl, weights, commandCache);
} else {
// If command is abnormal, handle it just like there is no command to prevent wrong command
if (StringUtils.isNotBlank(commandString)) {
logger.warn("command parse fail, ignored! command:" + commandString);
commandString = "";
}
// If there is no command, return manager group
finalResult.addAll(discoverOneGroup(refUrl));
}
// when command is changed, delete cache and unsubscribe group
Set<String> groupKeys = groupServiceCache.keySet();
for (String gk : groupKeys) {
if (!weights.containsKey(gk)) {
groupServiceCache.remove(gk);
URL urlTemp = urlCopy.createCopy();
urlTemp.addParameter(URLParamType.group.getName(), gk);
registry.unsubscribeService(urlTemp, this);
}
}
} else {
if(logger.isInfoEnabled()) logger.info("command is not changed. url:" + serviceUrl.toSimpleString());
// command not changed do nothing
return;
}
for (NotifyListener notifyListener : notifySet) {
notifyListener.notify(registry.getUrl(), finalResult);
}
// when command is empty, trigger resub service
if ("".equals(commandString)) {
if(logger.isInfoEnabled()) logger.info("reSub service" + refUrl.toSimpleString());
registry.subscribeService(refUrl, this);
}
}
public List<URL> discoverServiceWithCommand(URL serviceUrl, Map<String, Integer> weights, RpcCommand rpcCommand) {
String localIP = NetUtils.getLocalAddress().getHostAddress();
return this.discoverServiceWithCommand(serviceUrl, weights, rpcCommand, localIP);
}
public List<URL> discoverServiceWithCommand(URL serviceUrl, Map<String, Integer> weights, RpcCommand rpcCommand, String localIP) {
if (rpcCommand == null || CollectionUtil.isEmpty(rpcCommand.getClientCommandList())) {
return discoverOneGroup(serviceUrl);
}
List<URL> mergedResult = new LinkedList<URL>();
String path = serviceUrl.getPath();
List<RpcCommand.ClientCommand> clientCommandList = rpcCommand.getClientCommandList();
boolean hit = false;
for (RpcCommand.ClientCommand command : clientCommandList) {
mergedResult = new LinkedList<URL>();
// check if current url matches
boolean match = RpcCommandUtil.match(command.getPattern(), path);
if (match) {
hit = true;
if (!CollectionUtil.isEmpty(command.getMergeGroups())) {
// calculate weight
try {
buildWeightsMap(weights, command);
} catch (FrameworkException e) {
logger.error("build weights map fail!" + e.getMessage());
continue;
}
// According to the result, discover each group's services and combine them together
mergedResult.addAll(mergeResult(serviceUrl, weights));
} else {
mergedResult.addAll(discoverOneGroup(serviceUrl));
}
if(logger.isInfoEnabled()) logger.info("mergedResult: size-" + mergedResult.size() + " --- " + mergedResult.toString());
if (!CollectionUtil.isEmpty(command.getRouteRules())) {
if(logger.isInfoEnabled()) logger.info("router: " + command.getRouteRules().toString());
for (String routeRule : command.getRouteRules()) {
String[] fromTo = routeRule.replaceAll("\\s+", "").split("to");
if (fromTo.length != 2) {
logger.warn("Invalid route rule configuration");
continue;
}
String from = fromTo[0];
String to = fromTo[1];
if (from.length() < 1 || to.length() < 1 || !IP_PATTERN.matcher(from).find() || !IP_PATTERN.matcher(to).find()) {
logger.warn("Invalid route rule configuration");
continue;
}
boolean oppositeFrom = from.startsWith("!");
boolean oppositeTo = to.startsWith("!");
if (oppositeFrom) {
from = from.substring(1);
}
if (oppositeTo) {
to = to.substring(1);
}
int idx = from.indexOf('*');
boolean matchFrom;
if (idx != -1) {
matchFrom = localIP.startsWith(from.substring(0, idx));
} else {
matchFrom = localIP.equals(from);
}
// prefixed with !,reverse
if (oppositeFrom) {
matchFrom = !matchFrom;
}
if(logger.isInfoEnabled()) logger.info("matchFrom: " + matchFrom + ", localip:" + localIP + ", from:" + from);
if (matchFrom) {
boolean matchTo;
Iterator<URL> iterator = mergedResult.iterator();
while (iterator.hasNext()) {
URL url = iterator.next();
if (url.getProtocol().equalsIgnoreCase("rule")) {
continue;
}
idx = to.indexOf('*');
if (idx != -1) {
matchTo = url.getHost().startsWith(to.substring(0, idx));
} else {
matchTo = url.getHost().equals(to);
}
if (oppositeTo) {
matchTo = !matchTo;
}
if (!matchTo) {
iterator.remove();
if(logger.isInfoEnabled()) logger.info("router To not match. url remove : " + url.toSimpleString());
}
}
}
}
}
// use the first one matched TODO Consider if this meet most user cases
break;
}
}
List<URL> finalResult = new ArrayList<URL>();
if (!hit) {
finalResult = discoverOneGroup(serviceUrl);
} else {
finalResult.addAll(mergedResult);
}
return finalResult;
}
private void buildWeightsMap(Map<String, Integer> weights, RpcCommand.ClientCommand command) {
for (String rule : command.getMergeGroups()) {
String[] gw = rule.split(":");
int weight = 1;
if (gw.length > 1) {
try {
weight = Integer.parseInt(gw[1]);
} catch (NumberFormatException e) {
throw new FrameworkException(new Status(WEIGHT_OUT_OF_RANGE, weight));
}
if (weight < 0 || weight > 100) {
throw new FrameworkException(new Status(WEIGHT_OUT_OF_RANGE, weight));
}
}
weights.put(gw[0], weight);
}
}
private List<URL> mergeResult(URL url, Map<String, Integer> weights) {
List<URL> finalResult = new ArrayList<URL>();
if (weights.size() > 1) {
// construct a rule url with all groups and added as first
URL ruleUrl = new URLImpl("rule", url.getHost(), url.getPort(), url.getPath());
StringBuilder weightsBuilder = new StringBuilder(64);
for (Map.Entry<String, Integer> entry : weights.entrySet()) {
weightsBuilder.append(entry.getKey()).append(':').append(entry.getValue()).append(',');
}
ruleUrl.addParameter(URLParamType.weights.getName(), weightsBuilder.deleteCharAt(weightsBuilder.length() - 1).toString());
finalResult.add(ruleUrl);
}
for (String key : weights.keySet()) {
if (groupServiceCache.containsKey(key)) {
finalResult.addAll(groupServiceCache.get(key));
} else {
URL urlTemp = url.createCopy();
urlTemp.addParameter(URLParamType.group.getName(), key);
finalResult.addAll(discoverOneGroup(urlTemp));
registry.subscribeService(urlTemp, this);
}
}
return finalResult;
}
private List<URL> discoverOneGroup(URL urlCopy) {
if(logger.isInfoEnabled()) logger.info("CommandServiceManager discover one group. url:" + urlCopy.toSimpleString());
String group = urlCopy.getParameter(URLParamType.group.getName(), URLParamType.group.getValue());
List<URL> list = groupServiceCache.get(group);
if (list == null) {
list = registry.discoverService(urlCopy);
groupServiceCache.put(group, list);
}
return list;
}
public void setCommandCache(String command) {
commandStringCache = command;
commandCache = RpcCommandUtil.stringToCommand(commandStringCache);
if(logger.isInfoEnabled()) logger.info("CommandServiceManager set commandcache. commandstring:" + commandStringCache + ", comandcache "
+ (commandCache == null ? "is null." : "is not null."));
}
public void addNotifyListener(NotifyListener notifyListener) {
notifySet.add(notifyListener);
}
public void removeNotifyListener(NotifyListener notifyListener) {
notifySet.remove(notifyListener);
}
public void setRegistry(CommandFailbackRegistry registry) {
this.registry = registry;
}
}